Fragment Navigation for application

And how to build concise system for supporting it

Today there are many ways to organize navigation in an application. You can use activities as “page holders”. You can use fragments as “page holders” in some cases (for example, when you have tablet and phone with different navigation patterns). You can use even pure and custom views, if you want to control almost everything according to navigationing.

There is also pattern known as Single Activity Approach, when you have one Activity as page container, and each fragment represents its own page. Thus you have single activity (almost single in particular cases) application. A some time ago Google officially recommended this way for organizing navigation to implement in application development.

I've decided to tell your about our approach in company that was formulated almost two years ago. And since that time we had several problems, that leaded us to enhancements and sharpened this approach to transform it into something more stable and completed. So we cover those moments, too.

Discover approach

Lets start with declaration of routing interface ‘Navigation’ that is used to provide navigating methods. It is not router from some Architecture ways, so there will be no concrete pages we want to open. Instead of that there will be pack of common methods to interact with navigation system:

interface Navigation {
fun addFragment(fragment: Fragment) = changeFragment(fragment, backStack = true)
fun changeFragment(fragment: Fragment, backStack: Boolean)

fun newRootFragment(fragment: Fragment)
fun changeBackStack(vararg fragmentCreators: () -> Fragment)

fun back()
fun showDialog(dialog: DialogFragment, requestCode: Int)
}

So as you see, there are just general methods to play with back stack and built navigation that is required by app. Next in a base fragment or navigation fragment of my two year old article we need just to receive navigation instance through onAttach method.

private var navigation: Navigation? = null

override fun
onAttach(context: Context?) {
super.onAttach(context)
navigation = (activity as? Navigation)
}

Thus, as you can see, our activity should implement Navigation interface. But this is not quite flexible way, since you need to have base activity, which implements such big amount of methods. So in case if you don’t want to create a special layer of activities hierarchy and increase that hierarchy, it will be pretty messy to implement that interface in base Activity, which already have some logic and code as well.

So for more flexible implementation we introduce NavigationProvider:

interface NavigationProvider {
fun provideNavigation(): Navigation
}

Activity will implement it, and realisation of Navigation will be done in a separate class NavigationDispatcher:

General example of Navigation implementation

Plus of that approach is that you can use your own implementations of Navigation, but it will be easy to inject in your application. For example, you want to hide keyboard every time user leaves current page (go back to previous, or forward to the next one):

Example of decorator for Navigation with ability to auto-hide keyboard every time user leaves current page

Now let’s update fragment’s snippet little bit:

private var navigation: Navigation? = null

override fun
onAttach(context: Context?) {
super.onAttach(context)
navigation =
(activity as? NavigationProvider)?.provideNavigation() ?:
(activity as? Navigation)
}

We remained first approach for multi way to introduce navigation in application. It can be situations, when your activity works with navigation in other way, than Dispatcher and moreover you can not exude that logic in a separate class, so I’ve remained check for Navigation implementation check.

In activity supporting of Navigation will look like:

abstract class NavigationActivity : AppCompatActivity(),
NavigationProvider {

override fun provideNavigation() =
HideKeyboardNavigation(
NavigationDispatcher(this, /*id of navigation container*/)
)

}

First problem: Pass result to the previous fragment

In general aforementioned solution is initial, but pretty working solution. Until you want to return something from one fragment to another. Here we need to enhance our solution and add feature of returning data to previous fragment in a back stack.

To do that, we need to provide an additional method to Navigation interface

fun returnResult(args: Bundle)

Also we need to introduce new interface FragmentResultListener:

interface FragmentResultListener {
fun onFragmentResult(args: Bundle)
}

In NavigationDispatcher we need to add implementation of returnResult method:

override fun returnResult(args: Bundle) {
back()
(getCurrentFragment() as? FragmentResultListener)
?.onFragmentResult(args)
}

Now if you want to support passing result back to previous fragment, you need to implement interface in fragment-receiver and call returnResult in a fragment sender

Second problem: Back intercepting

Next problem is pretty common, too. There are situations, when you want to prevent fragment from closing on back pressing. For example, WebViewFragment maybe does want to delegate back handling to the WebView, and only if WebView can not handle back, then and only then pass control to the NavigationDispatcher.

To resolve that problem, we need to introduce new small interface BackListener:

interface BackListener {
fun onBackPressed(): Boolean
}

Then we need add back handling to navigation Activity:

Base navigation Activity

Unfortunately we can not fully encapsulate onBackPressed handling in NavigationDispatcher because of design of the Activity class. Now in Dispatcher (which will implement BackListener interface) we need to add:

override fun onBackPressed() = (getCurrentFragment() as? BackListener
?: EMPTY_BACK_LISTENER).onBackPressed()

private fun getCurrentFragment() = activity.supportFragmentManager.findFragmentById(containerId)

That’s all. For aforementioned WebViewFragment we will have, for example, next logic:

class WebViewFragment : Fragment(), BackListener {

//...

override fun onBackPressed(): Boolean {
if (webView.canGoBack()) {
webView.goBack()
return true
} else {
return false
}
}

//...
}

Third problem: Translucent status bar

This is pretty common problem, but you may encounter with it a little but later: Translucent status bar. If you read my old article Give Toolbar to each fragment, then following the same logic you need to configure status bar in each fragment, making activity with translucent status bat, or you will be faced with “glitching” status bar.

I mean if you will manually set activity’s flags in each fragment to configure transparency of status bar, then you will have next problem. Suppose, you have 2 fragments.One with translucent status bar, other with not. Thus, first fragment need to fit all whole phone display and second just display without status bar. If you will then go from fist fragment to second, you will see that layout is jumping from full screen to normal mode, which obviously is not a good solution

So to handle such situation, you need to make an activity with translucent status bar by default, and then add let each fragment decide, whether it want to fit system’s window or not, through appropriate attribute in the xml or with some other way. But I recommend in most cases play with fitsSystemWindows attribute, because it more powerful. For example, it automatically handles situations with multi-screen mode. Whether you app will be on top of the screen or on bottom, the system will draw layout correctly (on top you have status bar, and on bottom not, so on bottom toolbar does not need additional space). And I don’t know how to do it manually, since I didn't find a way to determine, whether your app on a top or on a bottom part of splitted screen.

So, as we understood, you need just to make activity with transparent theme and give to each fragment possibility to handle that, by them selfs. If you will use however attribute fitsSystemWindows you will not get desired result out of the box. Here is a link to the problem. To resolve it Christophe Beyls provided special custom FrameLayout:

You need to use this class as your fragment container, not simple FrameLayout.

Also, it will be good for fragment to always have status bar info nearby. For that we need to provide to interfaces similarly to Navigation and NavigationProvider:

interface StatusBarOwner {
val statusBarHeight: Int
val isStatusBarTranslucent: Boolean
var isStatusBarLight: Boolean
}

interface StatusBarProvider {
fun provideStatusBarOwner(): StatusBarOwner
}

So there will be separate StatusBarOwner implementation:

And an Activity will implement StatusBarProvider:

class NavigationActivity: AppCompatActivity(), StatusBarProvider {

private val statusBarManager: StatusBarManager by lazy { StatusBarManager(this, true) }

override fun
provideStatusBarOwner() = statusBarManager

override fun
onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
//...

lifecycle.addObserver(statusBarManager)

//...
}

//...
}

Last but no least, in aforementioned fragment (where we attached Navigation instance) we will add:

private var navigation: Navigation? = null
private var statusBarOwner
: StatusBarOwner? = null

override fun
onAttach(context: Context?) {
super.onAttach(context)
navigation =
(activity as? NavigationProvider)?.provideNavigation() ?:
(activity as? Navigation)

statusBarOwner =
(activity as? StatusBarProvider)?.provideStatusBarOwner() ?:
(activity as? StatusBarOwner)
}

Conclusion

One activity, many fragments is a flexible and solid approach to organize navigation in your application. You need just to be ready for some pitfalls. I’ve described few of them.

Sanbox project to demonstrate all that experience will be added little bit later. Sorry :(

Also, share your thoughts about other pitfalls in comments below. Thank you.

Love being creative to solve some problems with an simple and elegant ways