Single Activity with Navigation Component. Or how I suffered with counts. Boilerplate part 1

Hello! My name is Alisher, I’ve been an Android developer for 1.5 years now. During this time, I have a template (Boilerplate) project in which we have the basic architecture of the application. And in this article I will tell and show how I ate Single Activity Architecture with Fragments and Navigation Component.

For a general understanding, it is necessary to read an excellent article about Single Activity, License to drive a car, or why applications should be Single-Activity, and to complement the Navigation Component part of the jutsu.

In the implementation of a Single Activity, the main question is what to replace the Activity with? Based on the above articles, we will replace Activity with FlowFragments, but what is it? This is a Fragment that performs the function of an Activity. In the Navigation Component, we have a fragment with its own container and graph. In order not to write extra code, let’s write a base class:

abstract class BaseFlowFragment(
    @LayoutRes layoutId: Int,
    @IdRes private val navHostFragmentId: Int
) : Fragment(layoutId) {

    final override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

        val navHostFragment =
            childFragmentManager.findFragmentById(navHostFragmentId) as NavHostFragment
        val navController = navHostFragment.navController

        setupNavigation(navController)
    }

    protected open fun setupNavigation(navController: NavController) {
    }
}

This is an abstract class with initialization navController‘a, we need to clarify the moment since this will be a nested fragment in the main container of the Activity, we need to initialize navController‘a use childFragmentManager.

Next, let’s start how it all will look in a real project. In the simplest example, we have a Login / Register flow and a Home page with a bottom navigation.

Let’s create SignFlowFragment which is responsible for Authorization / Registration. AND MainFlowFragment for the Home page with bottom navigation.

class SignFlowFragment : BaseFlowFragment(
    R.layout.flow_fragment_sign, R.id.nav_host_fragment_sign
)

class MainFlowFragment : BaseFlowFragment(
    R.layout.flow_fragment_main, R.id.nav_host_fragment_main
) {
    
    private val binding by viewBinding(FlowFragmentMainBinding::bind)

    override fun setupNavigation(navController: NavController) {
        binding.bottomNavigation.setupWithNavController(navController)
    }
}
<FrameLayout 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=".presentation.ui.fragments.sign.SignFlowFragment">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment_sign"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:navGraph="@navigation/sign_graph" />


</FrameLayout>



<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=".presentation.ui.fragments.main.MainFlowFragment">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment_main"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="0dp"
        android:layout_height="match_parent"
        app:defaultNavHost="true"
        app:layout_constraintBottom_toTopOf="@id/bottom_navigation"
        app:layout_constraintTop_toTopOf="parent"
        app:navGraph="@navigation/main_graph" />

    <com.google.android.material.bottomnavigation.BottomNavigationView
        android:id="@+id/bottom_navigation"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:layout_constraintBottom_toBottomOf="parent"
        app:menu="@menu/menu_bottom_navigation" />


</androidx.constraintlayout.widget.ConstraintLayout>

Next we create SignIn And SignUp fragments. And we build navigation inside sign_graph. The case is such that we need to navigate with SignIn in SignUp And MainFlowFragment. And how to navigate between FlowFragments. Let’s create a kotlin file before this NavigationExtensions:

fun Fragment.activityNavController() = requireActivity().findNavController(R.id.nav_host_fragment)

fun NavController.navigateSafely(@IdRes actionId: Int) {
    currentDestination?.getAction(actionId)?.let { navigate(actionId) }
}

fun NavController.navigateSafely(directions: NavDirections) {
    currentDestination?.getAction(directions.actionId)?.let { navigate(directions) }
}

activityNavController we have it navController MainActivity which will help us navigate between FlowFragments. The remaining two extensions are for safe navigation, since fast navigation (either quickly pressing one button with a transition, or two different buttons with transitions) crashes IllegalArgumentException.

Next, how does navigation work with SignInFragment

private fun clickSignIn() {
    binding.buttonSignIn.setOnClickListener {
        UserData.isAuthorized = true
        activityNavController().navigateSafely(R.id.action_global_mainFlowFragment)
    }
}

private fun clickSignUp() {
    binding.buttonSignUp.setOnClickListener {
        findNavController().navigateSafely(R.id.action_signInFragment_to_signUpFragment)
    }
}

But the question is how to tie all this into MainActivity and which fragment should be opened first, we will solve such a case using a dynamic setting startDestination‘but. Must be removed before app:startDestination in the main line and app:navGraph from FragmentContainerView.

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout 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=".presentation.ui.activity.MainActivity">

    <androidx.fragment.app.FragmentContainerView
        android:id="@+id/nav_host_fragment"
        android:name="androidx.navigation.fragment.NavHostFragment"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:defaultNavHost="true" />


</FrameLayout>
<?xml version="1.0" encoding="utf-8"?>
<navigation 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:id="@+id/nav_graph"
    tools:ignore="InvalidNavigation">

    <action
        android:id="@+id/action_global_signFlowFragment"
        app:destination="@id/signFlowFragment"
        app:popUpTo="@id/nav_graph" />

    <action
        android:id="@+id/action_global_mainFlowFragment"
        app:destination="@id/mainFlowFragment"
        app:popUpTo="@id/nav_graph" />

    <fragment
        android:id="@+id/mainFlowFragment"
        android:name="com.alish.navigationflowsample.presentation.ui.fragments.main.MainFlowFragment"
        android:label="flow_fragment_main"
        tools:layout="@layout/flow_fragment_main" />

    <fragment
        android:id="@+id/signFlowFragment"
        android:name="com.alish.navigationflowsample.presentation.ui.fragments.sign.SignFlowFragment"
        android:label="flow_fragment_sign"
        tools:layout="@layout/flow_fragment_sign" />


</navigation>

How is the initialization navController‘a in MainActivity

private fun setupNavigation() {
    val navHostFragment =
        supportFragmentManager.findFragmentById(R.id.nav_host_fragment) as NavHostFragment
    navController = navHostFragment.navController

    val navGraph = navController.navInflater.inflate(R.navigation.nav_graph)
    when {
        UserData.isAuthorized -> {
            navGraph.setStartDestination(R.id.mainFlowFragment)
        }
        !UserData.isAuthorized -> {
            navGraph.setStartDestination(R.id.signFlowFragment)
        }
    }
    navController.graph = navGraph
}

plus of this approach. We are solving SharedViewModel’s problem with Single Activity. When using by activityViewModels, our ViewModel becomes a Singleton, since in this case the ViewModel is destroyed when the activity is destroyed, and we only have one for the entire application. We solve it with navGraphViewModels or hiltNavGraphViewModelswhich are attached to the graph and destroyed along with them.

The result of everything looks like this:

PS And yes, we are moving to Compose and why all this 🙂 If there are any points, I am open to constructive criticism.

This project
Boiler plate

Similar Posts

One Comment

  1. That’s what I looking for, I realize that Nav Comp with Authorization app not suitable for Single Activity Arch. It is hard to implement that. Thanks for your effort. I added small issue on Github.

Leave a Reply

Your email address will not be published. Required fields are marked *