In the recent series on Maintainable Architecture, the final task that we covered was separating out the Navigation logic by means of the Jetpack Navigation architecture component. Those that read the article will be aware that not only did it help to solve the issues that we were looking address, but it is actually a really well structured solution which I was really impressed with. That said, my initial experimentation with the library was not all plain sailing for the most part because of some fundamental errors and misunderstandings that I had. Once I overcame those the result was really nice. In this article we’ll take a look at some of those issues and how I overcame them which may hopefully help prevent others from making the same mistakes that I did.



As with the previous article on Navigation, this is not intended to be a beginners guide to using the navigation library. The official documentation is a good starting point, and there also a number of blog posts covering this already. Once you are familiar with the basics but before you attempt to use it in anger would be the time that you are likely to get the most benefit from this post.

One of the fundamental principles of how the navigation library works is that each navigation graph has a single point of entry, which get declared in the navigation XML using the startDestination attribute. This simple yet fundamental rule was the cause of biggest mistake that I made when first using the library not because the rule itself is in any way wrong, but it did not fit with what I needed to achieve and I ended up making a stupid mistake which I did not notice until later. This mistake resulted in some really weird behaviour which took me a while to fix because I did not spot it straight away, and when I did, I did not realise that it was an indirect result of the stupid mistake that I had made.

To explain what happened let’s first look at the Activity from the Weather Station app. First let’s look at the layout:

The layout for this activity is as follows:

<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".ui.MainActivity"> <fragment android:id="@+id/nav_controller" android:name="androidx.navigation.fragment.NavHostFragment" android:layout_width="0dp" android:layout_height="0dp" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@+id/appbar" app:defaultNavHost="true" app:navGraph="@navigation/weather_navigation" /> <com.google.android.material.appbar.AppBarLayout android:id="@+id/appbar" android:layout_height="wrap_content" android:layout_width="match_parent"> <androidx.appcompat.widget.Toolbar android:id="@+id/toolbar" android:layout_width="match_parent" android:layout_height="?attr/actionBarSize" android:background="?attr/colorPrimary" android:elevation="4dp" android:theme="@style/ThemeOverlay.AppCompat.Dark.ActionBar" app:popupTheme="@style/ThemeOverlay.AppCompat.Light" /> </com.google.android.material.appbar.AppBarLayout> </androidx.constraintlayout.widget.ConstraintLayout> 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 <? xml version = "1.0" encoding = "utf-8" ?> < androidx . constraintlayout . widget . ConstraintLayout xmlns : android = "http://schemas.android.com/apk/res/android" xmlns : app = "http://schemas.android.com/apk/res-auto" xmlns : tools = "http://schemas.android.com/tools" android : layout_width = "match_parent" android : layout_height = "match_parent" tools : context = ".ui.MainActivity" > < fragment android : id = "@+id/nav_controller" android : name = "androidx.navigation.fragment.NavHostFragment" android : layout_width = "0dp" android : layout_height = "0dp" app : layout_constraintBottom_toBottomOf = "parent" app : layout_constraintEnd_toEndOf = "parent" app : layout_constraintStart_toStartOf = "parent" app : layout_constraintTop_toBottomOf = "@+id/appbar" app : defaultNavHost = "true" app : navGraph = "@navigation/weather_navigation" / > < com . google . android . material . appbar . AppBarLayout android : id = "@+id/appbar" android : layout_height = "wrap_content" android : layout_width = "match_parent" > < androidx . appcompat . widget . Toolbar android : id = "@+id/toolbar" android : layout_width = "match_parent" android : layout_height = "?attr/actionBarSize" android : background = "?attr/colorPrimary" android : elevation = "4dp" android : theme = "@style/ThemeOverlay.AppCompat.Dark.ActionBar" app : popupTheme = "@style/ThemeOverlay.AppCompat.Light" / > < / com . google . android . material . appbar . AppBarLayout > < / androidx . constraintlayout . widget . ConstraintLayout >

There is a NavHostFragment instance with an ID of @+id/nav_controller which is a component from the library which does much of the heavy lifting for us. The Activity code is:

ui/MainActivity.kt class MainActivity : AppCompatActivity() { private lateinit var navController: NavController override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) navController = Navigation.findNavController(this, R.id.nav_controller) setSupportActionBar(toolbar) setupActionBarWithNavController(this, navController) setupWithNavController(toolbar, navController) if (REQUIRED_PERMISSIONS.any { checkSelfPermission(it) == PERMISSION_DENIED }) { ActivityCompat.requestPermissions(this, REQUIRED_PERMISSIONS, 0) } else { navController.navigate(R.id.currentWeather) } } override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) { if (requestCode == 0) { if (REQUIRED_PERMISSIONS.any { checkSelfPermission(it) == PERMISSION_DENIED }) { navController.navigate(R.id.noPermission) } else { navController.navigate(R.id.currentWeather) } } } override fun onOptionsItemSelected(item: MenuItem): Boolean { return when(item.itemId) { R.id.to_preferences -> item.onNavDestinationSelected(navController) else -> super.onOptionsItemSelected(item) } } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 class MainActivity : AppCompatActivity ( ) { private lateinit var navController : NavController override fun onCreate ( savedInstanceState : Bundle ? ) { super . onCreate ( savedInstanceState ) setContentView ( R . layout . activity_main ) navController = Navigation . findNavController ( this , R . id . nav_controller ) setSupportActionBar ( toolbar ) setupActionBarWithNavController ( this , navController ) setupWithNavController ( toolbar , navController ) if ( REQUIRED_PERMISSIONS . any { checkSelfPermission ( it ) == PERMISSION_DENIED } ) { ActivityCompat . requestPermissions ( this , REQUIRED_PERMISSIONS , 0 ) } else { navController . navigate ( R . id . currentWeather ) } } override fun onRequestPermissionsResult ( requestCode : Int , permissions : Array < out String > , grantResults : IntArray ) { if ( requestCode == 0 ) { if ( REQUIRED_PERMISSIONS . any { checkSelfPermission ( it ) == PERMISSION_DENIED } ) { navController . navigate ( R . id . noPermission ) } else { navController . navigate ( R . id . currentWeather ) } } } override fun onOptionsItemSelected ( item : MenuItem ) : Boolean { return when ( item . itemId ) { R . id . to_preferences - > item . onNavDestinationSelected ( navController ) else - > super . onOptionsItemSelected ( item ) } } }

This is performing our runtime permissions check, requesting them if we don’t already have them and then navigating to either the noPermission action if permission has been denied, or the currentWeather action if permission has been granted. Herein lies the problem – there are effectively two possible starting points depending on whether or not permission was granted.

In hindsight it is obvious to me that the noPermissions case does not really require a navigation graph at all because it is a single Fragment which offers no onward navigation options, so we could replace this by including it directly in the layout file, and simply toggling the visibility of it and the NavHostFragment. So if we have permission we make the NoPermissionFragment GONE , and NavHostFragment VISIBLE , but switch these states when we do not have permission.

However, at the time it seemed correct to define two global actions to cover the two cases, and include both in the same navigation graph. But then the question that I struggled with was what to set the startDestination to in the navigation file as potentially both of these could be a start point. This is where I made my big mistake. Misunderstanding what the startDestination actually was, I decided to set the startDestination to the ID of the NavHostFragment and not either of the possible starting Fragments. In fact, the navigation editor would not let me do this, so I directly modified the XML. Once again, in hindsight, I should have realised that the editor would not let me do it because it was a stupid thing to do.

But weirdly things kind of worked, and the CurrentWetherFragment was being displayed on start up. At this point I did not test that all of my permissions logic was working because I had not touched it other than to alter explicit Fragment loading to calling navigation actions instead. It was only later on when I came to fully test the app from the start I saw that the whole permissions handling was very badly broken, but struggled to understand why.

The behaviour I was seeing was that onCreate() was being called on the Activity and everything was happening as expected – a call was being made to ActivityCompat.requestPermissions() because the permission had not yet been granted. However the Activity never received the onRequestPermissionsResult() callback; instead a new MainActivity instance was being created and onCreate() called on that, and the whole cycle simply kept repeating. I haven’t studied the internals of the Navigation library to understand what was causing it to cause these new Activity instances, but it was being caused because I had set the startDestination to the NavHostFragment ID, and I’m guessing that this was doing something pretty unexpected.

The fix was simplicity itself – I changed the startDestination to @id/currentWeatherFragment (i.e. one of the Fragments declared within the navigation graph – which the editor did allow me to do) and everything began working as expected.

But not quite.

Although the permissions handling was now correct, I was seeing an ‘Up’ affordance being added to the ActionBar when my NoPermissionFragment was being displayed. Hitting this resulted in the permissions checking being performed again. This was not the behaviour I wanted because if the user has just denied permission, then the only recourse should be to exit the app. If the user want’s to re-check then they would be required to launch the app again.

In the final article in this series we’ll take a look at how I was able to get this behaving as I wanted, and also look at another pain point that I encountered.

There is no source code that has been specifically written in support of this article, but the existing project upon which these experiences are based is available here.

© 2018, Mark Allison. All rights reserved.

Related

Copyright © 2018 Styling Android. All Rights Reserved.

Information about how to reuse or republish this work may be available at http://blog.stylingandroid.com/license-information.