主に公式ドキュメントに書いてることを自分なりに解釈した言葉でまとめます。
Composeのプロジェクトを作ったら MainActivity
の onCreate()
に setContent
が配置されてると思いますが、その中で NavHost
を用意してあげます。ここが従来のNavigation ComponentのNavGraphみたいな感じになります。
まずは普通に画面遷移
NavHost
を用いて画面遷移する際に NavController
のインスタンスが必要なので、 NavHost
をつくる前に作っておきます。
val navController = rememberNavController()
次に NavHost
を定義します。 setContent
の中で呼ばれるように @Composable
なメソッドの中で定義します。
setContent {
NavHost(navController = navController, startDestination = "home") {
composable(route = "home") {
HomeScreen()
}
composable(route = "detail") {
DetailScreen()
}
}
}
よくありそうな、Home画面のようなリストがある画面から1つのItemをタップしたときに詳細画面に遷移するイメージの NavHost
です。
NavHost
の第2引数には、従来のNavigation Componentにも必要だった startDestination
を設定します。そうすることで、このKeyが route
に設定されている画面から起動します。
HomeScreen()
では、リストの中のItemがタップされたときに DetailScreen()
に遷移したいのですが、この際に、最初に定義した navController
を HomeScreen()
に渡してあげる必要があります。
HomeScreen()
の中で実際になにかタップされたときに DetailScreen()
に遷移したいときはこんな感じです。
@Composable
fun HomeScreen(navController: NavController) {
Button(
onClick = {
navController.navigate("detail")
}
) {
}
}
DetailScreen()
に遷移後、 Toolbar
などに置かれた戻るボタンが押されたときに前の画面に戻りたいときはこうです。
navController.popBackStack()
遷移先に値を渡す
HomeScreen()
から DetailScreen()
に遷移したときに何かしら値を渡したいときがよくあると思います。
まずは String
を渡せるようにしてみましょう。
setContent {
NavHost(navController = navController, startDestination = "home") {
composable(route = "home") {
HomeScreen(navController = navController)
}
composable(
route = "detail/{id}",
arguments = listOf(navArgument("id") { type = NavType.StringType })
) { backStackEntry ->
val argId = backStackEntry.arguments?.getString("id") ?: return@composable
DetailScreen(id = argId)
}
}
}
この NavHost
では HomeScreen()
から DetailScreen()
に id
という名前の String
を遷移時に渡しています。
HomeScreen()
で遷移するときは、こんな感じです。
val id = "hogeId"
navController.navigate("detail/$id")
NavHost
を定義するときに、 backStackEntry
を用いて遷移元から渡ってきた値を取り出すことができます。
また、 arguments
で定義している NavType
には他にもたくさんあります。
String
の渡し方はわかりましたが、実際に開発していて String
や Boolean
などの簡単な型の値を渡したくなるときはあまりなく、 User
がたくさん並んでるリストから1つタップして User
型を UserDetailScreen()
に渡して表示するみたいなユースケースが多いと思います。
そこで、 NavType
には NavType.SerializableType
というのがいるので、 Serializable
な data class
を定義してそれを渡すようにすれば良さそうです。
data class User(
val name: String,
) : Serializable
コードの全体像はこんな感じです。
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
composable(route = "home") {
HomeScreen(navController = navController)
}
composable(
route = "detail/{user}",
arguments = listOf(navArgument("user") { type = NavType.SerializableType(User::class.java) })
) { backStackEntry ->
val user = backStackEntry.arguments?.getSerializable("user") as User
DetailScreen(navController = navController, user = user)
}
}
}
}
}
@Composable
fun HomeScreen(navController: NavController) {
Scaffold {
Column {
Text(text = "This is HomeScreen")
val user = User(name = "hoge")
Button(onClick = { navController.navigate("detail/$user") }) {
}
}
}
}
@Composable
fun DetailScreen(navController: NavController, user: User) {
Scaffold {
Column {
Text(text = "This is DetailScreen $user")
Button(onClick = { navController.popBackStack() }) {
}
}
}
}
これを実行して、実際に遷移させると以下のようなエラーが起きてうまく動きません。
java.lang.UnsupportedOperationException: Serializables don't support default values.
色々と調べてみましたが、わたしの方ではこの解決策は見つけることができませんでした…。
もし、解決策をご存知の方がいましたらご教授願います。。。
そこで、こんな動画を見つけました。
この動画では、これに対処するために User
を一度 Gson
を用いてJsonに変換したものを String
として遷移先に渡して、遷移先の今回でいう DetailScreen()
で String
を User
に変換して取り出す方法で実装していました。
一応これで自分で定義した data class
を遷移先に渡すことができましたが、今はまだこのように実装するしかないのでしょうか…
遷移後に前の画面に戻れないようにする
今までのユースケースでは遷移後に戻るボタンで前の画面に戻れてよかったのですが、ログイン画面でログインボタンを押してホーム画面に戻ってくるときなど、遷移後に遷移前の画面に戻ってほしくないときがあると思います。
そのときは、遷移時にこのように実装します。
Button(
onClick = {
navController.navigate("home") {
popUpTo("login") { inclusive = true }
}
}
)
ホームに navigate
するときに同時に inclusive
というのを設定して現在のログイン画面を popUpTo
してあげます。
これによってAndroidの戻るボタンを押しても前の画面に戻らないような挙動を実現できます。
ネストしたNavigation
従来のNested navigation graphsのように、ComposeのNavigation Componentでもアプリ内の特定の画面遷移をモジュール化することができます。
たとえば、ログインやユーザ登録などの認証機能周りの画面遷移のモジュールと、ログイン後のBottomNavigationがあるような様々な機能に触れる画面遷移のモジュールなどでしょうか。
NavHost
の中で navigation
を用いて特定の画面遷移の塊をモジュール化してこのように実装します。
setContent {
val navController = rememberNavController()
NavHost(navController = navController, startDestination = "home") {
navigation(startDestination = "login", route = "auth") {
composable(route = "login") {
LoginScreen()
}
composable(route = "register") {
RegisterScreen()
}
}
composable(route = "home") {
HomeScreen(navController = navController)
}
composable(
route = "detail/{user}",
arguments = listOf(navArgument("user") { type = NavType.SerializableType(User::class.java) })
) { backStackEntry ->
val user = backStackEntry.arguments?.getSerializable("user") as User
DetailScreen(navController = navController, user = user)
}
}
}
認証周りの画面遷移を auth
という名前で定義して、遷移時に以下のようにすると auth
のNavigationの塊を見て startDestination
で定義されている login
に遷移します。
navController.navigate("auth")
おわり
ほかにも、ComposeのNavigation Componentにはディープリンクの機能もあってintent filterで特定のComposeの画面に遷移させることもできそうです。
Composeはまだbetaで、私も探り探りなので色々な人と知見を共有して他にもこんなふうに書けるよや、そこは使い方間違ってるよなどあれば是非コメントください。