Всем привет! Меня зовут Алишер, 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
который отвечает за Авторизацию / Регистрацию:
class SignFlowFragment : BaseFlowFragment(
R.layout.flow_fragment_sign, R.id.nav_host_fragment_sign
)
<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>
И MainFlowFragment
для Главной страницы с нижней навигацией:
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)
}
}
<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 Architecture. Через by activityViewModels
он привязывается к жизненному циклу activity, а он у нас один на все приложение, то есть наш SharedViewModel становиться Singleton'ом что не есть хорошо. Решаем это с помощью navGraphViewModels либо hiltNavGraphViewModels или же koinNavGraphViewModel, которые привязываются к графу и уничтожаются вместе с ними.
Результат всего выглядит так:
P.S. И да, переезжая на Compose зачем все это :) Если есть какие-то моменты, открыт к конструктивной критике.
Этот проект
Boilerplate-Android
sputnic
Compose navigation это полнейший трэш. Лучше пока сохранять фрагменты
TheAlisher Автор
В данный момент да, надеемся на улучшения в будущем