Recovering user input when ‘Forward’ is clicked in Flutter app
Use case: In a multi-screen app, the user enters some text, then closes the screen without saving by ‘Back’ button. They then click ‘Forward’ and expect their input is still there.
This could be done in many ways:
- A state object behind the scene. The input window should store changes there and get them when re-initializing. But that is complex. What if we have many URLs like
/input/something
all handled by the same screen? It’s too much work to pick the right preserved input. - A database or local storage. Even more work.
A better way is to just use the browser History API to attach hidden information to the URL itself.
For this, we will use app_state
package that is a router with BLoC state management built-in.
If you are not familiar with it, start with these tutorials:
- Instagram-like navigation with Router API in Flutter.
- Adding web URLs to Flutter app using app_state package.
The project source for this tutorial is here. The structure is:
Home screen is stateless and is of no interest.
InputBloc
and InputPath
are where the magic happens.
The bloc contains the text editing controller. On every change, it emits the new InputPath
object with text
in it. The text is not a part of the visible page path but it is attached to it in the browser history accompanying the current URL.
InputPath
class puts this text into the state
map that is actually stored:
When the user clicks ‘Back’, the page and the bloc are removed from the stack and disposed.
Then on ‘Forward’ click, tryParse
method is called (as on any other navigation). If the URL matches /input
, Flutter checks if the state was previously stored for this URL. If it was, app_state’s PageStackRouteInformationParser
detects this and does not even try to parse the URL. Instead it recovers everything from it.
So if tryParse
is called in InputPath
, the state is definitely empty, so we create an object with empty text.
Then the bloc gets created, and setStateMap
is called on it. If the state was recovered from browser history, it is passed there. This way the text is back to the controller.
If we had many routes like /input/something
handled by a single screen, a history entry for each one would store its text independently.
Other things to store and recover like that:
- Scrolling position in tall screens (add the
ScrollController
to bloc, listen to changes). - Text selection (store the whole
TextEditingValue
and not only the text).
Most of the job is done by Flutter Router API and app_state
package.
You can actually do this with raw Flutter Router API, but app_state
gives some advantages:
- It recovers the the state so your path parsing does not even start if the state is found.
- It manages the stack and allows to push and pop pages.
- It serializes states of multiple blocs in one state. It just so happened that in this example we only have one screen to preserve.
- It saves a few hundred lines of code.
So go ahead and start the app_state tutorial if you have not yet started.
What else?
What other navigation scenarios do you struggle with? Tell me in the comments, so I can make more tutorials.