Push-Pop-Push-Pop: Navigating the Complexity of Screen Result Handling in Flutter
A Tale of Push and Pop
In Flutter, retrieving result data from a screen is fairly straightforward in concept. Let’s take a look at the image below.
Here, we have two screens: Screen A and Screen B. From Screen A, we can navigate to Screen B by using Navigator.push()
method. And complementarily, Screen B can return its result data to Screen B by using Navigator.pop()
method.
Here’s a brief code snippet that shows how this concept can be implemented in a typical Flutter application.
Inside ScreenA
widget, we “push” theScreenB
widget and await the result data:
Inside ScreenB
widget, we can simply “pop” the result data to be received by ScreenA
.
The “push-pop” method is quite simple and robust in concept and implementation.
But Wait, There’s a Problem
This concept is indeed simple if we’re talking about 2–3 screens. However, we can imagine what would happen if we’re dealing with a slightly more number of screens.
Let’s use the image above as an example. Here, we have 5 screens that are connected to each other. And if Screen E wants to return its result data to Screen A, it has to pass the result data through 3 other Screens (Screen B, C, and D).
This way of passing the result data through unrelated screens can get quite messy and complicated fast.
So, how should we handle this problem instead of using regular “push-pop” method?
Solution Proposal: Shared Memory and Postponing Result
When I encountered this problem, I proposed ditching the unscalable “push-pop” method for something much simpler: a shared memory and postponing result.
We can start imagining this solution by assigning an identification for each screen in our application. In Flutter application, we can simply use the screen route path as the identification like this:
Now we can identify each screen by a String literal. For example “/screen-a” refers to Screen A, etc.
The next step is creating a shared memory that contains a map between a screen identification and the screen’s postponed result data. Why postponed, you might ask?
It’s because the result data of a screen is not necessarily be used by the previous screen, but it can be used by any screen with the specific identification in the navigation stack.
Let’s use the above example again. If Screen E wants to pass result data to Screen A, then it simply needs to add the result data into the shared memory with Screen A’s identification (“/screen-a”) like this:
Then, when user navigates back to Screen A, it can check whether there is a result data with its identification.
Using this concept, we can slightly alter our previous code snippets to something like this:
Notice the differences between the “push-pop” method with this one.
- Firstly, Screen A is no longer waiting for a result from Screen B. It doesn’t even care anymore which screen gives the result, it only cares about whether there is a result or not.
- Secondly, Screen E doesn’t need to pass the result data through
Navigator.pop()
method anymore. Instead, the screen now have to add the result data into the shared memory and “pop” the screen without any data.
And finally, the last thing you need to notice when implementing the shared memory is that you need to remove the result data every time it’s read by a screen. There are many ways to implement the shared memory, but the underlying concept is that it should behave similarly to the result data being passed by Navigator.pop()
method. It should be ephemeral and only read once.
Conclusion
Using the proposed “shared memory and postponing result” can bring an obvious advantage over the regular “push-pop” method, namely a scalable and cleaner way to communicate result data over a medium/large number of screens. But, it brings a number of things to consider too, for example the increasing coupling between the screens.
What do you think about this article? Do you agree with the proposed solution? How would you solve the same problem in your apps? Let me know in the comments.
As always, thanks for reading and happy coding!