Original address: medium.com/google-deve…

Original author: medium.com/david.vavr…

Published: February 24, 2019-6 minutes to read

The navigation library from Jetpack recently reached RC1, and all Android developers should start thinking about using it for new applications. I was responsible for the application architecture for Deutsche Aerobank, a new mobile-first German bank. Our application has a multi-module, single-activity Architecture using the ViewModels of Architecture Components. Integrating navigation components is a logical step, but not without some problems. In this post, I want to share how we can address these issues. This is a more advanced post, so I assume the reader has some knowledge of the official documentation.

Where should the navigation XML file be placed in a multi-module project?

Multi-module projects are a recommended way to structure new applications. Its advantages are faster build times and better separation of concerns in the code base. This is a simplified diagram of our Gradle module.

There are several options for where to put the navigation XML file.

App module”

Official documentation and Android Studio assume this. But you need to be able to navigate from one feature to another (and features don’t depend on the ‘app’ module). This can be solved by putting the ids of all navigation destinations into the ids.xml file within the ‘common-Android’ module. Then navigation will work, but you can’t use security parameters. You need to manually build Bundles to pass parameters to the destination.

The main image is in the ‘app’ module and the sub-image is in the function module

This is similar to having a big picture in the ‘app’ module, but more structured. You can use security parameters in a feature, but not when navigating to a different feature. You can also use the ID workaround in the ‘common-Android’ module.

Shared robot “module

All functionality relies on ‘common-Android’, so you can navigate anywhere using Safe Args. However, it has two disadvantages.

  • Since the ‘common-Android’ module has no dependency on functionality, the Fragment class is red in Android Studio. You don’t have auto-complete for the Fragment class, but it compiles without any problems.
  • There was an error that prevented the generation of intent filters for deep links. It has already been assigned, so hopefully it will be fixed in the final release.

These two issues are not a hindrance to us, and using Safe Args anywhere is really a big advantage. So, our first choice is to put the file in the ‘common-Android’ module.

How do I navigate from ViewModels?

Our application uses the MVVM architecture recommended in the Android Architecture component. The documentation shows how to navigate from Fragments, but this logic should be in the ViewModels. We tried to put all the template code into BaseFragment and BaseViewModel to simplify all the other Fragments/ViewModels code.

The command

We use command mode to communicate between ViewModel and Fragment. These are the commands we use for navigation.

sealed class NavigationCommand {
  data class To(val directions: NavDirections): NavigationCommand()
  object Back: NavigationCommand()
  data class BackTo(val destinationId: Int): NavigationCommand()
  object ToRoot: NavigationCommand()
}
Copy the code

The ViewModel publishes them to a LiveData object, which is listened on by Fragments. But it needs to be a one-off event. To do this, we use SingleLiveEvent from the architectural blueprint.

BaseFragment

The BaseFragment listens for navigation commands from the ViewModel like this.

override fun onActivityCreated(savedInstanceState: Bundle?). {
  super.onActivityCreated(savedInstanceState) vm? .navigationCommands? .observe { command ->when (command) {
      isNavigationCommand. - > findNavController (). Navigate (command. Directions)...Copy the code

ViewModel

BaseViewModel has these helper methods.

fun navigate(directions: NavDirections) {
  navigationCommands.postValue(NavigationCommand.To(directions))
}
Copy the code

And then it’s really easy to navigate from the ViewModel, like this.

navigate(CardListFragmentDirections.cardDetail(cardId))
Copy the code

How do I use parameters in ViewModels?

ViewModels typically require navigation parameters to load some data. Our BaseFragment will automatically pass parameters to the ViewModel like this.

override fun onCreate(savedInstanceState: Bundle?). {
  super.onCreate(savedInstanceState)
  if (savedInstanceState == null) { vm? .setArguments(arguments) vm? .loadData() } }Copy the code

The ViewModel can then easily use parameters like this.

override fun loadData(a) {
  valCardId = CardDetailFragmentArgs. FromBundle (args). CardId load (cardsRepository. GetCard (cardId)) {...Copy the code

How do I display a login interface with deep links?

Conditional navigation is not described in sufficient detail in official documentation. It basically recommends handling conditional navigation at the final destination. For example, the profile screen is displayed first, and then the login screen is displayed when the user logs in. We don’t like this approach for the following reasons.

  • The login screen should behave like another root destination. The application should be closed when the user presses the back button on the login screen. The user should not go to the previous screen — since the previous screen requires login, the user ends up in an infinite loop.
  • In particular, deep links, when the user is not logged in, should not be created at all. Creating a Fragment requires some resources, it starts making some network calls that fail because the user is not logged in, and so on. It would have been better if the login had been displayed before all this happened.

We tried replacing the navigation diagram in a NavHostFragment and creating a different root for the login screen. But that didn’t work when the configuration changed. Finally, we decided to use a different activity to log in. Our application is no longer strictly a single activity, but login is a separate process, which makes sense in this case. The login can have its own navigation chart, and it works as we want it to — as another navigation root.

Applications are launched directly or through MainActivity’s deep link. We can prevent any fragments from loading by completing the MainActivity as soon as possible and displaying the LoginActivity.

override fun onStart(a) {
  super.onStart()  
  if (sessionRepository.isLoggedOut()) {
    startActivity<LoginActivity>()
    finish()
  }
}
Copy the code

But what about deep links? Fortunately, we can pass all the intent parameters to LoginActivity.

val intent = Intent(this, LoginActivity::class.java)
intent.putExtra("deepLinkExtras".this.intent.extras)
startActivity(intent)
finish()
Copy the code

When the user logs in, we can pass parameters back to MainActivity.

val intent = Intent(this, MainActivity::class.java)
intent.putExtras(this.intent.getBundleExtra("deepLinkExtras"))
startActivity(intent)
finish()
Copy the code

This way, we always show logins when needed, the deep link works in both cases (user logout or login), and the back button works as expected.

We have a problem with this approach. After logging in from the deep link, the root target is not added to the back heap of MainActivity. We solved this problem by changing the startup mode of all activities to “singleTask “.

How do I navigate to the dialog box?

Our design has a lot of low-level dialog boxes. They contain multi-step processes, such as activating a card. We need to pass parameters to the dialog box, so it’s convenient to keep them in the navigation diagram as other fragments. There is an officially supported feature request, but it has little priority. Fortunately, the library is quite extensible, and other navigational destination types are possible. We use this GIST to implement DialogNavigator.

You can then specify dialog fragments like this.

<dialog
  android:id="@+id/nav_close_account"
  android:name="de.innoble.abx.closeacc.AccountCloseConfirmDialog"
    <argument
      android:name="iban"
      app:argType="string" />
</dialog>
Copy the code

The security parameters and everything else work the same as normal Fragments.

However, there is one big difference — the dialog box is not added to the back stack. When there are multiple dialogs and the back button is pressed, the user wants to see the following fragment, not the previous dialog. This has an annoying side effect: when navigating from a dialog box, you need to use the FragmentDirections of the following fragment instead of the dialog box. This is somewhat counterintuitive, but it was quickly discovered during development (the exception that crashed the application was “navigation destination unknown to this NavController”).

How to navigate back with a result?

Something like startActivityForResult() would be handy. Google recommends using a shared ViewModel for this functionality, but the API is not intuitive. There is a feature request for this, but it is of low priority. Until official support arrives, we are using our own solution. First define the interface.

interface NavigationResult {
    fun onNavigationResult(result: Bundle)
}
Copy the code

Implement this interface in the fragment where you want to receive the result. Results sent from another Fragment must be routed through an Activity. Add this method to your activity.

fun navigateBackWithResult(result: Bundle) {
  valchildFragmentManager = supportFragmentManager.findFragmentById(R.id.nav_host_fragment)? .childFragmentManagervar backStackListener: FragmentManager.OnBackStackChangedListener byDelegates.notNull() backStackListener = FragmentManager.OnBackStackChangedListener { (childFragmentManager? .fragments?.get(0) asNavigationResult).onNavigationResult(result) childFragmentManager.removeOnBackStackChangedListener(backStackListener) } childFragmentManager? .addOnBackStackChangedListener(backStackListener) navController().popBackStack() }Copy the code

Note that this solution only applies to Fragment navigation destinations.

TLDR;

  • In a multi-module project, put the navigation XML into the ‘common-Android’ module.
  • Navigate from the ViewModels via the command SingleLiveEvents. Put all the template in the BaseFragment/BaseViewModel.
  • Use Safe Args and pass the Fragment argument automatically to the ViewModel.
  • Use different activities for the login screen. Passing Intent Extras back and forth to support deep linking.
  • With the help of this GIST, include dialog boxes in the navigation diagram. Note that the dialog box is not added to the later stack.
  • Use our solution to navigate with a result until official support arrives.

www.deepl.com translation