Slide 1

Slide 1 text

Road to Single Activity Uncovered

Slide 2

Slide 2 text

About me yurihondo 2個目のサブ冷凍庫が欲しい X : yurihondo @yuyuyuyuyuri

Slide 3

Slide 3 text

Agenda ● Recap of Road to Single Activity ● Typesafe navigation arguments ● Complex Navigating

Slide 4

Slide 4 text

Recap of Road to Single Activity

Slide 5

Slide 5 text

https://2024.droidkaigi.jp/timetable/690950/

Slide 6

Slide 6 text

Activity Activity Activity Framgent Fragment Transaction Before

Slide 7

Slide 7 text

Single Activity NavGraph Navigation Screen (Composable) Activity After

Slide 8

Slide 8 text

Single Activity NavGraph Navigation Screen (Composable) Activity After ● メンテナンス性⤴ ● 拡張性⤴

Slide 9

Slide 9 text

Samples Navigation 2.7.7 https://github.com/yurihondo/screentransitionsample Navigation 2.8.x https://github.com/yurihondo/screentransitionsample/tree/feature/navigation_v2.8.x Compose Destinations https://github.com/yurihondo/screentransitionsample/tree/feature/compose_destinations

Slide 10

Slide 10 text

Typesafe navigation arguments

Slide 11

Slide 11 text

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

Slide 12

Slide 12 text

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

Slide 13

Slide 13 text

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

Slide 14

Slide 14 text

Navigation ~2.7.x composable( ... ) { entry -> HogeRoute( arg = entry.arguments?.getString("hoge_arg")!!, ... ) } ←型を指定

Slide 15

Slide 15 text

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

Slide 16

Slide 16 text

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

Slide 17

Slide 17 text

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

Slide 18

Slide 18 text

Navigation 2.8.0~ @Serializable data class HogeDestination( @SerialName("arg") val arg: String, ) navController.navigate(HogeDestination("hoge"))←Newして渡す

Slide 19

Slide 19 text

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

Slide 20

Slide 20 text

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

Slide 21

Slide 21 text

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

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

Navigation 2.8.0~ composable( … ) { backStackEntry -> HogeRoute( arg = backStackEntry.toRoute(), … ) } ← Runtimeでクラッシュ💥 引数 受け取りも 型安全に実装したい

Slide 24

Slide 24 text

Compose Destinations https://github.com/raamcosta/compose-destinations

Slide 25

Slide 25 text

Compose Destinations Navigation ボイラープレートを自動生成してくれるKSPライブラリ NavGraphを自動生成してくれたり、色々書かずに済む Destinationへ 引数受け渡しだけで なく、受け取りも型安全にできる

Slide 26

Slide 26 text

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

Slide 27

Slide 27 text

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

Slide 28

Slide 28 text

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

Slide 29

Slide 29 text

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

Slide 30

Slide 30 text

Compose Destinations @Destination @Composable internal fun HogeRoute( arg: String, … ) {...} destinationsNavigator.navigate(HogeRouteDestination("hoge")) 一体どうやって... ↓

Slide 31

Slide 31 text

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

Slide 32

Slide 32 text

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

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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

Slide 36

Slide 36 text

Compose Destinations @Composable override fun DestinationScope.Content() { val dependencyContainer = buildDependencies() val (arg) = navArgs HogeRoute( arg = arg, … ) } ←引数を取り出してセット 自動生成で型安全な 引数 受け取りを実現

Slide 37

Slide 37 text

Typesafe navigation arguments アプリ規模 画面数 ナビゲーションの複雑さ Vanilla Navigation Compose Compose Destinations 小規模アプリ 10画面未満 シンプル ◎ 最適 △ オーバースペック気味 中規模アプリ 10〜30画面 中程度 〇 使いやすい 〇 開発効率が向上 大規模アプリ 31画面以上 複雑 △ 管理が大変になる可能性 ◎ 型安全・管理が容易

Slide 38

Slide 38 text

Complex Navigating

Slide 39

Slide 39 text

Activities Main Activity Common Screen (Activity) Activity Activity Activity

Slide 40

Slide 40 text

MainActivity CommonScreen (Activity) MainGraph NestedGraph Graph Composable

Slide 41

Slide 41 text

MainActivity CommonScreen (Activity) MainGraph NestedGraph Graph Composable

Slide 42

Slide 42 text

MainActivity CommonScreen (Activity) MainGraph NestedGraph Graph Composable もうちょっと 複雑なケースを考える

Slide 43

Slide 43 text

MainActivity MainGraph HogeActivity FugaActivity StartActivity

Slide 44

Slide 44 text

MainActivity MainGraph HogeActivity FugaActivity StartActivity

Slide 45

Slide 45 text

MainActivity MainGraph FugaActivity HogeScreen (Composable)

Slide 46

Slide 46 text

MainActivity Graph FugaActivity HogeScreen (Composable) HogeScreenをFugaActivity Graphにも所属させれ 良いが...

Slide 47

Slide 47 text

HogeScreen PiyoScreen TotoScreen TeteScreen navigate navigate navigate

Slide 48

Slide 48 text

依存する画面やGraphなどなど全 て組み込む ツライ

Slide 49

Slide 49 text

MainActivity Graphを 共通利用すれ いい で !!

Slide 50

Slide 50 text

MainActivity MainGraph FugaActivity HogeScreen (Composable) BridgeActivity StartActivity

Slide 51

Slide 51 text

BridgeActivity ● 集約先 MainActivityに実装されている画面を別 Activityから開くため 橋渡し ● Activity等がMainActivityへ統合されれ 自然と 不要になる で、一時的な実装として有効

Slide 52

Slide 52 text

BridgeActivity MainGraph BridgeRoute BridgeActivity HogeScreen navigate Inten t

Slide 53

Slide 53 text

BridgeActivity MainGraph BridgeRoute BridgeActivity HogeScreen navigate Inten t

Slide 54

Slide 54 text

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()

Slide 55

Slide 55 text

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() ← 遷移したかどうか 状態

Slide 56

Slide 56 text

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() ← 遷移リクエスト ← 画面引数など

Slide 57

Slide 57 text

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)) } } } }

Slide 58

Slide 58 text

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)) } } } }

Slide 59

Slide 59 text

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 … ) } } } }

Slide 60

Slide 60 text

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を生成

Slide 61

Slide 61 text

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を呼び出す

Slide 62

Slide 62 text

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に設定する

Slide 63

Slide 63 text

BridgeActivity MainGraph BridgeRoute BridgeActivity HogeScreen navigate Inten t

Slide 64

Slide 64 text

BridgeActivity MainGraph BridgeRoute BridgeActivity HogeScreen navigate Inten t

Slide 65

Slide 65 text

@Destination(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) }

Slide 66

Slide 66 text

@Destination(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を起動

Slide 67

Slide 67 text

@Destination(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に

Slide 68

Slide 68 text

BridgeActivity MainGraph BridgeRoute BridgeActivity HogeScreen navigate Inten t

Slide 69

Slide 69 text

BridgeActivity MainGraph BridgeRoute BridgeActivity HogeScreen back Inten t

Slide 70

Slide 70 text

@Destination(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自体を閉じる

Slide 71

Slide 71 text

MainActivity MainGraph FugaActivity HogeScreen (Composable) BridgeActivity StartActivity

Slide 72

Slide 72 text

MainActivity MainGraph FugaActivity HogeScreen (Composable) BridgeActivity StartActivity 画面を開く時 アニメーションに Navigation Transitionアニメーショ ンが適用されない...

Slide 73

Slide 73 text

Complex Navigating ● BridgeActivityを導入すること 、複数Activityが ある場合に有効 ● 副作用もある で、あくまで一時的な対応にした 方が良い

Slide 74

Slide 74 text

Wrap up

Slide 75

Slide 75 text

Wrap up ● Navigation便利だけど、状況によって複雑な実装になること ある。 ○ シチュエーションに合わせて楽に使える方法を検討・調整 して行く が吉。 ● Compose Destinationsを検討してみる あり

Slide 76

Slide 76 text

Thanks