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

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.

Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!

В данной статье опишу подход 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/post/594621/


Интересные статьи

Интересные статьи

В очереди в кинотеатр я бросаю быстрый взгляд на часы. Человек позади меня замечает это и спрашивает, который час. Я смотрю на него в неожиданном замешательстве. За две секунды, прошедшие с тех пор,...
Вот такой график в очередной раз показала панель управления для одного из платных приложений. В панели управления заказами тоже пусто.Встречаются ли такие ситуации и у ва...
Одна из самых важных (на мой взгляд) функций в Битрикс24 это бизнес-процессы. Теоретически они позволяют вам полностью избавиться от бумажных служебок и перенести их в эл...
SWAP (своп) — это механизм виртуальной памяти, при котором часть данных из оперативной памяти (ОЗУ) перемещается на хранение на HDD (жёсткий диск), SSD (твёрдотельный накоп...
Как ранжирует Google сайты, как работает его алгоритм? Этими вопросами можно долго мучиться и не находить на них ответа, в то время как давно доступна инструкция Google для асессоров. Безусловно,...