Всем привет! Меня зовут Алишер, 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 и зачем все это 🙂 Если есть какие-то моменты, открыт к конструктивной критике.
ссылка на оригинал статьи https://habr.com/ru/post/654599/
Добавить комментарий