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.
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.