[Android] 003. androidx.navigation

androidx.navigationは↓で結構前に書いていて、こっそり更新したりしてましたが新しい環境で内容を追加してみたいと思います(令和6年版w)
https://bps-e.com/dev/android-009/

devloperの説明は以下
https://developer.android.com/jetpack/compose/navigation
codelabのサンプル
https://github.com/android/codelab-android-compose/tree/end/NavigationCodelab

依存関係

Project StructuerやGradleなどに直接指定だけではなく、コードからも追加できるようになってました

navigation-commonを追加しなくてもNavGraphBuilder使えるっぽいので
とりあえずnavigation-composeだけ追加しておきます

実装

NavHostControllerの扱い

基底のscreenで持っておけばいいけど、nowinandroidなどgoogleのサンプルなどのようにアプリのステータス管理用のclassを追加してそこに保持しておくのが無難だと思う
※ 内容は以前のものとほぼ同じ

ここでは通常使用するもののみ実装していますが、より複雑な遷移をする場合に必要なbackstackまわりは以下に詳しく説明されていたので参考まで
https://star-zero.medium.com/navigation-compose%E3%81%AEnavoptions-36607e71d2bf

@Stable 
class NavState(val navController: NavHostController) {
    fun onNavigate(route: String, restore: Boolean = false) {
        navController.navigate(route) {
            // 同じ画面に遷移した場合同じ画面が再利用される
            launchSingleTop = true

            if (restore) {
                popUpTo(navController.graph.findStartDestination().id) {
                    saveState = true
                }
                // saveState = trueの場合rememberSavable/viewModelの内容を復元(1度)
                restoreState = true
            }
        }
    }

    fun onBack(route: String = "") {
        if (route.isEmpty()) navController.navigateUp()
        else navController.popBackStack(route, inclusive = false, saveState = false)
    }

    @Composable
    fun currentBackStackEntry() = navController.currentBackStackEntryAsState().value
}
@Composable 
fun rememberNavState(navController: NavHostController = rememberNavController()): NavState = 
    remember(navController) { NavState(navController) }

Route名の扱い

Googleのsampleなどでも色々な定義の仕方がされていますが、enumならforEachで回せるのでenumで定義でいいんじゃないかなと思います
データ渡しをする場合などで柔軟に拡張したい場合はsealed classの方が便利かもです
昔のサンプルの定義名はNavRouteとかだったけど、最近はxxDestinationがトレンドのようです

例.

enum class NavDestination(@IdRes id: Int) {
    Top(R.id.xxx),
    Next(R.id.xxx);
    // routeに指定する文字列を返すと若干便利
    operator fun invoke() = this.name
}

// または
sealed class NavDestination(val route: String, @IdRes val id: Int) {
    data object Top: NavDestination("Top", R.id.xxx)
    data object Next: NavDestination("Next/{id}", R.id.xxx) {
        fun params(id: Int) = "Next/$id"
    }

    operator fun invoke() = this.route
    // operator fun invoke() = this::class.simpleName!!
    companion object {
        fun values() = listOf(Top, Next)
    }
}

// または...
interface NavDestination {
    val route: String
    val id: Int

    operator fun invoke() = this.route
}

object Top : NavDestination {
    override val route = "Top"
    @IdRes override val id = R.id.xxx
}

NavGraphBuilder

NavGraphBuilderを使わなくても実装できますが、画面遷移に階層がある場合などはNavGraphBuilderでまとめると管理しやすいです

fun NavGraphBuilder.navGraph(onBack: (String) -> Unit, onNavigate: (String) -> Unit) {
    NavDestination.entries.forEach { destination ->
        composable(route = destination()) {
            when(destination) {
                NavDestination.Top -> TopScreen(onBack, onNavigate)
                NavDestination.Next -> NextScreen(onBack, onNavigate)
            }
        }
    }
}

@Composable
fun TopScreen(onBack: (String) -> Unit, onNavigate: (String) -> Unit) {}

@Composable
fun NextScreen(onBack: (String) -> Unit, onNavigate: (String) -> Unit) {}

NavHost

内容は以前のものとほぼ同じです

@Composable
fun AppNavHost(navController: NavHostController, onBack: (String) -> Unit, onNavigate: (String) -> Unit) {
    NavHost(navController = navController, startDestination = NavDestination.Top()) {
        navGraph(onBack, onNavigate)
    }
}

@Composable
fun AppContent(navState: NavState = rememberNavState()) {
    AppNavHost(navState.navController, navState::onBack, navState::onNavigate)
}

遷移アニメーションを変更したい場合は以下のようにNavHostか、画面個別に指定したい場合はcomposable()の引数に指定します

NavHost(
    navController = navController,
    startDestination = NavDestination.Top(),
    enterTransition = { fadeIn(animationSpec = tween(700)) },
    exitTransition = { fadeOut(animationSpec = tween(700)) },
    popEnterTransition = { fadeIn(animationSpec = tween(700)) },
    popExitTransition = { fadeOut(animationSpec = tween(700)) },
) { ... }

画面間でデータを渡す

NavHostController.navigate()のるroute指定の文字列に付加して渡します
省略なしの場合は”/”区切り、省略可能にしたい場合はhtmlのパラメータ渡しの文法で指定します
配列の場合は?list=1&list=2&…のような指定しか現在はサポートされていません
https://issuetracker.google.com/issues/291989170

文字列を渡す場合はurlエンコードして渡します
デコードはbackstack側で行われます
半角スペースも%20にエンコードされるようにしてください

composable(route + "/{text}/{id}",
    arguments = listOf(
        navArgument("text") { type = NavType.StringType },
        navArgument("id") { type = NavType.IntType },
    )
) { 
    val text = it.arguments?.getString("text") ?: ""
    val id = it.arguments?.getInt("id") ?: 0
}

composable(route + "?text={text}&id={id}",
    arguments = listOf(
        navArgument("text") { type = NavType.StringType },
        navArgument("id") { type = NavType.IntType },
    )
) { ... }

遷移のイベントのハンドリング

NavHostController.addOnDestinationChangedListenerかcurrentBackStackEntryAsStateでできます

val navBackStackEntry by navState.navController.currentBackStackEntryAsState()
LaunchedEffect(navBackStackEntry) {
    navBackStackEntry?.apply {
        Log.d("entry", "${destination.route}")
        // 引数を取得する例
        arguments?.apply {
            destination.arguments.forEach { entry ->
                val value = when (entry.value.type) {
                    NavType.StringType -> getString(entry.key)
                    NavType.IntType -> getInt(entry.key)
                    else -> null
                }
                Log.d("entry", "${entry.key} = $value")
            }
        }
    }

    // 遷移前の情報(初回遷移時はnull)
    val previous = appState.navController.previousBackStackEntry
    previous?.apply {
        Log.d("previous", "${destination.route}")
    }
}

戻るキーやジャスチャーをハンドリングしたい場合は対象のScreenでBackHandlerを実装します
BackHandlerを使う場合は戻る処理がフックされますのでBackHandler内で独自の遷移処理を追加してください
※ 戻るキーやジャスチャーのみフックされるのでnavControllerのpopBackはハンドリングできないです

@Composable
fun TopScreen(...) {
    // 特にハンドリングのon/offが不要ならtrueを直接指定
    var backHandlingEnabled by remember { mutableStateOf(true) }
    BackHandler(backHandlingEnabled) {
        ...
    }
}

Android Studio Hedgehog 2023.1.1 built on November 10, 2023