Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Road to Single Activity Uncovered

yurihondo
October 11, 2024

Road to Single Activity Uncovered

DroidKaigiの「Road to Single Activity」のセッションで時間の関係でカバーできなかったトピックについての発表です。
https://2024.droidkaigi.jp/timetable/690950/

具体的には以下の内容を扱います。
- Typesafe navigation arguments
- Complex Navigating

yurihondo

October 11, 2024
Tweet

More Decks by yurihondo

Other Decks in Technology

Transcript

  1. Agenda • Recap of Road to Single Activity • Typesafe

    navigation arguments • Complex Navigating
  2. Navigation ~2.7.x fun NavController.navigateToHogeRoute( arg: String, navOptions: NavOptions? = null

    ) { this.navigate( route = "hoge_route/$arg", navOptions = navOptions ) }
  3. Navigation ~2.7.x fun NavController.navigateToHogeRoute( arg: String, navOptions: NavOptions? = null

    ) { this.navigate( route = "hoge_route/$arg", navOptions = navOptions ) } ←型を定義
  4. Navigation ~2.7.x composable( ... ) { entry -> HogeRoute( arg

    = entry.arguments?.getString("hoge_arg")!!, ... ) }
  5. Navigation ~2.7.x composable( ... ) { entry -> HogeRoute( arg

    = entry.arguments?.getString("hoge_arg")!!, ... ) } ←型を指定
  6. Navigation 2.8.0~ @Serializable data class HogeDestination( @SerialName("arg") val arg: String,

    ) navController.navigate(HogeDestination("hoge")) Routeを定義
  7. Navigation 2.8.0~ @Serializable data class HogeDestination( @SerialName("arg") val arg: String,

    ) navController.navigate(HogeDestination("hoge")) ←引数を定義
  8. Navigation 2.8.0~ @Serializable data class HogeDestination( @SerialName("arg") val arg: String,

    ) navController.navigate(HogeDestination("hoge"))←Newして渡す
  9. Navigation 2.8.0~ composable<HogeDestination>( … ) { backStackEntry -> HogeRoute( arg

    = backStackEntry.toRoute<HogeDestination>().arg, … ) }
  10. Navigation 2.8.0~ composable<HogeDestination>( … ) { backStackEntry -> HogeRoute( arg

    = backStackEntry.toRoute<HogeDestination>().arg, … ) } ↑型を指定
  11. Navigation 2.8.0~ composable<HogeDestination>( … ) { backStackEntry -> HogeRoute( arg

    = backStackEntry.toRoute<HogeDestination>().arg, … ) } ↑型を指定 記述を 間違ったら...
  12. Navigation 2.8.0~ composable<HogeDestination>( … ) { backStackEntry -> HogeRoute( arg

    = backStackEntry.toRoute(), … ) } ← Runtimeでクラッシュ💥
  13. Navigation 2.8.0~ composable<HogeDestination>( … ) { backStackEntry -> HogeRoute( arg

    = backStackEntry.toRoute(), … ) } ← Runtimeでクラッシュ💥 引数 受け取りも 型安全に実装したい
  14. Compose Destinations @Destination<HogeGraph> @Composable internal fun HogeRoute( arg: String, …

    ) {...} destinationsNavigator.navigate(HogeRouteDestination("hoge"))
  15. Compose Destinations @Destination<HogeGraph> @Composable internal fun HogeRoute( arg: String, …

    ) {...} destinationsNavigator.navigate(HogeRouteDestination("hoge")) ← 引数を設定
  16. Compose Destinations @Destination<HogeGraph> @Composable internal fun HogeRoute( arg: String, …

    ) {...} destinationsNavigator.navigate(HogeRouteDestination("hoge")) 引数を渡すため Routeオブジェクトが 自動生成される ↓
  17. Compose Destinations @Destination<HogeGraph> @Composable internal fun HogeRoute( arg: String, …

    ) {...} destinationsNavigator.navigate(HogeRouteDestination("hoge")) ロジック不要 型安全に受け取れる
  18. Compose Destinations @Destination<HogeGraph> @Composable internal fun HogeRoute( arg: String, …

    ) {...} destinationsNavigator.navigate(HogeRouteDestination("hoge")) 一体どうやって... ↓
  19. Compose Destinations public data object HogeRouteDestination : BaseRoute(), TypedDestinationSpec<HogeRouteDestinationNavArgs> {

    public operator fun invoke( arg: String, ): Direction { return Direction( route = "$baseRoute" + "/${stringNavType.serializeValue("arg", arg)}" ) } @Composable override fun DestinationScope<HogeRouteDestinationNavArgs>.Content() { val dependencyContainer = buildDependencies() val (arg) = navArgs HogeRoute( arg = arg, … ) } }
  20. Compose Destinations public data object HogeRouteDestination : BaseRoute(), TypedDestinationSpec<HogeRouteDestinationNavArgs> {

    public operator fun invoke( arg: String, ): Direction { return Direction( route = "$baseRoute" + "/${stringNavType.serializeValue("arg", arg)}" ) } @Composable override fun DestinationScope<HogeRouteDestinationNavArgs>.Content() { val dependencyContainer = buildDependencies() val (arg) = navArgs HogeRoute( arg = arg, … ) } } ← 👀
  21. Compose Destinations public data object HogeRouteDestination : …{ public operator

    fun invoke( arg: String, ): Direction { return Direction( route = "$baseRoute" + "/${stringNavType.serializeValue("arg", arg)}" ) } } ↓引数あり URLを自動生成
  22. Compose Destinations public data object HogeRouteDestination : BaseRoute(), TypedDestinationSpec<HogeRouteDestinationNavArgs> {

    public operator fun invoke( arg: String, ): Direction { return Direction( route = "$baseRoute" + "/${stringNavType.serializeValue("arg", arg)}" ) } @Composable override fun DestinationScope<HogeRouteDestinationNavArgs>.Content() { val dependencyContainer = buildDependencies() val (arg) = navArgs HogeRoute( arg = arg, … ) } } ← 👁 👁
  23. Compose Destinations @Composable override fun DestinationScope<HogeRouteDestinationNavArgs>.Content() { val dependencyContainer =

    buildDependencies() val (arg) = navArgs HogeRoute( arg = arg, … ) } ←引数を取り出してセット
  24. Compose Destinations @Composable override fun DestinationScope<HogeRouteDestinationNavArgs>.Content() { val dependencyContainer =

    buildDependencies() val (arg) = navArgs HogeRoute( arg = arg, … ) } ←引数を取り出してセット 自動生成で型安全な 引数 受け取りを実現
  25. Typesafe navigation arguments アプリ規模 画面数 ナビゲーションの複雑さ Vanilla Navigation Compose Compose

    Destinations 小規模アプリ 10画面未満 シンプル ◎ 最適 △ オーバースペック気味 中規模アプリ 10〜30画面 中程度 〇 使いやすい 〇 開発効率が向上 大規模アプリ 31画面以上 複雑 △ 管理が大変になる可能性 ◎ 型安全・管理が容易
  26. data class BridgeState( val hasBridged: Boolean = false, val request:

    Request? = null ) : Serializable { @Parcelize sealed interface Request : Parcelable @Parcelize data class ShowHoge( val arg: String, ) : Request } private var state = BridgeState()
  27. data class BridgeState( val hasBridged: Boolean = false, val request:

    Request? = null ) : Serializable { @Parcelize sealed interface Request : Parcelable @Parcelize data class ShowHoge( val arg: String, ) : Request } private var state = BridgeState() ← 遷移したかどうか 状態
  28. data class BridgeState( val hasBridged: Boolean = false, val request:

    Request? = null ) : Serializable { @Parcelize sealed interface Request : Parcelable @Parcelize data class ShowHoge( val arg: String, ) : Request } private var state = BridgeState() ← 遷移リクエスト ← 画面引数など
  29. class BridgeActivity : AppCompatActivity() { companion object { private const

    val KEY_REQUEST = "key_request" fun createIntentForHoge( activityContext: Context, arg: String, ): Intent { return Intent(activityContext, BridgeActivity::class.java).apply { putExtra(KEY_REQUEST, ShowHoge(arg)) } } } }
  30. class BridgeActivity : AppCompatActivity() { companion object { private const

    val KEY_REQUEST = "key_request" fun createIntentForHoge( activityContext: Context, arg: String, ): Intent { return Intent(activityContext, BridgeActivity::class.java).apply { putExtra(KEY_REQUEST, ShowHoge(arg)) } } } }
  31. class BridgeActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    if (savedInstanceState == null) { state = BridgeState( hasBridged = false, request = IntentCompat.getParcelableExtra(intent, ...) ) } setContent { Surface(…) { MainNavHost( startDirection = NavGraphs.Bridge … ) } } } }
  32. class BridgeActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    if (savedInstanceState == null) { state = BridgeState( hasBridged = false, request = IntentCompat.getParcelableExtra(intent, ...) ) } setContent { Surface(…) { MainNavHost( startDirection = NavGraphs.Bridge … ) } } } } ↑ Intentからstateを生成
  33. class BridgeActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    if (savedInstanceState == null) { state = BridgeState( hasBridged = false, request = IntentCompat.getParcelableExtra(intent, ...) ) } setContent { Surface(…) { MainNavHost( startDirection = NavGraphs.Bridge … ) } } } } ←MainGraphを呼び出す
  34. class BridgeActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) {

    if (savedInstanceState == null) { state = BridgeState( hasBridged = false, request = IntentCompat.getParcelableExtra(intent, ...) ) } setContent { Surface(…) { MainNavHost( startDirection = NavGraphs.Bridge … ) } } } } ↑BridgeRouteを渡し、 NavHost StartDestinationに設定する
  35. @Destination<BridgeNavGraph>(start = true) @Composable internal fun BridgeRoute( mainNavigator: MainNavigator, )

    { if (state.hasBridged) { LocalContext.current.findActivity().finish() return } when (val request = state.request) { is ShowHoge -> { mainNavigator.navigateToHoge( arg = request.arg, ) } else -> LocalContext.current.findActivity().finish() } state = state.copy(hasBridged = true) }
  36. @Destination<BridgeNavGraph>(start = true) @Composable internal fun BridgeRoute( mainNavigator: MainNavigator, )

    { if (state.hasBridged) { LocalContext.current.findActivity().finish() return } when (val request = state.request) { is ShowHoge -> { mainNavigator.navigateToHoge( arg = request.arg, ) } else -> LocalContext.current.findActivity().finish() } state = state.copy(hasBridged = true) } ←RequestみてHogeScreenを起動
  37. @Destination<BridgeNavGraph>(start = true) @Composable internal fun BridgeRoute( mainNavigator: MainNavigator, )

    { if (state.hasBridged) { LocalContext.current.findActivity().finish() return } when (val request = state.request) { is ShowHoge -> { mainNavigator.navigateToHoge( arg = request.arg, ) } else -> LocalContext.current.findActivity().finish() } state = state.copy(hasBridged = true) } ←遷移させたらhasBridgedをtrueに
  38. @Destination<BridgeNavGraph>(start = true) @Composable internal fun BridgeRoute( mainNavigator: MainNavigator, )

    { if (state.hasBridged) { LocalContext.current.findActivity().finish() return } when (val request = state.request) { is ShowHoge -> { mainNavigator.navigateToHoge( arg = request.arg, ) } else -> LocalContext.current.findActivity().finish() } state = state.copy(hasBridged = true) } ←BridgeRouteに戻ってきたら BridgeActivity自体を閉じる