[Short] Flutter: A simple navigation to details page with Riverpod and GoRouter

Quentin Klein
La Mobilery
Published in
4 min readFeb 25, 2024

Sometimes, when you develop an app, you need to show a detail page for an item you’ve already fetched.

This is exactly the principle of the master detail flow.

From the list of items

So let’s say you are in a Flutter screen that displays a list of items.

As we are in a list, chances are high that we do not display everything we have in item, it is a light version of the object.

That said, when you tap on a specific item, you want to navigate another page showing the details of this item.

With go_router, a basic implementation could be

final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const ListPage(),
routes: [
GoRoute(
path: ':id',
builder: (context, state) => DetailsPage(
id: state.pathParameters['id']!,
),
),
],
),
],
);

This way, you have your list of items at “/” then on “/:id” you have your detailed item.

To a specific item

On the item page with the declaration, you could have something like

class DetailsPage extends ConsumerWidget {
final String id;

const DetailsPage({
super.key,
required this.id,
});

@override
Widget build(BuildContext context, WidgetRef ref) {
return Scaffold(
body: Center(
child: Text('Details for $id'),
),
);
}
}

But, we could guess items are object with more fields, probably fetched over some API or some database. E.G. asynchronous items

Fine, we have something for that in riverpod, the FutureProvider

@riverpod
class ItemDetails extends _$ItemDetails {
@override
FutureOr<Item> build(String id) {
return ref.read(itemServiceProvider).getItem(id);
}
}

An now we can change our DetailsPage to

class DetailsPage extends ConsumerWidget {
final String id;

const DetailsPage({
super.key,
required this.id,
});

@override
Widget build(BuildContext context, WidgetRef ref) {
final itemFuture = ref.watch(itemDetailsProvider(id));
return Scaffold(
body: itemFuture.when(
data: (item) => ItemView(item: item),
error: (error, _) => ErrorView(error: error),
loading: () => const LoadingView(),
),
);
}
}

This way, we load our item value using our itemDetailsProvider.

But what if we already have the value of Item from our list?
> You give an extra

Fair point. But what if we have it sometimes, on we don’t have it some other time?
> Like?

Like showing the details page directly from an url or from a notification?

Using extra as default

To handle our specific case, we’re going to pass an extra, but only as optional! This is the magic.

First, lets update our itemDetailsProvider

@riverpod
class ItemDetails extends _$ItemDetails {
@override
FutureOr<Item> build(String id, {
Item? initialValue,
}) {
if (initialValue != null) {
return initialValue;
}
return ref.read(itemServiceProvider).getItem(id);
}
}

What have we done here?

  1. We have added an initialValue as optional parameter
  2. If this value is given, then we return it
  3. Else we fetch it

And now lets update our DetailsPage

class DetailsPage extends ConsumerWidget {
final String id;
final Item? initialItem;

const DetailsPage({
super.key,
required this.id,
this.initialItem,
});

@override
Widget build(BuildContext context, WidgetRef ref) {
final itemFuture =
ref.watch(itemDetailsProvider(id, initialValue: initialItem));
return Scaffold(
body: itemFuture.when(
data: (item) => ItemView(item: item),
error: (error, _) => ErrorView(error: error),
loading: () => const LoadingView(),
),
);
}
}

Now perfect, we have on optional initialItem as a parameter and we give it to our provider.

Let’s give this parameter in!

final router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const ListPage(),
routes: [
GoRoute(
path: ':id',
builder: (context, state) => DetailsPage(
id: state.pathParameters['id']!,
initialItem: state.extra as Item?,
),
),
],
),
],
);

The only line we’ve added is the one with initialItem, we get an optional extra that can be nullable.

And to finish, update our ListPage to provide the extra on clic.

But why?

The goal of this technique is to make it possible to have a direct access to the DetailsPage, like from a web browser or a notification clic.

This ways is classic, you fetch the item over the db or api and show it.

But in some cases, you come from another part of the app where the item has already been loaded, and it is a waste of time and resources to fetch it again.

This way you can give it as an argument and it will be used.

Special note

There are other cases that can be declined, for example you have some fields of item but not all the fields.

Then you can pimp the provider a little to provide a fast experience to your user instead of a loading screen.

For example you can use a Stream instead of a FutureOr, but you can also add intelligence to your provider to fetch conditionality if some fields are missing or null.

Github and recap

The code for this example is available here

Also here is a recap

Only a recap of the code already showed in the article
Code recap

--

--