Let’s now slightly rethink our query. Instead of “fetching all tasks for user 1” we are going to “fetch user 1 with all their tasks”.
Flutter Data has first-class support for relationships.
First, in models/user.dart
, we’ll create the User
model with a HasMany<Task>
relationship:
import 'package:flutter_data/flutter_data.dart';
import 'package:json_annotation/json_annotation.dart';
import 'task.dart';
part 'user.g.dart';
@JsonSerializable()
@DataRepository([JsonServerAdapter])
class User extends DataModel<User> {
@override
final int? id;
final String name;
final HasMany<Task> tasks;
User({this.id, required this.name, required this.tasks});
}
Time to run code generation and get a brand-new Repository<User>
:
flutter pub run build_runner build
Great. We are now going to issue the remote request via watchOne()
, in order to list (and watch for changes of) user 1
’s Task
models:
params: {'_embed': 'tasks'},
tells the server to include this user’s tasks (which our JSON adapter knows how to deserialize)alsoWatch: (user) => [user.tasks]
tells the watcher to rebuild the widget any time user or its tasks are updated or deleted; any number of relationships of any depth can be watched. (For instance, alsoWatch: (user) => [user.tasks, user.tasks.comments, user.tasks.comments.owner]
)class TasksScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final _newTaskController = useTextEditingController();
final state = ref.users.watchOne(
1, // user ID, an integer
params: {'_embed': 'tasks'}, // HTTP param
alsoWatch: (user) => [user.tasks] // watcher
);
if (state.isLoading) {
return CircularProgressIndicator();
}
final tasks = state.model!.tasks.toList();
return RefreshIndicator(
onRefresh: () => ref.users.findOne(1, params: {'_embed': 'tasks'}),
child: ListView(
children: [
TextField(
controller: _newTaskController,
onSubmitted: (value) async {
Task(title: value).save();
_newTaskController.clear();
},
),
for (final task in tasks)
Dismissible(
key: ValueKey(task),
direction: DismissDirection.endToStart,
onDismissed: (_) => task.delete(),
child: ListTile(
leading: Checkbox(
value: task.completed,
onChanged: (value) => task.toggleCompleted().save(),
),
title: Text('${task.title} [id: ${task.id}]'),
),
),
],
),
);
}
}
Import the user.dart
file, reload and watch it working!
Note that tasks 4
, 5
and 9
for example were not loaded as they do not belong to user 1
!
This is the API response for https://my-json-server.typicode.com/flutterdata/demo/users/1?_embed=tasks that was parsed by the built-in deserialize
method:
{
"id": 1,
"name": "frank06",
"tasks": [
{
"id": 1,
"title": "Laundry ๐งบ",
"completed": false,
"userId": 1
},
{
"id": 2,
"title": "Groceries ๐",
"completed": true,
"userId": 1
},
{
"id": 3,
"title": "Reservation at Malloys",
"completed": true,
"userId": 1
},
{
"id": 7,
"title": "Take Amanda to birthday",
"completed": true,
"userId": 1
},
{
"id": 8,
"title": "Get new surfboard ๐โโ๏ธ",
"completed": false,
"userId": 1
},
{
"id": 10,
"title": "Protest tyrannical mandates ๐",
"completed": true,
"userId": 1
}
]
}
As it is, adding a new task will not work. Why is that?
We are creating new Task
models without any User
associated to them:
onSubmitted: (value) async {
Task(title: value).save();
_newTaskController.clear();
},
Let’s fix this. Add a BelongsTo<User>
relationship in models/task.dart
and regenerate our code:
@JsonSerializable()
@DataRepository([JsonServerAdapter])
class Task extends DataModel<Task> {
@override
final int? id;
final String title;
final bool completed;
final BelongsTo<User> user;
Task({this.id, required this.title, this.completed = false, required this.user});
Task toggleCompleted() {
return Task(id: this.id, title: this.title, user: user, completed: !this.completed)
.withKeyOf(this);
}
}
Now we can provide the right user as a BelongsTo
:
class TasksScreen extends HookConsumerWidget {
@override
Widget build(BuildContext context, WidgetRef ref) {
final _newTaskController = useTextEditingController();
final state = ref.users.watchOne(
1, // user ID, an integer
params: {'_embed': 'tasks'}, // HTTP param
alsoWatch: (user) => [user.tasks] // watcher
);
if (state.isLoading) {
return CircularProgressIndicator();
}
final user = state.model!;
final tasks = user.tasks.toList();
return RefreshIndicator(
onRefresh: () => ref.users.findOne(1, params: {'_embed': 'tasks'}),
child: ListView(
children: [
TextField(
controller: _newTaskController,
onSubmitted: (value) async {
Task(title: value, user: BelongsTo(user)).save();
_newTaskController.clear();
},
),
for (final task in tasks)
Dismissible(
key: ValueKey(task),
direction: DismissDirection.endToStart,
onDismissed: (_) => task.delete(),
child: ListTile(
leading: Checkbox(
value: task.completed,
onChanged: (value) => task.toggleCompleted().save(),
),
title: Text('${task.title} [id: ${task.id}]'),
),
),
],
),
);
}
}
And adding new tasks now works!