[Android] Compose Destinations(v2) 使ってみた(基礎編)

AndroidライブラリのComposeDestinations関係の記事のサムネイル

この記事の内容

  • Compose Destinationsの概要
  • v1とv2の違い
  • 基本的なセットアップと使い方

Compose Destinationsとはなにか

公式のReadmeの説明にはこう書いてありました。

A KSP library that processes annotations and generates code that uses Official Jetpack Compose Navigation under the hood. It hides the complex, non-type-safe and boilerplate code you would have to write otherwise.
No need to learn a whole new framework to navigate – most APIs are either the same as with the Jetpack Components or inspired by them.

https://github.com/raamcosta/compose-dhttps://github.com/raamcosta/compose-destinations/blob/main/README.mdestinations

要約すると以下のような感じだと思います。

  • KSPライブラリです
  • 公式のJetpack Compose Navigationを使うために必要な「非タイプセーフ」「定型的なコード」を減らすためのものです

v2とv1の違い

アニメーションの強化と、コンパイル時のチェックの改善が主な変更点のようです。

全体的に拡張性を向上させた代わりに、記述する部分が多少増えた部分もあるようです。
以下のようなナビゲーショングラフの定義などが必須になりました

Kotlin
// v1まで
@Destination

// v2から
@Destination<RootGraph>

基本的な使い方

セットアップ

まずは依存関係を設定します。
今回は、v2用の設定なのでv1を使いたい場合は使い方が異なる可能性があります。

libs.versions.tomlに以下の記述を追加します。

TOML
[versions]
# ........
ksp = "1.9.0-1.0.13"
composeDestinations = "2.1.0-beta06"

[libraries]
# ........
composeDestinations = { group = "io.github.raamcosta.compose-destinations", name = "core", version.ref = "composeDestinations" }
composeDestinationsBottomSheet = { group = "io.github.raamcosta.compose-destinations", name = "bottom-sheet", version.ref = "composeDestinations" }
composeDestinationsKsp = { group = "io.github.raamcosta.compose-destinations", name = "ksp", version.ref = "composeDestinations" }

[bundles]
composeDestinations-bundle = ["composeDestinations", "composeDestinationsBottomSheet"]

[plugins]
# ........
ksp = { id = "com.google.devtools.ksp", version.ref = "ksp" }

ここで追加したのは、

  • ksp : コンパイル時に処理を追加したい場合に使用するプラグイン
  • composeDestinations-core : ComposeDestinationsのコア機能ライブラリ
  • composeDestinations-bottom-sheet : ComposeDestinationsでBottomSheetへの遷移を行いたい場合に使用する
  • composeDestinations-ksp : コンパイル時に自動でNavGraphを作成するためのKspプラグイン

次に、build.gradle.ksp(Module:)に以下の設定を追加します。

Kotlin
plugins {
    // ......
    alias(libs.plugins.ksp)
}

// ......

dependencies {
    // ......
    implementation(libs.bundles.composeDestinations.bundle)

    ksp(libs.composeDestinationsKsp)
}

以上で依存関係の定義は完了したので、コンパイルして問題が発生しなければ導入成功です。

実装

基本的な画面遷移

このライブラリは画面をコンポーネントとして完全に分離して書くことができます。
BottomSheetやポップアップを使いたい場合でも、Composableなメソッドに完全に分離して記述することができます。

まずは、画面の作成をします。
以下のようなアノテーションと引数を追加することで画面遷移ができるComposableを作成できます

Kotlin
@Destination<RootGraph>()
@Composable
fun HomeScreen(
    navigator: DestinationsNavigator, modifier: Modifier = Modifier
) {
// 画面の実装
}

通常のComposableな画面を作るときと違うのは、以下の点です

  • @Destination<RootGraph> : このアノテーションをつけることでRootGraphという名前のグラフに画面を追加する
  • navigator: DestinationsNavigator : これはDestinationアノテーションを付けて遷移先として設定している場合は必ず必要です。

実際に以下のようにComposableな画面を2つ作成して、どちらも同じようにボタンを真ん中に配置します。
Destinationアノテーションを付けて同じRootGraphグループに所属させます。

このときにHomeScreenのDestinationアノテーションに設定しているstart = trueというのがRootGraphを表示するときに最初に表示される画面になります。

ボタンを押したときに画面遷移をするようにしたいので、処理を追加したいのですが、ComposeDestinationsが生成するクラスが必要になるので、一旦コンパイルを走らせます。

Kotlin
@Destination<RootGraph>(start = true)
@Composable
fun HomeScreen(
    navigator: DestinationsNavigator, modifier: Modifier = Modifier
) {

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
        modifier = modifier.fillMaxSize()
    ) {
        Button(onClick = {
            //  後で実装
        }) {
            Text(text = "通常画面遷移")
        }
    }
}

@Destination<RootGraph>()
@Composable
fun DestinationScreen(navigator: DestinationsNavigator, modifier: Modifier = Modifier) {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
        modifier = modifier.fillMaxSize()
    ) {
        Button(onClick = {
            // 後で実装
        }) {
            Text(text = "Back Home")
        }
    }
}

ここで一度コンパイルを走らせると、画面遷移に必要な情報が自動で生成されます。

コンパイルが正常に終了したあとに、画面遷移の処理を記述します。
以下は、それぞれのComposable関数内のボタン押下時の処理です。

Kotlin
//  HomeScreen
Button(onClick = {
   // 指定した画面に遷移する
   navigator.navigate(DestinationScreenDestination)
}) {
   Text(text = "通常画面遷移")
}

//  DestinationScreen
Button(onClick = {
   // 遷移前の画面に戻る
   navigator.navigateUp()
}) {
   Text(text = "Back Home")
}

ここでDestinationScreenDestinationという作成していない定義を指定していますが、これは、ComposeDestinationライブラリが自動で生成したクラスになります。

このクラスの生成規則は以下のようになっています。

  • @Destinationをつけた@Composable関数についてのクラスを生成する
  • 関数名 + Destinationとなるように生成する

このクラスを引数に渡すことで指定したクラスの生成もとの画面に遷移できます。

最後に呼び出し元にのエントリーポイントとして画面を直接制定するのではなく、以下のような記述を追加します。

Kotlin
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            NavhostExampleTheme {
                Surface(
                   modifier = Modifier.fillMaxSize(),
                   color = MaterialTheme.colorScheme.background
                ) {
                   DestinationsNavHost(
                      navGraph = NavGraphs.root
                   )
                }
            }
        }
    }
}

いつもと違うのは

Kotlin
DestinationsNavHost(
     navGraph = NavGraphs.root
)

この部分ですね。
この部分で表示するナビゲーショングラフを指定して画面を表示させます。
この場合、rootという名前のナビゲーショングラフのstart = trueになっている画面を一番最初に表示します

このrootというのは、RootGraphのことです。
自動生成されます。

NavGraphs.kt
internal object NavGraphs {
    val root = RootNavGraph
}
RootNavGraph.kt
/**
 * Generated from [RootGraph]
 *
 * * 🗺️[RootGraph]
 * * ∙∙∙∙∙∙∙∙↳📍🏁[HomeScreen]
 * * ∙∙∙∙∙∙∙∙↳📍[DestinationScreen]
 */
@Keep
public data object RootNavGraph : BaseRoute(), DirectionNavHostGraphSpec {
    
    override val startRoute: TypedRoute<Unit> = HomeScreenDestination
    
    override val destinations: List<DestinationSpec> get() = listOf(
		HomeScreenDestination,
		DestinationScreenDestination,
    )

	override val defaultTransitions: NavHostAnimatedDestinationStyle = NoTransitions
    
	override val route: String = "root"



}

RootGraphとはなにか

RootGraphはナビゲーショングラフの一例で、画面をグループ化するのに使います。

ライブラリ内でのRootGraph定義は以下のようになっています。

Kotlin
@NavHostGraph
annotation class RootGraph

これを見てわかるように、ただの識別子のような使い方をされています。

RootGraphについては、最初から用意されているナビゲーショングラフですが、もちろん自分で作成する事もできます。
オリジナルのナビゲーショングラフの作成方法については、渡しが使う必要が出てくるか、要望が多い場合にまとめ記事を作成する予定です。

BottomSheetに遷移させる

通常のBottomSheetを作成しようとすると管理するために冗長な記述が必要になります。
(画面が閉じるまでコルーチンで待機したり..etc)

ComposeDestinationsを使用することで、BottomSheetも通常の画面と同じように使用することができるようになります。

以下は、BottomSheet遷移を追加するための手順です。

  1. Composable画面の作成
  2. 画面遷移元の動作を追加
  3. エントリーポイントの改修

1. Composable画面の作成

BottomSheetを作成するには通常の画面遷移の設定に加えて、styleの設定が必要になります。

DestinationStyleBottomSheet::classstyleに設定する必要があります。

他の設定は同じで、前の画面に戻る際の処理も同じでOKです

Kotlin
@Destination<RootGraph>(style = DestinationStyleBottomSheet::class)
@Composable
fun DestinationBottomScreen(navigator: DestinationsNavigator, modifier: Modifier = Modifier) {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
        modifier = modifier.fillMaxWidth().padding(vertical = 10.dp)
    ) {
        Button(onClick = {
            navigator.navigateUp()
        }) {
            Text(text = "Close BottomSheet")
        }
    }
}

画面遷移時に必要なクラスを生成するためにこのタイミングでコンパイルを走らせておいてください

2. 画面遷移元の動作を追加

次は遷移元のBottomSheetの呼び出しロジックを追加します。
と、いったものの通常の画面遷移と何も変わりません。

Kotlin
Button(onClick = {
    navigator.navigate(DestinationBottomScreenDestination)
}) {
     Text(text = "ボトムシートを展開")
}

最後に呼び出し元に少し調整を加える必要があります。

上記2つの設定だけでもエラーは発生しませんが、遷移ボタンを押しても何も起こりません。

3. エントリーポイントの改修

「BottomSheet」を画面遷移で使いたい場合は、MainActivity(エントリーポイント)にModalBottomSheetLayoutを追加して、DestinationsNavHostラップする必要があります。

更に、navControllerを取得して、bottomSheetのイベントなどを紐づける必要もあります。
navControllerはエントリーポイントに渡す必要があります。

これは、初回の設定をしてしまえばそれ移行は意識することなく使うことができるようになります。

Kotlin
val navController = rememberNavController()
val bottomSheetNavigator = rememberBottomSheetNavigator()
navController.navigatorProvider += bottomSheetNavigator

ModalBottomSheetLayout(bottomSheetNavigator = bottomSheetNavigator) {
  // DestinationsNavHostなどの通常の設定
  // navControllerを渡す必要はある
}

実際に使うときの記述は以下のようになると思います。

Kotlin
class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            NavhostExampleTheme {

                val navController = rememberNavController()
                val bottomSheetNavigator = rememberBottomSheetNavigator()
                navController.navigatorProvider += bottomSheetNavigator

                ModalBottomSheetLayout(bottomSheetNavigator = bottomSheetNavigator) {
                    Surface(
                        modifier = Modifier.fillMaxSize(),
                        color = MaterialTheme.colorScheme.background
                    ) {
                        DestinationsNavHost(
                            navGraph = NavGraphs.root,
                            navController = navController
                        )
                    }
                }
            }
        }
    }
}

最後にこれまで追加したコードをまとめました。

最終形

Kotlin
package com.example.navhostexample

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material.navigation.ModalBottomSheetLayout
import androidx.compose.material.navigation.rememberBottomSheetNavigator
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.compose.rememberNavController
import androidx.navigation.plusAssign
import com.example.navhostexample.ui.theme.NavhostExampleTheme
import com.ramcosta.composedestinations.DestinationsNavHost
import com.ramcosta.composedestinations.annotation.Destination
import com.ramcosta.composedestinations.annotation.RootGraph
import com.ramcosta.composedestinations.bottomsheet.spec.DestinationStyleBottomSheet
import com.ramcosta.composedestinations.generated.NavGraphs
import com.ramcosta.composedestinations.generated.destinations.DestinationBottomScreenDestination
import com.ramcosta.composedestinations.generated.destinations.DestinationScreenDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            NavhostExampleTheme {

                val navController = rememberNavController()
                val bottomSheetNavigator = rememberBottomSheetNavigator()
                navController.navigatorProvider += bottomSheetNavigator

                ModalBottomSheetLayout(bottomSheetNavigator = bottomSheetNavigator) {
                    Surface(
                        modifier = Modifier.fillMaxSize(),
                        color = MaterialTheme.colorScheme.background
                    ) {
                        DestinationsNavHost(
                            navGraph = NavGraphs.root,
                            navController = navController
                        )
                    }
                }
            }
        }
    }
}

@Destination<RootGraph>(start = true)
@Composable
fun HomeScreen(
    navigator: DestinationsNavigator, modifier: Modifier = Modifier
) {

    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
        modifier = modifier.fillMaxSize()
    ) {
        Button(onClick = {
            navigator.navigate(DestinationScreenDestination)
        }) {
            Text(text = "通常画面遷移")
        }

        Button(onClick = {
            navigator.navigate(DestinationBottomScreenDestination)
        }) {
            Text(text = "ボトムシートを展開")
        }
    }
}

@Destination<RootGraph>()
@Composable
fun DestinationScreen(navigator: DestinationsNavigator, modifier: Modifier = Modifier) {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
        modifier = modifier.fillMaxSize()
    ) {
        Button(onClick = {
            navigator.navigateUp()
        }) {
            Text(text = "Back Home")
        }
    }
}


@Destination<RootGraph>(style = DestinationStyleBottomSheet::class)
@Composable
fun DestinationBottomScreen(navigator: DestinationsNavigator, modifier: Modifier = Modifier) {
    Column(
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center,
        modifier = modifier.fillMaxWidth().padding(vertical = 10.dp)
    ) {
        Button(onClick = {
            navigator.navigateUp()
        }) {
            Text(text = "Close BottomSheet")
        }
    }
}


@Preview(showBackground = true, showSystemUi = true)
@Composable
fun GreetingPreview() {
    NavhostExampleTheme {}
}

動作

まとめ

ComposeDestinationを使うことで、今まで冗長になっていた画面遷移の記述をより簡潔に書くことができるようになりました。

参考リンク

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です