イカなる時も

なんか色々垂れ流してます

2023/07/01

composeのサンプルコードを触って、ナビゲーションボトムバーをどう実装するかみた。

@Composable
fun PlayGroundScreen() {
    val navController = rememberNavController()

    Scaffold(
        bottomBar = {
            BottomNavigation(
                backgroundColor = Color.LightGray,
            ) {
                val navBackStackEntry by navController.currentBackStackEntryAsState()
                val currentDestination = navBackStackEntry?.destination
                items.forEach { screen ->
                    BottomNavigationItem(
                        icon = { Icon(screen.iconImage, contentDescription = null) },
                        label = { Text(screen.route) },
                        selected = currentDestination?.hierarchy?.any { it.route == screen.route } == true,
                        onClick = {
                            navController.navigate(screen.route) {
                                // navigateを実行するとバックスタックにdestinationの情報がどんどん積まれのを避けるためpopup
                                popUpTo(navController.graph.findStartDestination().id) {
                                    saveState = true
                                }
                                // 同じdestinationが再度選択されたとき、インスタンスを再利用する。
                                launchSingleTop = true
                                // 同じdestinationが再度選択されたとき、状態をレストアする。
                                restoreState = true
                            }
                        }
                    )
                }
            }
        },
        content = { innerPadding ->
            NavHost(
                navController = navController,
                startDestination = Screen.Profile.route,
                modifier =  Modifier.padding(innerPadding),
            ) {
                composable(Screen.Profile.route) { Profile(navController) }
                composable(Screen.FriendsList.route) { Favotite(navController) }
            }
        }
    )
}

rememberNavController()でNavControllerを取得する。NavControllerは各destinationのbackstackやnavigation graphを持っているので、それをcompositionを超えて保存しておきたいからrememberしてる感じ。 画面が破棄されたときはどうするんだろうと思ったけど、中でrememberSaveableを使っているので大丈夫そう。

@Composable
public fun rememberNavController(
    vararg navigators: Navigator<out NavDestination>
): NavHostController {
    val context = LocalContext.current
    return rememberSaveable(inputs = navigators, saver = NavControllerSaver(context)) {
        createNavController(context)
    }.apply {
        for (navigator in navigators) {
            navigatorProvider.addNavigator(navigator)
        }
    }
}

popupしているところは少し複雑かも。

navigateが実行されるたびにバックスタックにdestinationが積まれてしまう。そうすると、例えば戻るボタンをタップしても(backstackに積まれた分だけ)同じ画面が表示され続ける(多分)。UXとしては最悪そうなのでちゃんとpopupしておく。

popUpTo(navController.graph.findStartDestination().id) {
    saveState = true
}

すでにbackstackのtopに積まれているdestinationと同じところへnavigateするとき、新たにbackstackに積むのは無駄なのでインスタンスを再利用する。つまり何もしないことっぽい(多分)。

launchSingleTop = true

これも同じところにnavigateするとき、stateが変わっちゃう(UIがリセットされる?)のを防ぐために設定するっぽい(多分)。

restoreState = true

あとはScaffoldのcontentにある通り、遷移先で画面に描画したいComposable関数を呼んであげる。

NavHost(
navController = navController,
startDestination = Screen.Profile.route,
modifier =  Modifier.padding(innerPadding),
) {
    composable(Screen.Profile.route) { Profile(navController) }
    composable(Screen.FriendsList.route) { Favotite(navController) }
}

SwiftUIの方がシンプルにかける感じはした。