Single Activity с Navigation Component. Или как я мучался с графами. Boilerplate ч. 1

от автора

Всем привет! Меня зовут Алишер, Android-разработчик уже как 1,5 года. За это время у меня появился шаблонный (Boilerplate) проект в котором у нас базовая архитектура приложения. А в этой статье я расскажу, и покажу как я ел Single Activity Architecture с Fragment’ами и Navigation Component.

Для общего понимания необходимо прочитать отличную статью про Single Activity, Лицензия на вождение болида, или почему приложения должны быть Single-Activity, и для дополнения части Navigation Component-дзюцу.

В реализации Single Activity основной вопрос, на что заменить Activity? Основываясь на вышеперечисленных статьях мы будем заменять Activity на FlowFragment’ы, а что это? Это Fragment который выполняет функцию Activity. В Navigation Component это у нас фрагмент со своим контейнером и графом. Чтобы не писать лишний код, напишем базовый класс:

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

Это абстрактный класс с инициализацией navController‘a, нужно уточнить момент так как это будет вложенный фрагмент в основной контейнер Activity, нам нужно при инициализации navController‘a использовать childFragmentManager.

Далее приступим как это все будет выглядеть в реальном проекте. Самый простой пример у нас есть флоу Авторизации / Регистрации и Главная страница с нижней навигацией.

Создадим SignFlowFragment который отвечает за Авторизацию / Регистрацию. И MainFlowFragment для Главной страницы с нижней навигацией.

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>

Дальше создаем SignIn и SignUp fragment’ы. И выстраиваем навигацию внутри sign_graph. Кейс такой что нам нужно навигировать с SignIn в SignUp и MainFlowFragment. А как навигировать между FlowFragment’ами. Создадим перед этим kotlin file 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 это у нас navController MainActivity который поможет нам навигировать между FlowFragment’ами. Остальные два extension’a для безопасной навигации, так как при быстрой навигации (либо быстро нажать на одну кнопку с переходом, либо две разные кнопки с переходами) происходит краш IllegalArgumentException.

Далее как происходит навигация с 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)     } }

Но вот вопрос как это все связать в MainActivity и какой фрагмент должен открытся первым, такой кейс мы решим с помощью динамического сеттинга startDestination‘а. Перед этим нужно убрать app:startDestination в основном графе и app:navGraph с 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>

Как происходит инициализация navController‘a в 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 }

Плюс этого подхода. Мы решаем проблему SharedViewModel’a с Single Activity. При использовании by activityViewModels, наш ViewModel становиться Singleton’ом так как в таком случае ViewModel уничтожается при уничтожении activity, а он у нас только один на все приложение. Решаем это с помощью navGraphViewModels или hiltNavGraphViewModels, которые привязываются к графу и уничтожаются вместе с ними.

Результат всего выглядит так:

P.S. И да, переезжаем на Compose и зачем все это 🙂 Если есть какие-то моменты, открыт к конструктивной критике.

Этот проект
Boilerplate


ссылка на оригинал статьи https://habr.com/ru/post/654599/