Creating a maintainable, flexible codebase is not easy but is an essential part of software engineering. In this series we’ll take a look at a simple, functional weather app and look at some of the issues in its design. We shall then refactor and re-design it to create a codebase which will be easier to maintain, less prone to bugs, and easier to add features to. This series is not going to be a deep dive in to the techniques and technologies that we’re going to use, but will be more an exploration of what benefits they give us. In this article we’ll look at how we can improve navigation within our app.



Over the course of this series we’ve put a lot of effort in to separating out distinct areas of responsibility of the app in to their own discrete components, and seen how it can help us when we come to add additional features. However the logic for navigation is still all over our UI layer – in the Activity, and individual Fragments. Not only is this adding a lot of duplication of some very similar code, but it means we have a few instances where one Fragment requires knowledge of a different Fragment in order allow the user to switch between Fragments. For example, CurrentWeatherFragment contains click handling to enable the user to tap on a specific day to get the detailed forecast for that day, and has the code to display a DailyForecastFragment instance, which requires the instantiation of that instance. The obvious solution would be to move all navigation logic to the Activity, and while it would certainly help focus the logic in to one place, it only solves a part of the problem. While it would focus the forward navigation logic, we still have to have logic of how ‘Up’ navigation is handled within individual Fragments. Let’s take a look at arguably the simplest Fragment in the app, PreferencesFragment:

ui/PreferencesFragment.kt class PreferencesFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.preferences) } override fun onAttach(context: Context?) { super.onAttach(context) if (context is AppCompatActivity) { context.supportActionBar?.apply { title = getString(R.string.units) setDisplayHomeAsUpEnabled(true) setHasOptionsMenu(true) } } } override fun onOptionsItemSelected(item: MenuItem): Boolean = when (item.itemId) { android.R.id.home -> { fragmentManager?.popBackStack() true } 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 class PreferencesFragment : PreferenceFragmentCompat ( ) { override fun onCreatePreferences ( savedInstanceState : Bundle ? , rootKey : String ? ) { addPreferencesFromResource ( R . xml . preferences ) } override fun onAttach ( context : Context ? ) { super . onAttach ( context ) if ( context is AppCompatActivity ) { context . supportActionBar ? . apply { title = getString ( R . string . units ) setDisplayHomeAsUpEnabled ( true ) setHasOptionsMenu ( true ) } } } override fun onOptionsItemSelected ( item : MenuItem ) : Boolean = when ( item . itemId ) { android . R . id . home - > { fragmentManager ? . popBackStack ( ) true } else - > super . onOptionsItemSelected ( item ) } }

Most of this code is about displaying and handling the “Up’ button in the ActionBar. Moving this to the Activity would increase the complexity of the Activity because it may need to have different logic applied as to what should be shown depending on which Fragment is currently visible. For example, CurrentWeatherFragment inflates a menu which contains the Action which will display the PreferencesFragment, and we clearly do not want this same menu to appear when PreferencesFragment is visible.

To make things worse, navigation within apps on Android is not the easiest thing to get right, even in a relatively simple app such as Weather Station. This is compounded by the fact that many apps implement their navigation rules slightly differently, so users do not get a consistent experience in different apps. As apps get more complex, having the navigation logic dotted around the app is likely to result in inconsistent navigation behaviour even within the app itself.

At Google IO 2018 a new Jetpack Architecture Component for Navigation was announced, and this can help us address these issues. Like the other Architecture components in Jetpack the Navigation library is an opinionated navigation framework which makes it easy to implement the recommended behaviour. I’m not going to give an in-depth description of the library because there are the official docs and some third-party blog posts which do that. For the purposes of this series of articles we’ll explore the benefits that we get from using the Navigation library in Weather Station.

Weather Station is a single Activity app, so we only need a single Navigation graph and controller. There are four Fragments:

CurrentWeatherFragment

This is the startDestination and displays the current weather and daily forecasts. DailyForecastFragment

This displays the detailed forecast for a specific day NoPermissionsFragment

This is displayed if the location runtime permission has been denied PreferencesFragment This allows the user to select preferred units for temperature and wind speed

Some actions were created which enable us to navigate between these. Let’s take a look at how this affects our Activity:

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 ) } } }

One important thing to note is that we no longer directly reference any of the Fragments – all of the navigation is done by instead referencing the IDs of the actions that we created. This can really have some benefits in larger apps where we may re-use the same action all over the app. If we need to change something then we change the action itself, and everywhere it is used will automatically change accordingly.

Another important thing is that there isn’t a FragmentTransaction, or reference to FragmentManager in sight. Those that are familiar with Fragments will be painfully aware that there will be chunks of almost identical boilerplate dotted around most codebases which are responsible for handling Fragment transactions and managing the back stack. The Navigation manager does all of that for you, and the navigate() calls are doing all of that with any specifics such as Fragment transition animations being defined by the action itself.

Not only that, but we will now get a far more consistent navigation user experience throughout our app which will also be in keeping with any other apps which utilise the Navigation library.

Last, but by no means least, in our Fragments we get similar benefits to those we saw in the Activity, but we also lose much of the boilerplate setup that we needed previously. The individual Fragments are no longer responsible for ensuring that the ‘Up’ navigation is shown in the ActionBar, nor handling what happens when it is tapped. This is most perfectly seen when we take another look at PreferencesFragment which we studied at the beginning of this article. Thanks to the Navigation library we can now simplify it to this:

ui/PrecerencesFragment.kt class PreferencesFragment : PreferenceFragmentCompat() { override fun onCreatePreferences(savedInstanceState: Bundle?, rootKey: String?) { addPreferencesFromResource(R.xml.preferences) } } 1 2 3 4 5 6 class PreferencesFragment : PreferenceFragmentCompat ( ) { override fun onCreatePreferences ( savedInstanceState : Bundle ? , rootKey : String ? ) { addPreferencesFromResource ( R . xml . preferences ) } }

That is a really good highlight of how efficient use of the Navigation library can help in the task of keeping our Fragments focused on handling the UI state, and it has helped us to further separate our concerns.

I must admit that I am already a big fan of the Navigation library even though (at the time of writing) it is still in alpha release. It gives some pretty tangible benefits by making fragment management much simpler and more consistent across the app, and giving our app a consistent navigation experience by doing so. That said, I did encounter a few pain points when implementing the Navigation library but I will cover those in a future article.

With navigation much better organised, and with all of the other work that we’ve done throughout this series, our Weather Station codebase if much better structured and should be much easier to maintain. I’m not saying it’s prefect, or that there aren’t other ways that we can improve it, but that’s where we’ll leave it for now.

The source code for this article 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.