Flutter: How to reload a widget after Navigator.pop()
Tags: flutter, dart, tutorial, appdevelopment
Published: 2021-11-27 08:54:27

When the user navigates from one screen to another, he most likely eventually comes back after he has finished his task on the second screen. The action on his second screen could have affected the first screen, so we want to reload its data after he has popped the route. How do we do that? And can we decide - depending on his actions - if we want to refresh the first screen? 

# The use case

Let's say we have an app that let's you create, edit and manage notes. There is

* A "note overview" screen that lists all of your notes
* A "create note" screen that lets you create a new note
* An "edit note" screen that lets you edit a created note

![Flutter - Reload after pop: note overview screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/j2a0ajzk9bt87de228yt.png)

![Flutter - Reload after pop: create note screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/b77uy8mxo79yfxhp0wel.png)

![Flutter - Reload after pop: edit note screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fya5630p7h3prmzbh9e3.png)

# Navigator.push and its return value

We assume that the user starts on the "note overview" screen and has the ability to navigate to "create note" and "edit note".

Flutter's navigation concept provides two possibilities to change the current route:

* Navigating via an anonymous route via [push()](https://api.flutter.dev/flutter/widgets/Navigator/push.html)
* Navigating via a named route via [pushNamed()](https://api.flutter.dev/flutter/widgets/Navigator/push.html)

In both cases, the return value of the method is a `Future`. That means: we can wait for the result and react accordingly.

One side note, though: if you use `pushNamed()` which is generally recommended as it leads to all routes being defined at one place and reduces code duplication, you might face a type error. Read this article for further details.

So we know how to navigate to a different route. But how do we trigger this Future's completion? That's where the opposite of `push()` comes into play: [pop()](https://api.flutter.dev/flutter/widgets/Navigator/pop.html).

The resulting navigation would look like this:

![Flutter - Reload after pop: sequence diagram](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/s8w9vh17aw1ojgopcjpb.png)

In terms of our sample note app, this would apply to the "edit note" route and the "create note" route. Assuming that our data source is remote, we would want to reload the overview once a new note is added or an existing note is edited.

# Reacting to a popped route

To capture a Future's return value, we have two options:

* Using [then()](https://api.flutter.dev/flutter/dart-async/Future/then.html)
* Using [async / await](https://dart.dev/codelabs/async-await)

That means we can either handle the second route being popped like this:

```dart
await Navigator.pushNamed(
  context,
  '/note/details',
  arguments: noteId,
);

_refreshData();
```

Or like this:

```dart
Navigator.pushNamed(
  context,
  '/note/details',
  arguments: noteId,
).then((_) {
  _refreshData()
});
```

Either way is fine. I personally prefer the await way because it prevents unnecessary nesting and is thus much more readable in my opinion.

# What about canceling?

We still have one problem, though: if we keep it this way, we reload the first route independent of the second route's cause to pop. The user could have just tapped the back button. We would know that in this case, nothing has changed, so there would be no reason to refresh. We'd refresh the first route anyways. This uses unnecessary computational power (and bandwidth). 

Without any further involvement, the usual back button tap performs a `Navigator.pop()` without any arguments. Good for us, because every other (intentional) pop can be fed with an argument telling the caller if something has changed (if a refresh is necessary). As a caller we can be certain then: if the return value of `pop()` is `null` or `false`, nothing has changed so don't refresh. Otherwise (if it's `true`), do refresh!

So the edit and create widgets would do something like this:

```dart
Navigator.of(context).pop(true);
```

And the overview widget would react like this:

```dart
final bool? shouldRefresh = await Navigator.pushNamed<bool>(
  context,
  '/note/details',
  arguments: noteId,
);

if (shouldRefresh) {
  _refreshData();
}
```
# How does it work with Dialogs and BottomSheets?

Let's assume, we have the ability to apply certain actions to our notes directly on the overview screen by long tapping the respective entries. 

![Flutter - Reload after pop: note overview screen with BottomSheet](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/4508uauswalwh6yfcgqd.png)

Here we basically have the same situation: we want to wait for the user to interact with the [BottomSheet](https://api.flutter.dev/flutter/material/BottomSheet-class.html) and react depending on what his action was. In particular, we want to know, if the action the user has executed influences the note list which would require the application to refresh it in order to present an up-to-date list.

The good thing is that the API to show a `BottomSheet` is quite similar to `push()`. [showModalBottomSheet](https://api.flutter.dev/flutter/material/showModalBottomSheet.html) has a type argument as well and returns a Future being typed with the given type.

This is what the method call to open the `BottomSheet` could look like:

```dart
final bool? shouldRefresh = showModalBottomSheet<bool>(
  context: context,
  builder: (BuildContext context) {
    return Padding(
      padding: const EdgeInsets.all(24.0),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          Text(
            'Note options',
            style: Theme.of(context).textTheme.headline5,
          ),
          SizedBox(
            height: 16,
          ),
          ListTile(
            leading: Icon(Icons.star_rounded),
            title: Text('Mark as favorite'),
            onTap: () {
              await _setNoteAsMarked();
              Navigator.pop(true);
            },
          ),
          ListTile(
            leading: Icon(Icons.delete_rounded),
            title: Text('Delete note'),
            onTap: () {
              await _deleteNote();
              Navigator.pop(true);
            },
          ),
        ],
      ),
    );
  },
);

if (shouldRefresh) {
  _refreshData();
}
```

Like in the above example with push, we wait for the result and only refresh the list of the return value is true (representing the necessity to refresh).

Important: In this code example, we use await to wait for the results of the action. This is supposed to emphasize that we must only refresh the list **once the server has responded** (assuming we're dealing with a web service here that manages our data). Otherwise, we refresh the list too early, before the new state has been persisted. When dealing with [BLoC](https://www.flutterclutter.dev/flutter/basics/what-is-the-bloc-pattern/2021/2084/), we'd need to setup a [BlocListener](https://pub.dev/documentation/flutter_bloc/latest/flutter_bloc/BlocListener-class.html) in order to listen for the moment when the API call was successful and then call `Navigator.pop(true)`.

The way to deal with [Dialogs](https://api.flutter.dev/flutter/material/Dialog-class.html) is the same as [showDialog](https://api.flutter.dev/flutter/material/showDialog.html) has the same API as `showModalBottomSheet`:

```dart
final bool? shouldRefresh = showDialog<bool>(
  context: context,
  builder: (BuildContext context) {
    return AlertDialog(
      content: Column(
        mainAxisSize: MainAxisSize.min,
        children: <Widget>[
          ListTile(
            leading: Icon(Icons.star_rounded),
            title: Text('Mark as favorite'),
            onTap: () => null,
          ),
          ListTile(
            leading: Icon(Icons.delete_rounded),
            title: Text('Delete note'),
            onTap: () => null,
          ),
        ],
      ),
    );
  },
);

if (shouldRefresh) {
  _refreshData();
}
```
# What if I have a nested navigation?

Let's say the route you are navigating to, performs a navigation itself like this:

![Flutter - Reload after pop: note overview screen with BottomSheet and nested navigation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1t1payv2mqdkcgr775gx.png)

Can we apply the above mentioned pattern as well? Does it make things more complicated?

![Flutter - Reload after pop: sequence diagram with nested navigation](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/tzj4n8au5y3c27tjhj35.png)

In fact, we have a call chain here: the first screen calls `showModalBottomSheet()` and waits for its response. Inside the `BottomSheet`, `Navigator.pushNamed()` is called and waited for, which opens up the final route (edit note screen). When this route is popped (e. g. the submit button is tapped), the `BottomSheet` receives a response (true or false) and forwards this response to `Navigator.pop()` which eventually arrives at the initial caller.

That means the pattern does not change. Every route is supposed to act independently by returning a specific value to `Navigator.pop()` being returned to the caller that navigated to the route. That's a good thing because every route navigating to our target route can decide for itself how to react on the return values.

Sometimes, it's also appropriate to return something more meaningful like an `enum` instead of a `bool`. This can be the case for a `BottomSheet` we might want to react differently depending on the chosen option. So we provide an `enum` to `Navigator.pop()` in the `BottomSheet` that gives the calling route the possibility to react depending on the value of this `enum`.

On the diagram this might look more complicated, but on the code side, there is nothing much going on.

# Conclusion

Basically reloading the original route when navigating to a second one is as simple as waiting for the result of the second route (using `then()` or `async / await`) which is determined by the argument given to pop(). When using `pushNamed` with a static type, there are things to consider.

When there is a call chain with more than two routes, the mechanic stays the same for every part of the chain: wait for the return value and react accordingly, which possibly includes forwarding the result.

Opening `BottomSheets` and `Dialogs` behaves likewise.

When tapping the back button or dismissing `BottomSheets` and `Dialogs`, the caller receives null as the return value and can react conforming to that.