Навигация на Jetpack Compose by Google

от автора

В данной статье опишу подход google, как организовать навигацию в android проекте на чистом compose UI.

Добавление зависимостей Gradle

Откройте файл app/build.gradle.kts и добавьте в секцию dependencies зависимость navigation-compose.

dependencies {     implementation("androidx.navigation:navigation-compose:2.4.0-beta02") }

Установка NavController

NavController является корневым элементом навигации compose, который отвечает за backstack composable функций её перемещение вперед, назад управление состоянием.

class MainActivity : ComponentActivity() {      override fun onCreate(savedInstanceState: Bundle?) {         super.onCreate(savedInstanceState)         setContent {             App()         }     } }
@Composable fun App() {     AppTheme {         val appState = rememberAppState()         Scaffold(                     bottomBar = {                         if (appState.shouldShowBottomBar) {                             BottomBar(                                     tabs = appState.bottomBarTabs,                                     currentRoute = appState.currentRoute!!,                                     navigateToRoute = appState::navigateToBottomBarRoute                             )                         }                     },                     scaffoldState = appState.scaffoldState             ) {             NavHost(                     navController = appState.navController,                     startDestination = MainDestinations.HOME_ROUTE             ) {                 navGraph()             }         }     } }

Добавим подписчика для отслеживания событий изменения состояния навигации rememberAppState()

@Composable fun rememberAppState(         scaffoldState: ScaffoldState = rememberScaffoldState(),         navController: NavHostController = rememberNavController() ) =         remember(scaffoldState, navController) {             AppState(scaffoldState, navController)         }      

Определим Graph навигации, укажем название графа route и стартовый экран startDestination (тип задаваемых значений String)

fun NavGraphBuilder.navGraph() {     navigation(             route = MainDestinations.HOME_ROUTE,             startDestination = HomeSections.CATALOG.route     ) {         addHomeGraph()     } }

Граф навигации

Определим какие будут экраны в графе

fun NavGraphBuilder.addHomeGraph(         modifier: Modifier = Modifier ) {     composable(HomeSections.CATALOG.route) {         CatalogScreen()     }     composable(HomeSections.PROFILE.route) {         ProfileScreen()     }     composable(HomeSections.SEARCH.route) {         SearchScreen()     } }

Добавим объект MainDestinations, который будет содержать название экранов, по которым будет осуществляться навигация.

object MainDestinations {     const val HOME_ROUTE = "home"     const val GAME_CARD_DETAIL_ROUTE = "cardRoute"     const val GAME_CARD = "gameCard"     const val SUB_CATALOG_ROUTE = "subCatalog"     const val CATALOG_GAME = "catalogGame" }

Добавим enum class содержащий список вкладок в bottomNavigation

enum class HomeSections(         @StringRes val title: Int,         val icon: ImageVector,         val route: String ) {     CATALOG(R.string.home_catalog, Icons.Outlined.Home, "$HOME_ROUTE/catalog"),     PROFILE(R.string.home_profile, Icons.Outlined.AccountCircle, "$HOME_ROUTE/profile"),     SEARCH(R.string.home_search, Icons.Outlined.Search, "$HOME_ROUTE/search") }

Добавим класс работающий с состоянием навигации AppState.kt

@Stable class AppState(         val scaffoldState: ScaffoldState,         val navController: NavHostController ) {     // ----------------------------------------------------------     // Источник состояния BottomBar     // ----------------------------------------------------------      val bottomBarTabs = HomeSections.values()     private val bottomBarRoutes = bottomBarTabs.map { it.route }      // Атрибут отображения навигационного меню bottomBar     val shouldShowBottomBar: Boolean         @Composable get() = navController                 .currentBackStackEntryAsState().value?.destination?.route in bottomBarRoutes      // ----------------------------------------------------------     // Источник состояния навигации     // ----------------------------------------------------------      val currentRoute: String?         get() = navController.currentDestination?.route      fun upPress() {         navController.navigateUp()     }      // Клик по навигационному меню, вкладке.     fun navigateToBottomBarRoute(route: String) {         if (route != currentRoute) {             navController.navigate(route) {                 launchSingleTop = true                 restoreState = true                 //Возвращаем выбранный экран,                  //иначе если backstack не пустой то показываем ранее открытое состяние                 popUpTo(findStartDestination(navController.graph).id) {                     saveState = true                 }             }         }     } }  private fun NavBackStackEntry.lifecycleIsResumed() =         this.lifecycle.currentState == Lifecycle.State.RESUMED  private val NavGraph.startDestination: NavDestination?     get() = findNode(startDestinationId)  private tailrec fun findStartDestination(graph: NavDestination): NavDestination {     return if (graph is NavGraph) findStartDestination(graph.startDestination!!) else graph }

Промежуточный результат описанный выше. Добавили graph, навигационное меню, описали экраны которые будут участвовать в навигации. Далее опишу два способа как передать параметры из одного экрана в другой.

Передача аргументов

Пример 1. Сериализуем передаваемый объект в String json

добавим в AppState.kt функцию для перехода на карточку с игрой. Передаваемый объект в моём случае это был data class GameCard должен быть помечен аннотацией Serializable.

fun navigateToGameCard(game: GameCard, from: NavBackStackEntry) {         //Проверяем ЖЦ навигации, чтобы избавиться от повторяющихся событий. Ложных нажатий.         if (from.lifecycleIsResumed()) {             navigateModel(                     route = MainDestinations.GAME_CARD_DETAIL_ROUTE,                     model = game             )         } }      inline fun <reified T> navigateModel(route: String, model: T) {     val json = Json.encodeToString(model)     navController.navigate("$route/$json") }

заметим, что метод navController.navigate(..) принимает тип String на вход т.е. route содержащий в себе путь, что открыть и аргумент после /

Теперь расшифруем данные на принимающей стороне и выполним переход на карточку с игрой

Модифицируем addHomeGraph, добавив composable открытия карточки.

fun NavGraphBuilder.addHomeGraph(         upPress: () -> Unit ) {     composable(             route = "${MainDestinations.GAME_CARD_DETAIL_ROUTE}/{${MainDestinations.GAME_CARD}}",             arguments = listOf(navArgument(MainDestinations.GAME_CARD) { type = NavType.StringType })     ) { backStackEntry ->         val arguments = requireNotNull(backStackEntry.arguments)         arguments.getString(MainDestinations.GAME_CARD)?.let { cardDataString ->             val card = Json.decodeFromString<GameCard>(cardDataString)             CardDialog(card, upPress)         }     } }

Указываем route, который принимает на вход название пути MainDestinations.GAME_CARD_DETAIL_ROUTE и объект MainDestinations.GAME_CARD string который открываем. Следующий параметр arguments который содержит в себе список аргументов примитивных типов.

Пример 2. Передача параметра

fun navigateToGameCard(game: Int, from: NavBackStackEntry) {         //Проверяем ЖЦ навигации, чтобы избавиться от повторяющихся событий. Ложных нажатий.         if (from.lifecycleIsResumed()) {             navController.navigate("${MainDestinations.GAME_CARD_DETAIL_ROUTE}/$game")         }     }
composable(             route = "${MainDestinations.GAME_CARD_DETAIL_ROUTE}/{${MainDestinations.GAME_CARD}}",             arguments = listOf(navArgument(MainDestinations.GAME_CARD) { type = NavType.IntType })     ) { backStackEntry ->         val arguments = requireNotNull(backStackEntry.arguments)         val gameCardId = arguments.getInt(MainDestinations.GAME_CARD, 0)         if(gameCardId != 0)             CardDialog(gameCardId, upPress, {}, {})     }

Отличительной особенностью является, передача ID карточки с последующим её запросом к БД для извлечения всех необходимых данных.

Примечание: единственным разочарованием было, что теперь при навигации необходимо задавать маршрут перехода route в строковом формате, тогда как обычном jetpack navigation задавались id для фрагментов в создаваемом графе и системой создавался список маршрутов в ресурсах. Чтобы меньше создавать логических ошибок, рекомендую названия route выносить в отдельный файл, который будет за это отвечать.

Репозиторий с рассмотренной навигацией можно найти на github.


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


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *