Creating nested routes
Let's say that we want to add a delete user feature to our application. We need a way to access the user we want to delete and ask the system to actually delete it.
Create a parametric route
To access our user we need to create a new route that has a dynamic path,
something like /users/:user_id
.
Let's start by creating a new route called user_id
with the CLI:
gazelle create route
Open up the newly created route and edit the code to this:
final userId = GazelleRoute.parameter(
name: "user_id",
).get(userIdGet);
The parameter
constructor sets the current route as a dynamic route, we can
access the actual value of the route from the request parameter inside our
handler.
Update our handler
Now we need to update the default handler to be a DELETE
handler.
Rename the handler file to user_id_delete.dart
, and update it with this code:
import 'package:gazelle_core/gazelle_core.dart';
import 'package:models/models.dart';
import 'package:server/users_collection.dart';
Future<GazelleResponse<User>> userIdDelete(
GazelleContext context,
GazelleRequest request,
) async {
// Get the path parameter from the request.
final userId = request.pathParameters["user_id"]!;
final deletedUser = usersCollection.singleWhere((user) => user.id == userId);
usersCollection.remove(deletedUser);
return GazelleResponse(
statusCode: GazelleHttpStatusCode.success.ok_200,
body: deletedUser,
);
}
The last thing to do is to make this handler a DELETE handler, update the route code to this:
final userId = GazelleRoute.parameter(
name: "user_id",
).delete(userIdGet);
Attach the new route to the users
route
To register the user_id
route under the users
route, update the users.dart
route with this code:
import 'package:gazelle_core/gazelle_core.dart';
import 'package:server/routes/user_id/user_id.dart';
import 'package:server/routes/users/users_post.dart';
import 'users_get.dart';
final users = GazelleRoute(
name: "users",
children: [
userId,
],
).get(usersGet).post(usersPost);
As you can see, we've added a child to the children
property of the route.
This builds the hierarchy of your routes.
Run gazelle codegen client
to update the frontend client!
Update the app
Now update the app with the following code:
import 'package:flutter/material.dart';
import 'package:client/client.dart';
void main() {
gazelle.init();
runApp(const MaterialApp(home: MyApp()));
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
late Future<List<User>> _users;
@override
void initState() {
super.initState();
_users = gazelle.client.api.users.get();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('my_awesome_app'),
),
body: Center(
child: FutureBuilder(
future: _users,
builder: (_, snapshot) => switch (snapshot) {
AsyncSnapshot(:final data?) => UsersList(
users: data,
userOnTap: (user) => Navigator.of(context)
.push(
MaterialPageRoute(builder: (_) => UserView(user: user)))
.then((user) {
ScaffoldMessenger.of(context).showSnackBar(SnackBar(
content: Text("${user.username} has been deleted."),
backgroundColor: Colors.green,
));
setState(() {
_users = gazelle.client.api.users.get();
});
}),
),
AsyncSnapshot(:final error?) =>
Text('${error.runtimeType}: $error'),
_ => const CircularProgressIndicator(),
},
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => _addUser(context).then((_) => setState(() {
_users = gazelle.client.api.users.get();
})),
child: const Icon(Icons.add),
),
);
}
}
class UsersList extends StatelessWidget {
const UsersList({
required this.users,
required this.userOnTap,
super.key,
});
final List<User> users;
final void Function(User user) userOnTap;
@override
Widget build(BuildContext context) {
return ListView.builder(
itemCount: users.length,
itemBuilder: (context, index) {
final user = users[index];
return ListTile(
title: Text(user.name),
subtitle: Text(user.username),
leading: Text(user.id),
onTap: () => userOnTap(user),
);
},
);
}
}
Future<T?> _addUser<T>(BuildContext context) => showModalBottomSheet<T>(
context: context,
builder: (_) {
return const AddUser();
},
);
class AddUser extends StatefulWidget {
const AddUser({
super.key,
});
@override
State<AddUser> createState() => _AddUserState();
}
class _AddUserState extends State<AddUser> {
late final TextEditingController _nameController;
late final TextEditingController _usernameController;
@override
void initState() {
super.initState();
_nameController = TextEditingController();
_usernameController = TextEditingController();
}
@override
void dispose() {
_nameController.dispose();
_usernameController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 8.0),
child: Column(
children: [
Text(
"Create a new user",
style: Theme.of(context).textTheme.headlineSmall,
),
const SizedBox(height: 24),
TextField(
controller: _nameController,
decoration: const InputDecoration(hintText: "Name"),
),
TextField(
controller: _usernameController,
decoration: const InputDecoration(hintText: "Username"),
),
const SizedBox(height: 24),
FilledButton.icon(
onPressed: () => gazelle.client.api.users
.post(
UserPost(
name: _nameController.text.trim(),
username: _usernameController.text.trim(),
),
)
.then((user) => Navigator.of(context).pop(user)),
label: const Text("Create"),
icon: const Icon(Icons.create),
),
],
),
);
}
}
class UserView extends StatelessWidget {
const UserView({
required this.user,
super.key,
});
final User user;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(user.username),
),
body: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Center(
child: Text(
user.name,
textAlign: TextAlign.center,
),
),
FilledButton.icon(
onPressed: () => gazelle.client.api.users
.userId(user.id)
.delete()
.then((user) => Navigator.of(context).pop(user)),
label: const Text("Delete"),
icon: const Icon(Icons.delete),
)
],
),
);
}
}
As you can see, we are now able to access the /users/:user_id
route with a
dynamic parameter.
Try to tap on one user from the list and delete it with the button at the center
of the page!
The result should look something like this