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:async';
import 'dart:convert'; import 'dart:convert';
import 'package:http/http.dart' as http; import 'package:http/http.dart' as http;
import 'package:vikunja_app/api/response.dart';
class Client { class Client {
final JsonDecoder _decoder = new JsonDecoder(); final JsonDecoder _decoder = new JsonDecoder();
@ -25,33 +26,45 @@ class Client {
'Content-Type': 'application/json' 'Content-Type': 'application/json'
}; };
Future<dynamic> get(String url) { Future<Response> get(String url,
return http [Map<String, List<String>> queryParameters]) {
.get('${this.base}$url', headers: _headers) // TODO: This could be moved to a seperate function
.then(_handleResponse); 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 return http
.delete('${this.base}$url', headers: _headers) .delete('${this.base}$url', headers: _headers)
.then(_handleResponse); .then(_handleResponse);
} }
Future<dynamic> post(String url, {dynamic body}) { Future<Response> post(String url, {dynamic body}) {
return http return http
.post('${this.base}$url', .post('${this.base}$url',
headers: _headers, body: _encoder.convert(body)) headers: _headers, body: _encoder.convert(body))
.then(_handleResponse); .then(_handleResponse);
} }
Future<dynamic> put(String url, {dynamic body}) { Future<Response> put(String url, {dynamic body}) {
return http return http
.put('${this.base}$url', .put('${this.base}$url',
headers: _headers, body: _encoder.convert(body)) headers: _headers, body: _encoder.convert(body))
.then(_handleResponse); .then(_handleResponse);
} }
dynamic _handleResponse(http.Response response) { Response _handleResponse(http.Response response) {
if (response.statusCode < 200 || if (response.statusCode < 200 ||
response.statusCode >= 400 || response.statusCode >= 400 ||
json == null) { json == null) {
@ -65,12 +78,14 @@ class Client {
throw new ApiException( throw new ApiException(
response.statusCode, response.request.url.toString()); 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 { class InvalidRequestApiException extends ApiException {
final String message; final String message;
InvalidRequestApiException(int errorCode, String path, this.message) InvalidRequestApiException(int errorCode, String path, this.message)
: super(errorCode, path); : 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 { class ApiException implements Exception {
final int errorCode; final int errorCode;
final String path; final String path;
ApiException(this.errorCode, this.path); ApiException(this.errorCode, this.path);
@override @override

View File

@ -12,7 +12,7 @@ class ListAPIService extends APIService implements ListService {
Future<TaskList> create(namespaceId, TaskList tl) { Future<TaskList> create(namespaceId, TaskList tl) {
return client return client
.put('/namespaces/$namespaceId/lists', body: tl.toJSON()) .put('/namespaces/$namespaceId/lists', body: tl.toJSON())
.then((map) => TaskList.fromJson(map)); .then((response) => TaskList.fromJson(response.body));
} }
@override @override
@ -22,25 +22,27 @@ class ListAPIService extends APIService implements ListService {
@override @override
Future<TaskList> get(int listId) { 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 @override
Future<List<TaskList>> getAll() { Future<List<TaskList>> getAll() {
return client.get('/lists').then( return client.get('/lists').then((response) =>
(list) => convertList(list, (result) => TaskList.fromJson(result))); convertList(response.body, (result) => TaskList.fromJson(result)));
} }
@override @override
Future<List<TaskList>> getByNamespace(int namespaceId) { Future<List<TaskList>> getByNamespace(int namespaceId) {
return client.get('/namespaces/$namespaceId/lists').then( return client.get('/namespaces/$namespaceId/lists').then((response) =>
(list) => convertList(list, (result) => TaskList.fromJson(result))); convertList(response.body, (result) => TaskList.fromJson(result)));
} }
@override @override
Future<TaskList> update(TaskList tl) { Future<TaskList> update(TaskList tl) {
return client return client
.post('/lists/${tl.id}', body: tl.toJSON()) .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) { Future<Namespace> create(Namespace ns) {
return client return client
.put('/namespaces', body: ns.toJSON()) .put('/namespaces', body: ns.toJSON())
.then((map) => Namespace.fromJson(map)); .then((response) => Namespace.fromJson(response.body));
} }
@override @override
@ -24,19 +24,19 @@ class NamespaceAPIService extends APIService implements NamespaceService {
Future<Namespace> get(int namespaceId) { Future<Namespace> get(int namespaceId) {
return client return client
.get('/namespaces/$namespaceId') .get('/namespaces/$namespaceId')
.then((map) => Namespace.fromJson(map)); .then((response) => Namespace.fromJson(response.body));
} }
@override @override
Future<List<Namespace>> getAll() { Future<List<Namespace>> getAll() {
return client.get('/namespaces').then( return client.get('/namespaces').then((response) =>
(list) => convertList(list, (result) => Namespace.fromJson(result))); convertList(response.body, (result) => Namespace.fromJson(result)));
} }
@override @override
Future<Namespace> update(Namespace ns) { Future<Namespace> update(Namespace ns) {
return client return client
.post('/namespaces/${ns.id}', body: ns.toJSON()) .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 'dart:async';
import 'package:vikunja_app/api/client.dart'; 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/api/service.dart';
import 'package:vikunja_app/models/task.dart'; import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/service/services.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) { Future<Task> add(int listId, Task task) {
return client return client
.put('/lists/$listId', body: task.toJSON()) .put('/lists/$listId', body: task.toJSON())
.then((map) => Task.fromJson(map)); .then((response) => Task.fromJson(response.body));
} }
@override @override
@ -24,6 +25,16 @@ class TaskAPIService extends APIService implements TaskService {
Future<Task> update(Task task) { Future<Task> update(Task task) {
return client return client
.post('/tasks/${task.id}', body: task.toJSON()) .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: { var token = await client.post('/login', body: {
'username': username, 'username': username,
'password': password 'password': password
}).then((map) => map['token']); }).then((response) => response.body['token']);
return UserAPIService(Client(token, client.base)) return UserAPIService(Client(token, client.base))
.getCurrentUser() .getCurrentUser()
.then((user) => UserTokenPair(user, token)); .then((user) => UserTokenPair(user, token));
@ -25,12 +25,12 @@ class UserAPIService extends APIService implements UserService {
'username': username, 'username': username,
'email': email, 'email': email,
'password': password 'password': password
}).then((resp) => resp['username']); }).then((response) => response.body['username']);
return login(newUser, password); return login(newUser, password);
} }
@override @override
Future<User> getCurrentUser() { 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:meta/meta.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/models/user.dart'; import 'package:vikunja_app/models/user.dart';
class TaskList { class TaskList {
@ -7,7 +6,6 @@ class TaskList {
final String title, description; final String title, description;
final User owner; final User owner;
final DateTime created, updated; final DateTime created, updated;
final List<Task> tasks;
TaskList( TaskList(
{@required this.id, {@required this.id,
@ -15,8 +13,7 @@ class TaskList {
this.description, this.description,
this.owner, this.owner,
this.created, this.created,
this.updated, this.updated});
this.tasks});
TaskList.fromJson(Map<String, dynamic> json) TaskList.fromJson(Map<String, dynamic> json)
: id = json['id'], : id = json['id'],
@ -24,10 +21,7 @@ class TaskList {
description = json['description'], description = json['description'],
title = json['title'], title = json['title'],
updated = DateTime.parse(json['updated']), updated = DateTime.parse(json['updated']),
created = DateTime.parse(json['created']), created = DateTime.parse(json['created']);
tasks = (json['tasks'] == null ? [] : json['tasks'] as List<dynamic>)
?.map((taskJson) => Task.fromJson(taskJson))
?.toList();
toJSON() { toJSON() {
return { return {

View File

@ -27,12 +27,16 @@ class Task {
reminders = (json['reminder_dates'] as List<dynamic>) reminders = (json['reminder_dates'] as List<dynamic>)
?.map((r) => DateTime.parse(r)) ?.map((r) => DateTime.parse(r))
?.toList(), ?.toList(),
due = due = json['due_date'].toString() == 'null'
json['due_date'] != null ? DateTime.parse(json['due_date']) : null, ? null
: DateTime.parse(json['due_date']),
description = json['description'], description = json['description'],
title = json['title'], title = json['title'],
done = json['done'], 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() => { toJSON() => {
'id': id, 'id': id,

View File

@ -1,12 +1,12 @@
import 'dart:async'; import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:vikunja_app/components/AddDialog.dart'; import 'package:vikunja_app/components/AddDialog.dart';
import 'package:vikunja_app/components/TaskTile.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/list.dart';
import 'package:vikunja_app/models/task.dart';
import 'package:vikunja_app/pages/list/list_edit.dart'; import 'package:vikunja_app/pages/list/list_edit.dart';
import 'package:vikunja_app/stores/list_store.dart';
class ListPage extends StatefulWidget { class ListPage extends StatefulWidget {
final TaskList taskList; final TaskList taskList;
@ -19,24 +19,18 @@ class ListPage extends StatefulWidget {
class _ListPageState extends State<ListPage> { class _ListPageState extends State<ListPage> {
TaskList _list; TaskList _list;
List<Task> _loadingTasks = []; int _currentPage = 1;
bool _loading = true;
@override @override
void initState() { void initState() {
_list = TaskList( _list = TaskList(id: widget.taskList.id, title: widget.taskList.title);
id: widget.taskList.id, title: widget.taskList.title, tasks: []); Future.microtask(() => _loadList());
super.initState(); super.initState();
} }
@override
void didChangeDependencies() {
super.didChangeDependencies();
_loadList();
}
@override @override
Widget build(BuildContext context) { Widget build(BuildContext context) {
final taskState = Provider.of<ListProvider>(context);
return Scaffold( return Scaffold(
appBar: AppBar( appBar: AppBar(
title: new Text(_list.title), title: new Text(_list.title),
@ -51,15 +45,31 @@ class _ListPageState extends State<ListPage> {
)))) ))))
], ],
), ),
body: !this._loading body: !taskState.isLoading
? RefreshIndicator( ? RefreshIndicator(
child: _list.tasks.length > 0 child: taskState.tasks.length > 0
? ListView( ? ListView.builder(
padding: EdgeInsets.symmetric(vertical: 8.0), padding: EdgeInsets.symmetric(vertical: 8.0),
children: ListTile.divideTiles( itemBuilder: (context, i) {
context: context, tiles: _listTasks()) if (i.isOdd) return Divider();
.toList(),
) 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.')), : Center(child: Text('This list is empty.')),
onRefresh: _loadList, 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() { Future<void> _loadList() async {
var tasks = (_list?.tasks?.map(_buildTile) ?? []).toList(); _loadTasksForPage(1);
tasks.addAll(_loadingTasks.map(_buildLoadingTile));
return tasks;
} }
TaskTile _buildTile(Task task) { void _loadTasksForPage(int page) {
return TaskTile(task: task, loading: false); Provider.of<ListProvider>(context, listen: false).loadTasks(
} context: context,
listId: _list.id,
TaskTile _buildLoadingTile(Task task) { page: page,
return TaskTile(
task: task,
loading: true,
); );
} }
Future<void> _loadList() {
return VikunjaGlobal.of(context)
.listService
.get(widget.taskList.id)
.then((list) {
setState(() {
_loading = false;
_list = list;
});
});
}
_addItemDialog(BuildContext context) { _addItemDialog(BuildContext context) {
showDialog( showDialog(
context: context, context: context,
builder: (_) => AddDialog( builder: (_) => AddDialog(
onAdd: (name) => _addItem(name, context), onAdd: (title) => _addItem(title, context),
decoration: new InputDecoration( decoration: new InputDecoration(
labelText: 'Task Name', hintText: 'eg. Milk'))); labelText: 'Task Name', hintText: 'eg. Milk')));
} }
_addItem(String name, BuildContext context) { _addItem(String title, BuildContext context) {
var globalState = VikunjaGlobal.of(context); Provider.of<ListProvider>(context, listen: false)
var newTask = Task( .addTask(context: context, title: title, listId: _list.id)
id: null, title: name, owner: globalState.currentUser, done: false); .then((_) {
setState(() => _loadingTasks.add(newTask));
globalState.taskService.add(_list.id, newTask).then((task) {
setState(() {
_list.tasks.add(task);
});
}).then((_) {
_loadList();
setState(() => _loadingTasks.remove(newTask));
Scaffold.of(context).showSnackBar(SnackBar( Scaffold.of(context).showSnackBar(SnackBar(
content: Text('The task was added successfully!'), content: Text('The task was added successfully!'),
)); ));

View File

@ -3,12 +3,14 @@ import 'dart:async';
import 'package:flutter/cupertino.dart'; import 'package:flutter/cupertino.dart';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:after_layout/after_layout.dart'; import 'package:after_layout/after_layout.dart';
import 'package:provider/provider.dart';
import 'package:vikunja_app/components/AddDialog.dart'; import 'package:vikunja_app/components/AddDialog.dart';
import 'package:vikunja_app/global.dart'; import 'package:vikunja_app/global.dart';
import 'package:vikunja_app/models/list.dart'; import 'package:vikunja_app/models/list.dart';
import 'package:vikunja_app/models/namespace.dart'; import 'package:vikunja_app/models/namespace.dart';
import 'package:vikunja_app/pages/list/list.dart'; import 'package:vikunja_app/pages/list/list.dart';
import 'package:vikunja_app/stores/list_store.dart';
class NamespacePage extends StatefulWidget { class NamespacePage extends StatefulWidget {
final Namespace namespace; final Namespace namespace;
@ -89,6 +91,7 @@ class _NamespacePageState extends State<NamespacePage>
} }
Future<void> _loadLists() { 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) return VikunjaGlobal.of(context)
.listService .listService
.getByNamespace(widget.namespace.id) .getByNamespace(widget.namespace.id)
@ -99,8 +102,14 @@ class _NamespacePageState extends State<NamespacePage>
} }
_openList(BuildContext context, TaskList list) { _openList(BuildContext context, TaskList list) {
Navigator.of(context).push( Navigator.of(context).push(MaterialPageRoute(
MaterialPageRoute(builder: (context) => ListPage(taskList: list))); 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) { _addListDialog(BuildContext context) {
@ -116,7 +125,7 @@ class _NamespacePageState extends State<NamespacePage>
_addList(String name, BuildContext context) { _addList(String name, BuildContext context) {
VikunjaGlobal.of(context) VikunjaGlobal.of(context)
.listService .listService
.create(widget.namespace.id, TaskList(id: null, title: name, tasks: [])) .create(widget.namespace.id, TaskList(id: null, title: name))
.then((_) { .then((_) {
setState(() {}); setState(() {});
_loadLists(); _loadLists();

View File

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

View File

@ -1,5 +1,6 @@
import 'dart:async'; import 'dart:async';
import 'package:vikunja_app/api/response.dart';
import 'package:vikunja_app/models/list.dart'; import 'package:vikunja_app/models/list.dart';
import 'package:vikunja_app/models/namespace.dart'; import 'package:vikunja_app/models/namespace.dart';
import 'package:vikunja_app/models/task.dart'; import 'package:vikunja_app/models/task.dart';
@ -26,6 +27,8 @@ abstract class TaskService {
Future<Task> update(Task task); Future<Task> update(Task task);
Future delete(int taskId); Future delete(int taskId);
Future<Task> add(int listId, Task task); Future<Task> add(int listId, Task task);
Future<Response> getAll(int listId,
[Map<String, List<String>> queryParameters]);
} }
abstract class UserService { 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" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "1.1.8" version: "1.1.8"
nested:
dependency: transitive
description:
name: nested
url: "https://pub.dartlang.org"
source: hosted
version: "0.0.4"
path: path:
dependency: transitive dependency: transitive
description: description:
@ -160,6 +167,13 @@ packages:
url: "https://pub.dartlang.org" url: "https://pub.dartlang.org"
source: hosted source: hosted
version: "2.4.0" version: "2.4.0"
provider:
dependency: "direct main"
description:
name: provider
url: "https://pub.dartlang.org"
source: hosted
version: "4.0.5+1"
quiver: quiver:
dependency: transitive dependency: transitive
description: description:
@ -258,3 +272,4 @@ packages:
version: "2.2.0" version: "2.2.0"
sdks: sdks:
dart: ">=2.6.0 <3.0.0" dart: ">=2.6.0 <3.0.0"
flutter: ">=1.12.1"

View File

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