One popular way to structure screens in Android applications is to dedicate a top-level Fragment to each screen. Then, to switch between screens, you replace the currently visible Fragment with another Fragment.
In my previous post I explained the architectural significance of properly designed navigation logic. If you haven’t read it yet, I recommend that you do that now. In this follow-up article I’ll show you how to implement ScreenNavigator abstractions described in the previous post. In addition, I want to give a shout-out to FragNav library which is amazing, but not widely known tool.
Why You Need Third-Party Library for Navigation between Fragments
I’ve been working with Fragments since I decided to become an Android developer. What I can tell you after all these years is that Fragments are tricky beasts. The most difficult thing about Fragments is their lifecycle, of course, but navigation between Fragments isn’t that straightforward either.
To replace a Fragment in some container ViewGroup you can use the following code:
getSupportFragmentManager() .beginTransaction() .replace(R.id.fragment_container, fragment) .commit();
That’s not difficult. However, Fragment transactions won’t be added to backstack by default, so you won’t be able to navigate back to the previous screen. To enable back-navigation, you’d do something like this:
getSupportFragmentManager() .beginTransaction() .addToBackStack(null) .replace(R.id.fragment_container, fragment) .commit();
Then you can go back using this logic:
getSupportFragmentManager().popBackStack();
In some cases, you might want to clear all other backstacked Fragments before you navigate to the next one. This code achieves that:
getSupportFragmentManager().popBackStack(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
So, you can handle Fragments navigation manually, but it might be tricky. In addition, there are some edge cases that you wouldn’t know about until you run into them. Therefore, instead of dealing with all these complications yourself, you can just use a third-party lib that implements the required functionality.
One such third-party lib is FragNav, and it’ll be the main focus of this article.
Set Up FragNav
FragNav library exposes most of its functionality through a class called FragNavController. This class is stateful, which means that you can’t create a new instance in each Fragment (because they won’t share the same state). Therefore, you need to make sure that all navigation clients within the scope of each specific Activity use the same instance of FragNavController.
The most effective way of achieving the above requirement would be to use proper dependency injection in your application. Then you can make FragNavController so-called Activity-scoped object. If you don’t use dependency injection, then just instantiate FragNavController inside Activity and make sure that all navigation calls are delegated to it. Note that when I say “all calls”, I also mean calls from Fragments and other components which reside within that Activity.
Once you’ve got FragNavController scoped to Activity, construct ScreenNavigator abstraction in this manner:
public class ScreensNavigator { private final FragNavController mFragNavController; public ScreensNavigator(FragNavController fragNavController) { mFragNavController = fragNavController; } }
Before we use FragNavController, it must be initialized. Add init() method to ScreenNavigator and put this logic into it:
public void init(Bundle savedInstanceState) { mFragNavController.setRootFragmentListener(mRootFragmentListener); mFragNavController.initialize(FragNavController.TAB1, savedInstanceState); }
Then call this new init() method from onCreate() method of each of your Activities that will use ScreenNavigator:
@Override protected void onCreate(@Nullable Bundle savedInstanceState) { ... mScreensNavigator.init(savedInstanceState); }
Please note how I pass savedInstanceState Bundle as a parameter. This is crucially important to do for the navigation to work properly across configuration changes and process death. However, to make sure that this Bundle contains FragNavController’s state, you’ll need to add additional method to ScreenNavigator:
public void onSaveInstanceState(Bundle saveInstanceState) { mFragNavController.onSaveInstanceState(saveInstanceState); }
Call this method from onSaveInstanceState() method of each of your Activities that will use ScreenNavigator:
@Override public void onSaveInstanceState(Bundle saveInstanceState) { super.onSaveInstanceState(saveInstanceState); mScreensNavigator.onSaveInstanceState(saveInstanceState); }
This completes the minimal setup for FragNavController.
Define The Root Fragment
If you ever implemented manual navigation between Fragments, you surely know this pattern in Activity’s onCreate() method:
if (savedInstanceState == null) { getSupportFragmentManager() .beginTransaction() .addToBackStack(null) .replace(R.id.fragment_container, FirstFragment.newInstance()) .commit(); }
I check whether the Activity is created for the first time (that’s the meaning of savedInstanceState being null), and, if it is, I navigate to the very first Fragment.
FragNavController takes care of this check for you. You just need to tell it which Fragment is the first one. In terms of FragNav, that first Fragment is called “root Fragment”. The way you define root Fragment is using RootFragmentListener. Inside init() method I already set mRootFragmentListener on FragNavController, but I haven’t shown you its implementation yet. That’s it:
private FragNavController.RootFragmentListener mRootFragmentListener = new FragNavController.RootFragmentListener() { @Override public int getNumberOfRootFragments() { return 1; } @NotNull @Override public Fragment getRootFragment(int index) { switch (index) { case FragNavController.TAB1: return RootFragment.newInstance(); default: throw new IllegalStateException("unsupported tab index: " + index) } } };
At this point, you might ask yourself what’s the reason for this approach and why there is this TAB1 constant there. I’ll write about tabs a bit later, but, in short, FragNav supports bottom tab navigation out of the box. Therefore, even if you don’t have bottom tabs, you’ll need to kind of pretend that you do have one tab when you use FragNav.
After you define the root Fragment this way, your application will start and show this Fragment right away.
Navigate Between Fragments
Until now, you wrote quite a bit of code to set up FragNav in your app and then show the first Fragment. Was all this complexity justified? Yes, it was, because now, whenever you need to navigate to another Fragment, you can just add a very simple method to ScreenNavigator:
public void toProductDetails(Product product) { mFragNavController.pushFragment(ProductDetailsFragment.newInstance(product)); }
That’s it. Call this method from other clients and you’ll get to this screen.
Back Navigation
To navigate back to the previous screen, add this method to ScreenNavigator’s API:
public boolean navigateBack() { if(mFragNavController.isRootFragment()) { return false; } else { mFragNavController.popFragment(); return true; } }
Then override onBackPressed() in your Activities and put this kind of logic in there:
@Override public void onBackPressed() { if (mViewMvc.isDrawerVisible()) { mViewMvc.closeDrawer(); } else if (!mScreensNavigator.navigateBack()) { super.onBackPressed(); } }
Nice!
Clear Previously Shown Fragments Stack
In some situations, you want to navigate to a Fragment while clearing the “history” of navigation, so the user won’t be able to go back. For example, this pattern is often used for applications’ main screens.
That’s how you’d do it with FragNav:
public void toHome() { mFragNavController.clearStack(); mFragNavController.pushFragment(HomeFragment.newInstance()); }
Beautiful!
Bottom Tab Navigation
As I already told you, FragNav supports bottom tabs navigation out of the box. Not only that, it also keeps separate Fragments backstacks for all your tabs. That’s very advanced functionality that would be extremely difficult to implement manually, but it becomes a breeze with FragNav.
To use more than one tab in your app, just go back to RootFragmentListener and declare that you’ll have more than one root Fragment:
private FragNavController.RootFragmentListener mRootFragmentListener = new FragNavController.RootFragmentListener() { @Override public int getNumberOfRootFragments() { return 3; } @NotNull @Override public Fragment getRootFragment(int index) { switch (index) { case FragNavController.TAB1: return RootFragment1.newInstance(); case FragNavController.TAB2: return RootFragment2.newInstance(); case FragNavController.TAB3: return RootFragment3.newInstance(); default: throw new IllegalStateException("unsupported tab index: " + index) } } };
As you see, I also create association between tabs and their respective root Fragments.
After that, all you’ve got to do to navigate to other tabs is add methods similar to the below two to ScreenNavigator:
public void toMainTab() { mFragNavController.switchTab(FragNavController.TAB1); } public void toSettings() { mFragNavController.switchTab(FragNavController.TAB2); }
Now just call these methods in response to user clicks on one of the bottom tabs.
Summary
In this post I demonstrated how you can implement ScreenNavigator using FragNav library. You probably noticed that I’m very impressed with it, but just in case you didn’t, let me state it explicitly: FragNav is amazing.
In my opinion, the elegance of FragNav’s API is comparable to these of OkHttp and Retrofit. All these libraries address complex tasks, but allow you to perform these complex tasks using simple, elegant and powerful APIs. You’ve already seen the simplicity and elegance of FragNav, but I should probably mention its power and flexibility as well.
FragNav is highly configurable and includes some tricks and optimizations that I wouldn’t even think about. You want to allow state loss on all Fragment transactions? This is just one line of code with FragNav. Maybe you want to eagerly pre-instantiate all root Fragments to make switch between tabs quicker? This too is just one line of code. Or, maybe, you’re so obsessed with performance that you don’t want to perform fragment transactions when you switch tabs and would prefer to just show/hide individual Fragments instead? Yes, even such a crazy use case is just one line of code! And this list of configurations if far from being complete.
When I just began my Android journey I wrote all the Fragments management logic manually in each place. It was ugly and led to massive code duplication. Then I realized that I can extract most of this code into base classes for my Activities and Fragments, thus reducing duplication. Later I learned that composition and dependency injection are preferable to inheritance in most cases, so I further extracted this logic into FragmentHelper class and copy-pasted it between my projects. That worked alright, but, sure enough, I discovered bugs in FragmentHelper along the way and wanted to fix them. It was tedious work and, in some cases, it was impossible to do because I wasn’t involved in some project anymore. Therefore, I decided to make FragmentHelper into a proper library. This made the maintenance simpler on my side, and developers who took over my past projects would get standard notifications about new versions.
I was quite happy with FragmentHelper and even considered adding new advanced features. However, last year I started a new Android application which used bottom tabs. The other developer on that project proposed to use FragNav instead of my FragmentHelper. The moment we integrated this library, I immediately knew that I won’t use my FragmentHelper library ever again. So, after investing years, literally, into my own library for Fragment navigation, I deprecated it in the blink of an eye when I learned about FragNav. Today, I think that all Android developers who work with Fragments should be at least aware of its existence.
As always, thanks for reading and don’t forget to subscribe for my newsletter if you liked this article.
Hi, what do you think about official Navigation Component and bottom navigation implementation from Google Samples?
https://github.com/android/architecture-components-samples/tree/master/NavigationAdvancedSample
IMO it looks promising and it has a better support
Hey Paul,
I shared my opinion about Navigation Component in the previous post. TL; DR; my opinion is not favorable and it looks like Navigation Component is the new LoaderManager.
Took a quick look at that sample you’ve linked to. To be honest, I simply can’t understand why would you want to have LiveData which provides NavController. I mean, even the name Live*Data* suggests that it should be used for data, not for objects. The comment in Activity’s onCreate() about necessity of waiting for onRestoreInstanceState() is also puzzling. I haven’t seen anything like that before.
I didn’t dig any deeper because what I saw in just one file pretty much confirms my gut feelings: Navigation Component is to be avoided.
Regards
To be honest I have nightmares since reading this file: https://github.com/android/architecture-components-samples/blob/master/NavigationAdvancedSample/app/src/main/java/com/example/android/navigationadvancedsample/NavigationExtensions.kt
Oh, this comment was intended to be an answer to Paul’s one 😉
Hey Vasiliy,
Funny, I was going to copy this link and find you on the /r/androiddev sub reddit to ask you what your opinion of this library was and then I realized that you wrote this article after all! Lol.
How do you feel FragNav compares to other navigation libraries such as bluelinelabs/Conductor or Zhuinden/simple-stack? At least in terms of ease of implementation, usage, and perhaps complexity? I’ve only used Conductor before as a 3rd party library to aid with navigation, and only ever written custom solutions that abstract all the code needed to perform navigation (for instance, navigateToRoot() would pop all but the root), even now the project I’ve inherited uses a custom solution (it works but it’s so gross) which we would like to transition to something nicer.
Hi,
Yeah, this is a small world )
I never used Conductor, but I did evaluate it for one of my customers. It’s not exactly an alternative to FragNav because the idea there is to not use Fragments at all. I, personally, think that Conductor is a nice framework. There are surely some quirks that you’d discover only if you’d work with it, but in general I think it’s underused in Android world. I’m also not sure about its support for bottom navigation and mutliple backstacks.
Unfortunately, I know even less about Simple-Stack. Wanted to try it for a long time because I believe Gabor (the author) is one of the more knowledgable developers out there, but haven’t had a chance so far. From what Gabor says, Simple-Stack doesn’t support multiple backstacks as well as FragNav. On the other hand, it has some advanced features like replacing one of the backstacked entities. I, personally, never needed that, but if your use cases need that flexibility – I’d give Simple-Stack a try.
Regards
Vasiliy
Hi! Your posts is really awesome. I have a question, how can i use it with dynamic feature module ?
Hello,
I haven’t worked with dynamic feature modules yet, so I’m not sure how it should work.
I think dynamic feature module is very useful for the users, but it cause a lot of problem for developers. I had tried your solution in dynamic feature module and a lot of problem occurs, and i’m trying to find a solution right now. but with Navigation Component, it can solve these problem quite easy. I wait for your post about Dynamic feature module <3 !
Hi Vasiliy
Nice library and great article of course. do you know if this library is compatible with Bottom Navigation View as I’m struggling with the recommended ones(BottomBar and AHBottomBar) to disable animations and doing other stuff on the view?
Hi Gabriel,
I’ve never used any library for bottom navigation. Always implemented it myself. It’s easier than most developers would guess if you don’t need anything special. This lib has a support for simple Fragment animations.
Hi Vasiliy!
Firstly, I have to point out that this is an amazing blog 🙂 It is always a pleasure receiving a email notification about new blogpost.
I have a question about navigation though.
How do you approach a navigation with FragNav when you have authentication flow, which is linear without tabs and it happens mostly separately, and main part of application, which has tabs?
I’ve ended up with separate activities for authentication flow (which has fragments inside of course), and main application. This allowed me to have different navigator implementation, one with single tab and the other with all the tabs needed. I couldn’t figure out clean solution to have only one activity but more accurately – one navigator implementation.
Cheers!
Hi Łukasz,
Thanks for the warm words. I use exactly the same approach for authentication – standalone Activity which encapsulates all auth-related flows. In fact, I use this approach for more than just auth: any kind of separate sub-flow which can be ended at many points is a good candidate for extraction into standalone Activity.
Hey Vasiliy, is it good to handle click events from fragments in activity by using event bus especially those click which takes you to next fragments. Becuase FragnavController is initialized in activity and is not accessible by fragments directly.
Hi,
I would be very hesitant to implemen something like that because clicks from specific components should be handled within those components. Otherwise, you risk making it very difficult to trace flows in your app.
Therefore, my recommendation would be to find a way to make ScreenNavigator (however you call it) accesible in Fragments. Properly structured dependency injection is the best solution for this problem.
Do you still use FragNav in your projects? Or did you move to Jetpack Navigation library?
Asking this because github of FragNav shows that it is not being updated from last 1.5 years.
Yep, using it in a project right now. Haven’t seen one good reason to move to Jetpack navigation so far.
Hi,
I am new to Android and this post is exactly what I wanted. Saved my time.
I have just one question, what is mViewMvc in following code.
@Override
public void onBackPressed() {
if (mViewMvc.isDrawerVisible()) {
mViewMvc.closeDrawer();
} else if (!mScreensNavigator.navigateBack()) {
super.onBackPressed();
}
}
Thank you
Hey,
mViewMvc is part of MVC architectural pattern which I use when developing for Android. I wrote a series of posts about it which you can read here.
That said, if you’re new to Android, maybe sticking to the “official” MVVM for a while, before you’re familiar with the basics, will be better for you than jumping into other arch patterns.
Regards