アプリの基本的な画面です。
シンプルなもので眺めてみます。
■ 構成
「Column」を使って2つのパーツを縦に並べます。
メイン画面を表示する部分は
「NavHost」、
それを表示切り替えする下側に並んだ操作部分は
「BottomNavigation」
です。
コードは以下のように構成できます。
@Composable
fun MainScreen() {
val navController = rememberNavController()
Column {
NavHost(
navController = navController,
) {
composable(NavigationItem.HOME.route) {
HomeScreen()
}
composable(NavigationItem.CRYPT.route) {
CryptScreen()
}
// ...
}
BottomNavigation {
val backStack by navController.currentBackStackEntryAsState()
val current = backStack?.destination?.route
NavigationItem.values().forEach { item ->
BottomNavigationItem(
// ...
)
}
}
}
}
enum class NavigationItem(
// ...
)
■ NavController
NavHost を BottomNavigation を紐付けるコントローラーです。
val navController = rememberNavController()
両方からアクセスしやすい場所に置くことになります。
■ NavigationItem
BottomNavigation に並べるアイテムの情報をまとめておきます。
以下、3つの組み合わせになります。
- 画面を表す内部文字列(遷移先)
- アイコン画像
- 表示する画面名称文字列
すべて定数なので enum クラスで作成します。
enum class NavigationItem(
val route: String,
val icon: ImageVector,
@StringRes val label: Int,
) {
HOME("home", Icons.Outlined.Home, R.string.nav_label_home),
CRYPT("crypt", Icons.Outlined.MonetizationOn, R.string.nav_label_crypt),
// ...
}
ここでは多言語化を考慮して、
遷移を表す内部で使用する文字列と、画面名称を表示する文字列を区別しています。
必要なければまとめてもいいと思います。
■ BottomNavigation
NavigationItem を BottomNavigation に配置します。
BottomNavigation {
val backStack by navController.currentBackStackEntryAsState()
val current = backStack?.destination?.route
NavigationItem.values().forEach { item ->
BottomNavigationItem(
selected = current == item.route,
onClick = {
navController.navigate(item.route) {
// no back stacks. 0 is the root navigation
popUpTo(id = 0) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = { Icon(item.icon, null) },
label = {
Text(
text = stringResource(item.label),
maxLines = 1
)
},
alwaysShowLabel = false
)
}
}
onClick のラムダブロックでは、
- バックスタックなし。
- 画面の state は保存/リストアする。
としています。
それぞれの画面内での遷移がない場合は、これがシンプルで自然だと思います。
画面のスクロールポジションもこれで保持できます。
■ NavHost
画面のメイン表示部分です。
NavigationItem に紐づけて各画面の Screen-level Composable を記述しておきます。
NavHost(
) {
composable(NavigationItem.HOME.route) {
HomeScreen()
}
composable(NavigationItem.CRYPT.route) {
CryptScreen()
}
// ...
}
■ まとめ
@Composable
fun MainScreen() {
val navController = rememberNavController()
Column(
modifier = Modifier.fillMaxSize(),
verticalArrangement = Arrangement.Bottom
) {
NavHost(
navController = navController,
startDestination = NavigationItem.HOME.route,
modifier = Modifier
.weight(1f)
) {
composable(NavigationItem.HOME.route) {
HomeScreen()
}
composable(NavigationItem.CRYPT.route) {
CryptScreen()
}
// ...
}
BottomNavigation {
val backStack by navController.currentBackStackEntryAsState()
val current = backStack?.destination?.route
NavigationItem.values().forEach { item ->
BottomNavigationItem(
selected = current == item.route,
onClick = {
navController.navigate(item.route) {
// no back stacks. 0 is the root navigation
popUpTo(id = 0) {
saveState = true
}
launchSingleTop = true
restoreState = true
}
},
icon = { Icon(item.icon, null) },
label = {
Text(
text = stringResource(item.label),
maxLines = 1
)
},
alwaysShowLabel = false
)
}
}
}
}
enum class NavigationItem(
val route: String,
val icon: ImageVector,
@StringRes val label: Int,
) {
HOME("home", Icons.Outlined.Home, R.string.nav_label_home),
CRYPT("crypt", Icons.Outlined.MonetizationOn, R.string.nav_label_crypt),
// ...
}
Scaffold って使い勝手悪くね?