2

Navigating towards a new navigation

 3 years ago
source link: https://sourcediving.com/navigating-towards-a-new-navigation-154011622435
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Navigating towards a new navigation

Image for post
Image for post
https://pixabay.com/photos/boat-storm-rain-raining-vessel-962791/

TLDR: We migrated our code-base to the Navigation component and we are happy now, even though we weren’t during most of the migration process.

We decide to migrate to the Navigation component

The Cookpad Android team decided to migrate to the Jetpack Navigation component. We want to share here our experience of the migration itself rather than a tutorial about how we did it because the way we did it was simply by following the official docs. To be clear: this is not about how can you work out the migration but rather how the migration will work you.

We wanted to migrate to the Navigation component to replace homemade workarounds by an official-standard solution, those were components used for navigating between screens declared in a multi-module project setup. Most of our screens were Activities and to navigate between them we started Intents with the proper setup in each case. With the purpose to move forward on Google expectations, we decided to switch to Fragments and the Navigation component to handle the navigation between them. But we had a few concerns that prevented us from making the decision:

  • Product required to preserve the back stack in each tab of the bottom navigation bar, and this feature was not available as part of the Navigation library. Luckily, Google had in its navigation sample code repository a showcase called NavigationAdvancedSample which illustrates how to support a multiple back stack. Unfortunately, it was quite limited as it did not take into account the complexities of a production app, so we couldn’t trust that using it out of the box would work for us.
  • We never were big fans of Fragments and the Navigation component expects you to use them, intensively, almost exclusively; but in return, the library offers a new API that makes using them feel that they are just normal pieces of software that draw thigs on the screen: you don’t need to commit awful transactions with some manager to go to the next screen. Plus, it seems nowadays fragments are the new activities and the actual activities are the Android app.

So we decided that taking that risk was worth it considering all the potential benefits, mainly: replacing a custom solution by a standard way of navigating. We were capable of foreseeing the main issues but not their major ramifications, which was good enough considering that we never wanted to see way beyond to avoid losing the motivation that made us start. Knowing too much too soon would have prevented us from knowing this much now and you from reading this post (which could be perfectly fine on the other hand).

We navigate to a shipwreck as the destination itself

Don’t fight the framework, let the framework fight itself. If the library expects from you to do B, and in order to do B you first need to do A, but the library does not allow you to do A (right, yeah?), then find a way to let believe the library that you did A. Do as the library tells you except in those cases where contradicting its instructions will make your compliance with further, more relevant instructions. Let’s narrow this idea to a concrete example: to pass data between screens, the library advises to use SafeArgs, this is ideal as it does compile-time checks to avoid launching screens without the proper parameters. But in order to be able to do this without adding a complexity that would make the usage of SafeArgs not worth it, you need to keep one single navigation graph for your entire application. Why? Here we go:

  • The arguments declared in the destination of another graph need to be copy/pasted into the action that points to that destination. This leads to huge duplications of code and also opens the door to dangerous situations: if you add a new argument to the destination and forget to update all the actions that point to that destination (and there are always more screens in your app than you think), the app will crash at runtime and the SafeArgs plugin will not be longer “safe”.
  • Global actions can not be shared between graphs, which means that any screen accessing another screen that is not declared in the same graph requires to rewrite its associated action. This issue, in conjunction with the previous one, guarantees massive code duplication.
  • When calling a graph from another graph, it is not possible to select the entry point (the screen that we want to launch), we can only use the one annotated as such in the destination graph.

You can’t follow the official stance of using multiple graphs on top of SafeArgs and at the same time follow the basic rules of common sense. But of course, we needed to pay for this one single graph, even if the API of the library became easier due to the concept of the graph was no longer present in the way we created and consumed destinations.

We decide to have one single navigation graph and we pay for it

The sample code that Google provides to showcase how to create a bottom navigation bar with multiple back stacks requires to have different graphs for each tab, otherwise when launching the app via explicit deep links either the app will crash or it will duplicate the deep link destination for each one of the tabs (depending on the version of the library). To solve this issue, we needed to hack the hack that Google provided, and we thought that the result was useful enough to promote it to an Android library: BottomNavWatson, named Watson after the legendary ActionBarSherlock, as it is in the bottom the same way Sherlock is above Watson. This library allows, when using the BottomNavigationView, to use the multiple back stack workaround provided on Google Samples with one single navigation graph per application.

That was the main ramification of the issue of using multiple back stacks. But the interesting thing is the chain reaction that led to the specific scenario where we needed to create the Watson library, or rather the underlying mindset: always conform with the contract of the library, even if it means leveraging some loopholes to fully conform in the crucial points.

We must accept that the views we don’t see no longer exist

The other significant ramification was related to fragments. The documentation of the library is missing a crucial piece of information, especially the docs which are dedicated to guiding the migration: when navigating back to the previous screen its view will be always (re)created. This is neither the same as a process death nor a config change, in those cases, the view is destroyed but also the fragment, and in the latter, only the ViewModel survives, however in this new case the fragment is retained and only its view destroyed. This wasn’t a familiar screen state for us, and thus we did not have our screens prepared for that. The fragment’s properties aren’t re-created in this state but still its view is re-created, and this could lead to subtle bugs for components like the PagedListAdapter and the Paginator.

But saying that the view is recreated implies that it has to be destroyed at some point, and that happens every time that the view goes to the background. Indeed, another major issue that we faced was updating the views of screens that were on the background using a reactive pipeline (kind of an Rx event bus). When using activities to navigate between screens, the view is retained in the background and it is possible to update it meanwhile the screen is on the background, but with this new state we needed to rethink and change the whole process: the view could no longer be updated in place, right away, the emission from the pipeline had to be deferred until the view was again re-created, and for that purpose, we used a LiveData stream whose subscription was properly tied to the lifecycle of the view. Migrating to the nav library made one latent issue become quite obvious: careless lifecycle management for view (re)creations leads to inconsistent screen states. In this sense, migrating to the nav library made bad things worse, up to a point where we needed to fix them.

We’re happy with the migration overall, but not as much as we’re with it being finished

The major benefits that we had with the migration can be summarised as follow:

  • Being able to replace our own abstractions to navigate between screens declared in different Gradle modules by a standard solution which any developer will be familiar with.
  • Exposing latent issues with the way we handled the lifecycle of the views that were generating memory leaks.
  • Using SafeArgs to ensure at compile time that we’re launching each screen with the required parameters.
  • Get rid of the boilerplate required to provide unique entry points for launching screens.
  • A handy API to build PendingIntent(s) to provide screen destinations for push notifications and deep links.

Some considerations when migrating to the Navigation component

  • Always prefer using the Fragment::viewLifecycleOwner to subscribe to LiveData streams inside the Fragment::onViewCreated callback.
  • Always remember to dispose resources/fields that may contain View/Context references and outlive the View on the onDestroyView callback (e.g.: adapters, tab mediators, view delegates, etc).
  • Always delegate View state to ViewModel and LiveDatas in order to be able to restore the screen state after re-creation.
  • Do not use any event bus solution on Fragments, always prefer to subscribe to them on ViewModel, and expose its events using LiveData. This will avoid crashes/misbehavior when the pipeline emits a value and the Fragment’s view is destroyed.
  • Don’t use lazy initialization when storing RxBindings for View events, as this is a potential cause of leaks. Prefer using a get() accessor or just subscribe to it inside Fragment::onViewCreated callback.
  • PagedListAdapter(s) need to bind to a proper lifecycle every time that the view is recreated, without that the Paginator(s) will start to misbehave.

A consideration before migrating to the Navigation component

If your team doesn’t have enough resources to invest in this migration maybe it’s not a good idea to start it. We had allocated two members of the Android team (Felipe Augusto Pedroso 👋 and myself) fully focused on this task for about three months. Only a part of the issues that we had to face were actual navigation issues, the others were side effects of a new navigation system which changes the lifecycle of the screens.


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK