Repository navigation
Description
Apps regularly want to respond to a stream of events that might be coming from non-UI related sources (such as network or platform channels). Common responses to such events might include:
- Navigating to a new page.
- Showing a dialog.
- Showing a snackbar.
Such events are typically not tied to the lifecycle of the "current page". The BuildContext of the code that invokes the action could be long gone when the action finally resolves.
A few examples:
final value = await permissionPluginCall();
Navigator.of(context).pushNamed(‘xxx’);
try {
final value = await someRpcCall();
} catch (e, st) {
Scaffold.of(context).showSnackBar(...)
}
try {
await someCode();
} catch (e, st) {
ErrorDisplayWidget.of(context).handleException(...)
}
There are solutions around some of these problems such as using NavigatorKey however, I have not seen a canonical way to grab a valid (aka current) context from anywhere in the app.
The ideal outcome of this issue would be to have a pattern similar to InheritedWidget.of(context) which locates the current context for you. Something like InheritedWidget.current.
Workarounds I have seen are pretty terrible:
- Passing
BuildContextaround. - Passing
Statearound. - Using APIs such as
context.findRenderObject().attachedto determine whether context is still valid.
Activity
mehmetf commentedon May 15, 2020
I tried using
GlobalKeyto solve this at least for dialogs but even that does not work as expected.If you try this, you are met with:
which is surprising at first because I am using the NavigatorState's context.... Why can't it find the navigator? After all, I am able to push routes using the key....
Then a few seconds later, you realize that this is Navigator's context whereas dialog needs to be generated in a widget which is descendant of Navigator. For people not familiar with Flutter's inherited widgets, this could lead to hours-long debugging session.
HansMuller commentedon May 19, 2020
Here's a proposal for a new pair of widgets that address the problems raised in this issue. We've discussed how the widgets would be implemented, but the API sketch that follows primarily focuses on how the API would be used.
Safely showing a Dialog
The following example method builds an app-specific dialog. It's static because the future support for restoring instance state when suspended apps are resumed on Android, requires static Dialog builder methods. The context parameter will be below the dialog's navigator.
To safely show the dialog we'll need to find a "stable" BuildContext, i.e. one that's guaranteed to still be part of the Element tree when the dialog is shown. The new inherited
AppMessageswidget encapsulates such a BuildContext. To show the dialog using AppMessages:AppMessages will apply showDialog's builder parameter to a context that is below the MaterialApp's Navigator.
To show a dialog after an async operation has completed, use showDialogWhen():
Safely showing SnackBars
In terms of API, the support for SnackBars is similar. The BuildContext passed to SnackBar building methods will be below the Scaffold. Given a example method that builds a SnackBar:
The ScaffoldMessages
showSnackBarmethod makes its SnackBar appear on Scaffolds that are above the inherited ScaffoldMessages widget:To display a SnackBar when an async operation has completed, use
showSnackBarWhen:The implementation of ScaffoldMessages is considerably different from AppMessages because the app may have many Scaffolds. All of the Scaffolds below the inherited ScaffoldMessages widget will show the SnackBar, although except in unusual situations, only one snack bar will be visible.
mehmetf commentedon May 19, 2020
This API might not be well suited for the use cases described above.
In the case of:
We don't want to show a snackbar every time so wrapping this in a
showSnackbarWhenfeels awkward. It feels like we are supposed to always return a snackbar widget; which isn't the case here.Something like this would be ideal:
but then isn't that the same problem as before?
May be we can have something like:
This doesn't feel like a friendly API since it is not readable (return null?). The ordering is also messed up (showDialog appears first but then it depends on the result of something that comes later).
I am also not clear how:
helps the current situation. If user navigates away from the page during
someNetworkOperation, isn't that context invalid now? Is the callback being called with a different context that stays valid? If so, why could I not do:since AppMessages knows which context to use already?
HansMuller commentedon May 20, 2020
The idea was that AppMessages.of(context) would return a AppMessagesController, which can safely be used later. Likewise for ScaffoldMessages.of(context).
The showFooWhen() methods were supposed to be a convenience, but you're right that they're ill-suited to conditionally showing a message.
mehmetf commentedon May 20, 2020
Yep. That works ergonomically.
I would have preferred not to have to grab the
AppMessagesfrom context since its lifecycle is not tied to that context. I also fully expect apps to pass this object around. You can expect the code I have written above that initiates the RPC call to be buried 5 layers down some business logic and not having access toStateorContext.That's why a singleton AppMessages works best for this scenario. MaterialApp/WidgetsApp sets up such a singleton that can be accessed from anywhere in the app. When these alerts are being shown, context doesn't matter.
I fully expect apps to do something like this:
Then everywhere else in the app just calls
dialogController.showDialog(). Are we OK with that?HansMuller commentedon May 21, 2020
By default, the value of AppMessages.of(context) would have the same lifetime as the app. So if you create a global variable whose value is updated each time the app is rebuilt, and callers use the global variable rather than a stale value, then yes that will work.
Updating the value in the MaterialApp's builder callback wouldn't work though, since the AppMessages widget must be below the app's Navigator.
mehmetf commentedon May 21, 2020
I assume
WidgetsAppcreates top-level Navigator. What would be the best place to set this global variable then?HansMuller commentedon May 21, 2020
The widget that builds the MaterialApp could also reset _dialogController.
mehmetf commentedon May 21, 2020
A sample app that does that would go a long way :-).
mehmetf commentedon Jun 1, 2020
Gentle ping. Do we have any progress here Hans? Your proposal looks good to me. We just need a couple of sample apps to demo the API.
HansMuller commentedon Jun 1, 2020
Do we need both the snack bar and the dialog solution, or is this really just about showing dialogs?
23 remaining items
mehmetf commentedon Jul 28, 2020
Given how frequently and quickly apps run into this and the type of bad patterns it encourages (see #57218 (comment)), I would keep the P2.
If it is feasible to make this a package, great! Perhaps we can brainstorm what that package should look like.
Hopefully not. We have seen many bad patterns emerge when applications try to solve this on their own.
Hixie commentedon Jul 29, 2020
Things you might want to do when your app receives some out-of-band message:
How can any meaningfully useful API cover all these use cases in a coherent manner?
I don't understand why this isn't just the same as reacting to any change in app state.
mehmetf commentedon Jul 29, 2020
Michael asked a similar question. I responded with #57218 (comment).
We are not talking about all (re)actions here. In Flutter terms, these out of band actions are out of "current" context. Showing a dialog and showing a snackbar (common) and navigating to a new route (less common) are the only examples we have so far. Curiously, they all seem to follow the
SomeWidget.of(context).modifyUI()type of API which is not the norm for Flutter. It almost feels imperative.I will noodle on this a bit more and see if I can come up with a pattern that I am happy recommending to internal clients.
Hixie commentedon Jul 30, 2020
navigating to a new route will soon no longer (need to be) be imperative, once Router lands. Dialogs are just routes.
Snackbars are tricky for various reasons but Kate has some work on changing how those work; I recommend commenting on her design doc if you have ideas for how to make that more manageable.
mehmetf commentedon Jul 31, 2020
I had tried using
navigatorKeyand it had not worked before. I just tried again it now works as expected!! Perhaps an underlying bug got fixed in the past 2 months? @chunhtai @PiinksLet me know what you think of this pattern.
A simple dialog service class:
is instantiated as a service and injected:
then use the navigator key:
From anywhere in the app, inject
_dialogServiceand call:Instead of a dialog service, we can expose a NavigatorService but this is the basic idea.
chunhtai commentedon Jul 31, 2020
The navigator key can be used after this pr #58259
Hixie commentedon Aug 4, 2020
I'm still a little unclear as to what a solution here would look like or what would be sufficient to resolve this issue.
goderbauer commentedon Aug 4, 2020
My understanding is that we are resolving the concrete case of the SnackBack with Kate's SnackbarMessanger work. The Navigator use case is already sorted out as pointed out by @chunhtai above.
mehmetf commentedon Aug 4, 2020
Right. Perhaps a more focused issue can be filed to track Kate's work. If so, you can close this issue.
Piinks commentedon Aug 4, 2020
Thanks @mehmetf! Filed:#62921
github-actions commentedon Aug 18, 2021
This thread has been automatically locked since there has not been any recent activity after it was closed. If you are still experiencing a similar issue, please open a new bug, including the output of
flutter doctor -vand a minimal reproduction of the issue.