0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Navigation Composeの型安全なstartDestination

Posted at

はじめに

2024年5月1日にリリースされたNavigationのバージョン2.8.0-alpha08から、型安全なobjectでNavHostのdestinationを指定することができるようになりました。
これに伴い、NavHostに渡すstartDestinationもobjectで指定するようになりました。

公式のサンプルコードを見た際に、startDestinationの指定の仕方が気になったので調べてみました。

サンプルコード引用
  // Define a home destination that doesn't take any arguments
  @Serializable
  object Home

  // Define a profile destination that takes an ID
  @Serializable
  data class Profile(val id: String)

  // Now define your NavHost using type safe objects
  NavHost(navController, startDestination = Home) {  // HomeもProfileも受け取れるなら、あらゆる型を受け取れてしまうのでは?
      composable<Home> {
          HomeScreen(onNavigateToProfile = { id ->
              navController.navigate(Profile(id))
          })
      }
      composable<Profile> { backStackEntry ->
          val profile: Profile = backStackEntry.toRoute()
          ProfileScreen(profile)
      }
  }

新しく追加されたNavHostのシグネチャ

実際にどう変わったのか、まずはシグネチャで比較してみます。

旧来のNavHostのシグネチャ
@Composable
public fun NavHost(
    navController: NavHostController,
    startDestination: String,
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    route: String? = null,
    // トランジション関連は省略
    builder: NavGraphBuilder.() -> Unit
)
2.8.0-alpha08以降で追加されたシグネチャ
@Composable
public fun NavHost(
    navController: NavHostController,
    startDestination: Any,                         
    modifier: Modifier = Modifier,
    contentAlignment: Alignment = Alignment.TopStart,
    route: KClass<*>? = null,
    typeMap: Map<KType, @JvmSuppressWildcards NavType<*>> = emptyMap(),
    // トランジション関連は省略
    builder: NavGraphBuilder.() -> Unit
)

比較してみると、startDestinationがStringからAnyに変わっているのが分かります。

やはり想像した通り、startDestinationにはあらゆる型を指定できてしまうようです。

startDestinationがAnyであることへの不安

startDestinationの型がAnyだからといって、誤った値が設定される可能性は低いですし、万が一誤った値が入ったとしても、アプリを起動すればすぐに実行時エラーで検出できます。そのため、過度に心配する必要はないかもしれません。

それでも、型安全を確保するためには、できるだけコンパイル時に誤った値を排除したいものです。

解決策

startDestinationの型がないならstartDestination用のインターフェースを用意すればいいじゃない。
ということで専用のインターフェースを用意し、startDestinationには特定の型の値しか入らないことを保証できるようにしました。

interface StartDestination

// startDestinationの判定処理が完了するまでの間に表示する画面
@Serializable
private object Loading : StartDestination

// アプリの使い方を説明する画面
@Serializable
private object Onboarding : StartDestination

// メイン画面
@Serializable
private object Main : StartDestination

@Composable
fun MTransNavHost(
    modifier: Modifier = Modifier,
    navController: NavHostController,
) {
    // 開始画面を表す状態
    val startDestinationState: MutableState<StartDestination> = remember {
        mutableStateOf(Loading)
    }

    // startDestination判定
    LaunchedEffect(Unit) {
        startDestination(
            startDestinationState = startDestinationState,
        )
    }
    
    NavHost(
        navController = navController, 
        startDestination = startDestinationState.value
    ) {
        composable<Loading> {
            // 初回起動判定がすぐに完了する場合は、あえて何も表示しない方がチラつきがなくて良い。
        }
        composable<Onboarding> {
            OnboardingScreen()
        }
        composable<Main> {
            MainScreen()
        }
    }
}

// 非同期で初回起動を判定するロジック
private suspend fun startDestination(
    startDestinationState: MutableState<StartDestination>,
) {
    val isFirstLaunch = // DataStoreからフラグを取得する等、初回起動を判定するロジック

    startDestinationState.value = if (isFirstLaunch) {
        Onboarding
    } else {
        Main
    }
}

説明の都合上、destinationを表すobjectをすべて同じ場所に記述していますが、実務ではそれぞれの画面Composableと同じファイル内で管理したほうがよいでしょう。

まとめ

上記の実装を行うことでstartDestinationの型を固定することができました。
また、副次的なメリットですが、開始画面となるobjectは必ずStartDestination型を実装しているので、どの画面がstartDestinationになりうるか?というのを明示的にコードで表せるようになっています。

参考

0
0
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
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?