Re-implemented getting tasks with the new seperate endpoint #39

Open
konrad wants to merge 34 commits from feature/task-list into master
15 changed files with 210 additions and 106 deletions

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:convert';
import 'package:http/http.dart' as http;
import 'package:vikunja_app/api/response.dart';
class Client {
final JsonDecoder _decoder = new JsonDecoder();
@ -25,33 +26,45 @@ class Client {
'Content-Type': 'application/json'
};
Future<dynamic> get(String url) {
return http
.get('${this.base}$url', headers: _headers)
.then(_handleResponse);
Future<Response> get(String url,
[Map<String, List<String>> queryParameters]) {
// TODO: This could be moved to a seperate function
var uri = Uri.parse('${this.base}$url');
// Because these are all final values, we can't just add the queryParameters and must instead build a new Uri Object every time this method is called.
var newUri = Uri(
scheme: uri.scheme,
userInfo: uri.userInfo,
host: uri.host,
port: uri.port,
path: uri.path,
query: uri.query,
queryParameters: queryParameters,
// Because dart takes a Map<String, String> here, it is only possible to sort by one parameter while the api supports n parameters.
fragment: uri.fragment);
return http.get(newUri, headers: _headers).then(_handleResponse);
}
Future<dynamic> delete(String url) {
Future<Response> delete(String url) {
return http
.delete('${this.base}$url', headers: _headers)
.then(_handleResponse);
}
Future<dynamic> post(String url, {dynamic body}) {
Future<Response> post(String url, {dynamic body}) {
return http
.post('${this.base}$url',
headers: _headers, body: _encoder.convert(body))
.then(_handleResponse);
}
Future<dynamic> put(String url, {dynamic body}) {
Future<Response> put(String url, {dynamic body}) {
return http
.put('${this.base}$url',
headers: _headers, body: _encoder.convert(body))
.then(_handleResponse);
}
dynamic _handleResponse(http.Response response) {
Response _handleResponse(http.Response response) {
if (response.statusCode < 200 ||
response.statusCode >= 400 ||
json == null) {
@ -65,12 +78,14 @@ class Client {
throw new ApiException(
response.statusCode, response.request.url.toString());
}
return _decoder.convert(response.body);
return new Response(
_decoder.convert(response.body), response.statusCode, response.headers);
}
}
class InvalidRequestApiException extends ApiException {
final String message;
InvalidRequestApiException(int errorCode, String path, this.message)
: super(errorCode, path);

Please save this in stores. This shouldn't be handled by the client.

Please save this in stores. This shouldn't be handled by the client.
Review

Done!

Done!
@ -83,6 +98,7 @@ class InvalidRequestApiException extends ApiException {
class ApiException implements Exception {
final int errorCode;
final String path;
ApiException(this.errorCode, this.path);
@override

View File

@ -12,7 +12,7 @@ class ListAPIService extends APIService implements ListService {
Future<TaskList> create(namespaceId, TaskList tl) {
return client
.put('/namespaces/$namespaceId/lists', body: tl.toJSON())
.then((map) => TaskList.fromJson(map));
.then((response) => TaskList.fromJson(response.body));
}
@override
@ -22,25 +22,27 @@ class ListAPIService extends APIService implements ListService {
@override
Future<TaskList> get(int listId) {
return client.get('/lists/$listId').then((map) => TaskList.fromJson(map));
return client
.get('/lists/$listId')
.then((response) => TaskList.fromJson(response.body));
}
@override
Future<List<TaskList>> getAll() {
return client.get('/lists').then(
(list) => convertList(list, (result) => TaskList.fromJson(result)));
return client.get('/lists').then((response) =>
convertList(response.body, (result) => TaskList.fromJson(result)));
}
@override
Future<List<TaskList>> getByNamespace(int namespaceId) {
return client.get('/namespaces/$namespaceId/lists').then(
(list) => convertList(list, (result) => TaskList.fromJson(result)));
return client.get('/namespaces/$namespaceId/lists').then((response) =>
convertList(response.body, (result) => TaskList.fromJson(result)));
}
@override
Future<TaskList> update(TaskList tl) {
return client
.post('/lists/${tl.id}', body: tl.toJSON())
.then((map) => TaskList.fromJson(map));
.then((response) => TaskList.fromJson(response.body));
}
}

View File

@ -12,7 +12,7 @@ class NamespaceAPIService extends APIService implements NamespaceService {
Future<Namespace> create(Namespace ns) {
return client
.put('/namespaces', body: ns.toJSON())
.then((map) => Namespace.fromJson(map));
.then((response) => Namespace.fromJson(response.body));
}
@override
@ -24,19 +24,19 @@ class NamespaceAPIService extends APIService implements NamespaceService {
Future<Namespace> get(int namespaceId) {
return client
.get('/namespaces/$namespaceId')
.then((map) => Namespace.fromJson(map));
.then((response) => Namespace.fromJson(response.body));
}
@override
Future<List<Namespace>> getAll() {
return client.get('/namespaces').then(
(list) => convertList(list, (result) => Namespace.fromJson(result)));
return client.get('/namespaces').then((response) =>
convertList(response.body, (result) => Namespace.fromJson(result)));
}
@override
Future<Namespace> update(Namespace ns) {
return client
.post('/namespaces/${ns.id}', body: ns.toJSON())
.then((map) => Namespace.fromJson(map));
.then((response) => Namespace.fromJson(response.body));
}
}

9
lib/api/response.dart Normal file
View File

@ -0,0 +1,9 @@
// This is a wrapper class to be able to return the headers up to the provider
// to properly handle things like pagination with it.
class Response {
Response(this.body, this.statusCode, this.headers);
final dynamic body;
final int statusCode;
final Map<String, String> headers;
}

View File

@ -1,6 +1,7 @@
import 'dart:async';
import 'package:vikunja_app/api/client.dart';
import 'package:vikunja_app/api/response.dart';
import 'package:vikunja_app/api/service.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/service/services.dart';
@ -12,7 +13,7 @@ class TaskAPIService extends APIService implements TaskService {
Future<Task> add(int listId, Task task) {
return client
.put('/lists/$listId', body: task.toJSON())
.then((map) => Task.fromJson(map));
.then((response) => Task.fromJson(response.body));
}
@override
@ -24,6 +25,16 @@ class TaskAPIService extends APIService implements TaskService {
Future<Task> update(Task task) {
return client
.post('/tasks/${task.id}', body: task.toJSON())
.then((map) => Task.fromJson(map));
.then((response) => Task.fromJson(response.body));
}
@override
Future<Response> getAll(int listId,
[Map<String, List<String>> queryParameters]) {
return client.get('/lists/$listId/tasks', queryParameters).then(
(response) => new Response(
convertList(response.body, (result) => Task.fromJson(result)),
response.statusCode,
response.headers));
}
}

View File

@ -13,7 +13,7 @@ class UserAPIService extends APIService implements UserService {
var token = await client.post('/login', body: {
'username': username,
'password': password
}).then((map) => map['token']);
}).then((response) => response.body['token']);
return UserAPIService(Client(token, client.base))
.getCurrentUser()
.then((user) => UserTokenPair(user, token));
@ -25,12 +25,12 @@ class UserAPIService extends APIService implements UserService {
'username': username,
'email': email,
'password': password
}).then((resp) => resp['username']);
}).then((response) => response.body['username']);
return login(newUser, password);
}
@override
Future<User> getCurrentUser() {
return client.get('/user').then((map) => User.fromJson(map));
return client.get('/user').then((response) => User.fromJson(response.body));
}
}

View File

@ -1,5 +1,4 @@
import 'package:meta/meta.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/models/user.dart';
class TaskList {
@ -7,7 +6,6 @@ class TaskList {
final String title, description;
final User owner;
final DateTime created, updated;
final List<Task> tasks;
TaskList(
{@required this.id,
@ -15,8 +13,7 @@ class TaskList {
this.description,
this.owner,
this.created,
this.updated,
this.tasks});
this.updated});
TaskList.fromJson(Map<String, dynamic> json)
: id = json['id'],
@ -24,10 +21,7 @@ class TaskList {
description = json['description'],
title = json['title'],
updated = DateTime.parse(json['updated']),
created = DateTime.parse(json['created']),
tasks = (json['tasks'] == null ? [] : json['tasks'] as List<dynamic>)
?.map((taskJson) => Task.fromJson(taskJson))
?.toList();
created = DateTime.parse(json['created']);
toJSON() {
return {

View File

@ -27,12 +27,16 @@ class Task {
reminders = (json['reminder_dates'] as List<dynamic>)
?.map((r) => DateTime.parse(r))
?.toList(),
due =
json['due_date'] != null ? DateTime.parse(json['due_date']) : null,
due = json['due_date'].toString() == 'null'
? null
: DateTime.parse(json['due_date']),
description = json['description'],
title = json['title'],
done = json['done'],
owner = User.fromJson(json['created_by']);
owner = json['created_by'].toString() == "null"
? null
: User.fromJson(json[
'created_by']); // There has to be a better way of doing this...
toJSON() => {
'id': id,

View File

@ -1,12 +1,12 @@
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:vikunja_app/components/AddDialog.dart';
import 'package:vikunja_app/components/TaskTile.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/list.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/pages/list/list_edit.dart';
import 'package:vikunja_app/stores/list_store.dart';
class ListPage extends StatefulWidget {
final TaskList taskList;
@ -19,24 +19,18 @@ class ListPage extends StatefulWidget {
class _ListPageState extends State<ListPage> {
TaskList _list;
List<Task> _loadingTasks = [];
bool _loading = true;
int _currentPage = 1;
@override
void initState() {
_list = TaskList(
id: widget.taskList.id, title: widget.taskList.title, tasks: []);
_list = TaskList(id: widget.taskList.id, title: widget.taskList.title);
Future.microtask(() => _loadList());
super.initState();
}
@override
void didChangeDependencies() {
super.didChangeDependencies();
_loadList();
}
@override
Widget build(BuildContext context) {
final taskState = Provider.of<ListProvider>(context);
return Scaffold(
appBar: AppBar(
title: new Text(_list.title),
@ -51,15 +45,31 @@ class _ListPageState extends State<ListPage> {
))))
],
),
body: !this._loading
body: !taskState.isLoading
? RefreshIndicator(
child: _list.tasks.length > 0
? ListView(
child: taskState.tasks.length > 0
? ListView.builder(
padding: EdgeInsets.symmetric(vertical: 8.0),
children: ListTile.divideTiles(
context: context, tiles: _listTasks())
.toList(),
)
itemBuilder: (context, i) {
if (i.isOdd) return Divider();
final index = i ~/ 2;
// This handles the case if there are no more elements in the list left which can be provided by the api
if (taskState.maxPages == _currentPage &&
index == taskState.tasks.length - 1) return null;
if (index >= taskState.tasks.length &&
_currentPage < taskState.maxPages) {
_currentPage++;
_loadTasksForPage(_currentPage);
}
return index < taskState.tasks.length
? TaskTile(
task: taskState.tasks[index],
)
: null;
})
: Center(child: Text('This list is empty.')),
onRefresh: _loadList,
)
@ -70,56 +80,31 @@ class _ListPageState extends State<ListPage> {
));

Please create a custom widget which handles auto loading new tasks and inserting dividers.

Please create a custom widget which handles auto loading new tasks and inserting dividers.
}
List<Widget> _listTasks() {
var tasks = (_list?.tasks?.map(_buildTile) ?? []).toList();
tasks.addAll(_loadingTasks.map(_buildLoadingTile));
return tasks;
Future<void> _loadList() async {
_loadTasksForPage(1);
}
TaskTile _buildTile(Task task) {
return TaskTile(task: task, loading: false);
}
TaskTile _buildLoadingTile(Task task) {
return TaskTile(
task: task,
loading: true,
void _loadTasksForPage(int page) {
Provider.of<ListProvider>(context, listen: false).loadTasks(
context: context,
listId: _list.id,
page: page,
);
}
Future<void> _loadList() {
return VikunjaGlobal.of(context)
.listService
.get(widget.taskList.id)
.then((list) {
setState(() {
_loading = false;
_list = list;
});
});
}
_addItemDialog(BuildContext context) {
showDialog(
context: context,
builder: (_) => AddDialog(
onAdd: (name) => _addItem(name, context),
onAdd: (title) => _addItem(title, context),
decoration: new InputDecoration(
labelText: 'Task Name', hintText: 'eg. Milk')));
}
_addItem(String name, BuildContext context) {
var globalState = VikunjaGlobal.of(context);
var newTask = Task(
id: null, title: name, owner: globalState.currentUser, done: false);
setState(() => _loadingTasks.add(newTask));
globalState.taskService.add(_list.id, newTask).then((task) {
setState(() {
_list.tasks.add(task);
});
}).then((_) {
_loadList();
setState(() => _loadingTasks.remove(newTask));
_addItem(String title, BuildContext context) {
Provider.of<ListProvider>(context, listen: false)
.addTask(context: context, title: title, listId: _list.id)
.then((_) {
Scaffold.of(context).showSnackBar(SnackBar(
content: Text('The task was added successfully!'),
));

View File

@ -3,12 +3,14 @@ import 'dart:async';
import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart';
import 'package:after_layout/after_layout.dart';
import 'package:provider/provider.dart';
import 'package:vikunja_app/components/AddDialog.dart';
import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/list.dart';
import 'package:vikunja_app/models/namespace.dart';
import 'package:vikunja_app/pages/list/list.dart';
import 'package:vikunja_app/stores/list_store.dart';
class NamespacePage extends StatefulWidget {
final Namespace namespace;
@ -89,6 +91,7 @@ class _NamespacePageState extends State<NamespacePage>
}
Future<void> _loadLists() {
// FIXME: This is called even when the tasks on a list are loaded - which is not needed at all
return VikunjaGlobal.of(context)
.listService
.getByNamespace(widget.namespace.id)
@ -99,8 +102,14 @@ class _NamespacePageState extends State<NamespacePage>
}
_openList(BuildContext context, TaskList list) {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => ListPage(taskList: list)));
Navigator.of(context).push(MaterialPageRoute(
builder: (context) => ChangeNotifierProvider<ListProvider>(
create: (_) => new ListProvider(),
child: ListPage(
taskList: list,
),
),
Review

Maybe init the provider in the ListPage? Make sure to use Builder as child when doing it.

Maybe init the provider in the ListPage? Make sure to use Builder as child when doing it.
Review

You mean in the build method of it?

You mean in the `build` method of it?
));
}
_addListDialog(BuildContext context) {
@ -116,7 +125,7 @@ class _NamespacePageState extends State<NamespacePage>
_addList(String name, BuildContext context) {
VikunjaGlobal.of(context)
.listService
.create(widget.namespace.id, TaskList(id: null, title: name, tasks: []))
.create(widget.namespace.id, TaskList(id: null, title: name))
.then((_) {
setState(() {});
_loadLists();

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:vikunja_app/api/response.dart';
import 'package:vikunja_app/models/list.dart';
import 'package:vikunja_app/models/namespace.dart';
import 'package:vikunja_app/models/task.dart';
@ -28,7 +29,6 @@ var _lists = {
1: TaskList(
id: 1,
title: 'List 1',
tasks: _tasks.values.toList(),
owner: _users[1],
description: 'A nice list',
created: DateTime.now(),
@ -120,20 +120,13 @@ class MockedListService implements ListService {
class MockedTaskService implements TaskService {
@override
Future delete(int taskId) {
_lists.forEach(
(_, list) => list.tasks.removeWhere((task) => task.id == taskId));
_tasks.remove(taskId);
return Future.value();
}
@override
Future<Task> update(Task task) {
_lists.forEach((_, list) {
if (list.tasks.where((t) => t.id == task.id).length > 0) {
list.tasks.removeWhere((t) => t.id == task.id);
list.tasks.add(task);
}
});
_tasks[task.id] = task;
return Future.value(_tasks[task.id] = task);
}
@ -141,9 +134,17 @@ class MockedTaskService implements TaskService {
Future<Task> add(int listId, Task task) {
var id = _tasks.keys.last + 1;
_tasks[id] = task;
_lists[listId].tasks.add(task);
return Future.value(task);
}
@override
Future<Response> getAll(int listId,
[Map<String, List<String>> queryParameters]) {
return Future.value(new Response(_tasks.values.toList(), 200, {}));
}
@override
int get maxPages => 1;
}
class MockedUserService implements UserService {

View File

@ -1,5 +1,6 @@
import 'dart:async';
import 'package:vikunja_app/api/response.dart';
import 'package:vikunja_app/models/list.dart';
import 'package:vikunja_app/models/namespace.dart';
import 'package:vikunja_app/models/task.dart';
@ -26,6 +27,8 @@ abstract class TaskService {
Future<Task> update(Task task);
Future delete(int taskId);
Future<Task> add(int listId, Task task);
Future<Response> getAll(int listId,
[Map<String, List<String>> queryParameters]);
}
abstract class UserService {

View File

@ -0,0 +1,54 @@
import 'package:flutter/material.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/global.dart';
class ListProvider with ChangeNotifier {
bool _isLoading = false;
int _maxPages = 0;
// TODO: Streams
List<Task> _tasks = [];
bool get isLoading => _isLoading;
int get maxPages => _maxPages;
set tasks(List<Task> tasks) {
_tasks = tasks;
notifyListeners();
}
List<Task> get tasks => _tasks;
void loadTasks({BuildContext context, int listId, int page = 1}) {
_isLoading = true;
notifyListeners();
VikunjaGlobal.of(context).taskService.getAll(listId, {
"sort_by": ["done", "id"],
"order_by": ["asc", "desc"],
"page": [page.toString()]
}).then((response) {
if (response.headers["x-pagination-total-pages"] != null) {
_maxPages = int.parse(response.headers["x-pagination-total-pages"]);
}
_tasks.addAll(response.body);
_isLoading = false;
notifyListeners();
});
}
Future<void> addTask({BuildContext context, String title, int listId}) {
var globalState = VikunjaGlobal.of(context);
var newTask = Task(
id: null, title: title, owner: globalState.currentUser, done: false);
_isLoading = true;
notifyListeners();
return globalState.taskService.add(listId, newTask).then((task) {
_tasks.insert(0, task);
_isLoading = false;
notifyListeners();
});
}
}

View File

@ -139,6 +139,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "1.1.8"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4"
path:
dependency: transitive
description:
@ -160,6 +167,13 @@ packages:
url: "https://pub.dartlang.org"
source: hosted
version: "2.4.0"
provider:
dependency: "direct main"
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.5+1"
quiver:
dependency: transitive
description:
@ -258,3 +272,4 @@ packages:
version: "2.2.0"
sdks:
dart: ">=2.6.0 <3.0.0"
flutter: ">=1.12.1"

View File

@ -14,6 +14,7 @@ dependencies:
http: ^0.12.1
after_layout: ^1.0.7
sentry: ^3.0.1
provider: ^4.0.5
dev_dependencies:
flutter_test: