6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

JetpackComposeのNavigation Componentを触ったのでまとめる

Posted at

主に公式ドキュメントに書いてることを自分なりに解釈した言葉でまとめます。

Composeのプロジェクトを作ったら MainActivityonCreate()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()に遷移したいのですが、この際に、最初に定義した navControllerHomeScreen()に渡してあげる必要があります。

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には他にもたくさんあります。
スクリーンショット 2021-04-18 16.10.22.png

Stringの渡し方はわかりましたが、実際に開発していて StringBooleanなどの簡単な型の値を渡したくなるときはあまりなく、 Userがたくさん並んでるリストから1つタップして User型を UserDetailScreen()に渡して表示するみたいなユースケースが多いと思います。
そこで、 NavTypeには NavType.SerializableTypeというのがいるので、 Serializabledata 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()StringUserに変換して取り出す方法で実装していました。

一応これで自分で定義した 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で、私も探り探りなので色々な人と知見を共有して他にもこんなふうに書けるよや、そこは使い方間違ってるよなどあれば是非コメントください。

参考

6
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?