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

Navigation 2 を 3 に移行する(予定)ためにやったこと

Avatar for yokomii yokomii
September 12, 2025

Navigation 2 を 3 に移行する(予定)ためにやったこと

Avatar for yokomii

yokomii

September 12, 2025
Tweet

More Decks by yokomii

Other Decks in Programming

Transcript

  1. X

  2.  /BW #BDLTUBDL όοΫελοΫ͸zঢ়ଶzΛ಺แ͢ ΔϦετ val backStack : List<Any> =

    remember { mutableListOf<Any>(FoodList) } data object FoodList data class FoodDetail(val id: String)
  3.  /BW #BDLTUBDL όοΫελοΫ͕؅ཧ͢Δঢ়ଶΛ ΩʔͱݺͿ "OZܕ val backStack : List<Any>

    = remember { mutableListOf<Any>(FoodList) } data object FoodList data class FoodDetail(val id: String)
  4.  /BW #BDLTUBDL ΩʔʹҾ਺Λ࣋ͨͤΔ͜ͱ͕Ͱ͖ Δ val backStack : List<Any> =

    remember { mutableListOf<Any>(FoodList) } data object FoodList data class FoodDetail(val id: String)
  5.  /BW #BDLTUBDL όοΫελοΫʹ͸ඞͣϧʔτ ΩʔΛઃఆ͢Δ ۭʹͳΔ͜ͱ͸૝ఆ֎ val backStack : List<Any>

    = remember { mutableListOf<Any>(FoodList) } data object FoodList data class FoodDetail(val id: String)
  6.  /BW #BDLTUBDL ໨తڍಈ /BWJHBUJPO$PNQPTF /BW ը໘ਐΉʢ௨ৗͷOBWJHBUFʣ OBW$POUSPMMFSOBWJHBUF lGPPE@MJTUJE GPPE@EFUBJMl

    CBDL4UBDLBEE 'PPE%FUBJM lJEz ը໘໭Δ OBW$POUSPMMFSQPQ#BDL4UBDL CBDL4UBDLSFNPWF-BTU0S/VMM ೚ҙ஍఺·Ͱ໭ΔʢJODMVTJWFGBMTFʣ OBW$POUSPMMFSQPQ#BDL4UBDL GPPE@MJTU  JODMVTJWFGBMTF CBDL4UBDLESPQ-BTU8IJMF\JU'PPE-JTU^ ೚ҙ஍఺·Ͱ໭ΔʢJODMVTJWFUSVFʣ OBW$POUSPMMFSQPQ#BDL4UBDL lGPPE@MJTU  JODMVTJWFUSVF CBDL4UBDLESPQ-BTU8IJMF\JU'PPE-JTU^ CBDL4UBDLESPQ-BTU  MBVODI4JOHMF5PQʢτοϓʹಉ͡ϧʔτ͕͋Δͳ Βੵ·ͳ͍ʣ OBW$POUSPMMFSOBWJHBUF lGPPE@MJTUz \MBVODI4JOHMF5PQUSVF^ JG CBDL4UBDLMBTU0S/VMM 'PPE-JTU  CBDL4UBDLBEE 'PPE-JTU
  7.  /BW%JTQMBZ /BW%JTQMBZ όοΫελοΫͷঢ়ଶΛݩʹɺը ໘Λඳը͢Δίϯϙʔωϯτ DG/BW)PTUʢ/BWJHBUJPO $PNQPTFʣ NavDisplay( backStack =

    backStack, onBack = { backStack.removeLastOrNull() }, entryProvider = { key -> when (key) { is FoodList -> NavEntry(key) { FoodListScreen() } is FoodDetail -> NavEntry(key) { FoodDetailScreen(id = key.id) } else -> error("Unknown route: $key") } } )
  8.  /BW%JTQMBZ /BW%JTQMBZCBDL4UBDL όοΫελοΫΛઃఆ NavDisplay( backStack = backStack, onBack =

    { backStack.removeLastOrNull() }, entryProvider = { key -> when (key) { is FoodList -> NavEntry(key) { FoodListScreen() } is FoodDetail -> NavEntry(key) { FoodDetailScreen(id = key.id) } else -> error("Unknown route: $key") } } )
  9.  /BW%JTQMBZ /BW%JTQMBZFOUSZ1SPWJEFS ΩʔʹରԠ͢Δ/BW&OUSZΛฦ͢ ίʔϧόοΫ  DG/BW(SBQI#VJMEFSDPNQPTBCMF ʢ/BWJHBUJPO$PNQPTFʣ NavDisplay( backStack

    = backStack, onBack = { backStack.removeLastOrNull() }, entryProvider = { key -> when (key) { is FoodList -> NavEntry(key) { FoodListScreen() } is FoodDetail -> NavEntry(key) { FoodDetailScreen(id = key.id) } else -> error("Unknown route: $key") } } )
  10.  /BW%JTQMBZ /BW%JTQMBZFOUSZ1SPWJEFS FOUSZ1SPWJEFS%4-Ͱهड़ͷলུ NavDisplay( … entryProvider = entryProvider {

    entry<FoodList> { FoodListScreen() } entry<FoodDetail>{ key -> FoodDetailScreen(id = key.id) } } )
  11.  /BW&OUSZ /BW&OUSZ Ωʔͱඳը͢ΔίϯςϯπΛରԠ ͚ͮͨ΋ͷ  DG/BW#BDL4UBDL&OUSZ ʢ/BWJHBUJPO$PNQPTFʣ public open

    class NavEntry<T : Any>( private val key: T, public val contentKey: Any = defaultContentKey(key), public open val metadata: Map<String, Any> = emptyMap(), private val content: @Composable (T) -> Unit, ) { ... } BOESPJEYOBWJHBUJPOSVOUJNF/BW&OUSZLU
  12.  /BW&OUSZ /BW&OUSZDPOUFOU,FZ ΤϯτϦʔͷࣝผࢠ σϑΥϧτ͸LFZUP4USJOH public open class NavEntry<T :

    Any>( private val key: T, public val contentKey: Any = defaultContentKey(key), public open val metadata: Map<String, Any> = emptyMap(), private val content: @Composable (T) -> Unit, ) { ... } internal fun defaultContentKey(key: Any): Any = key.toString() BOESPJEYOBWJHBUJPOSVOUJNF/BW&OUSZLU
  13.  /BW&OUSZ /BW&OUSZNFUBEBUB ΤϯτϦʔʹ௥Ճ৘ใΛ෇༩͢Δ ͨΊͷ,FZWBMVFϚοϓ public open class NavEntry<T :

    Any>( private val key: T, public val contentKey: Any = defaultContentKey(key), public open val metadata: Map<String, Any> = emptyMap(), private val content: @Composable (T) -> Unit, ) { ... } BOESPJEYOBWJHBUJPOSVOUJNF/BW&OUSZLU
  14.  4DFOF4DFOF4USBUFHZ 4DFOF ը໘্ʹΤϯτϦʔΛͲͷΑ͏ ʹඳը͢Δ͔Λܾఆ͠ɺඳըॲཧ Λఏڙ public interface Scene<T :

    Any> { // γʔϯͷҰҙੑΛࣔ͢Ωʔ val key: Any // γʔϯ͕ඳը͢ΔΤϯτϦʔ܈ val entries: List<NavEntry<T>> // γʔϯͷഎ໘ʹඳը͞ΕΔΤϯτϦʔ܈ val previousEntries: List<NavEntry<T>> // ඳըॲཧ val content: @Composable () -> Unit } BOESPJEYOBWJHBUJPOVJ4DFOFLU
  15.  4DFOF4DFOF4USBUFHZ 4DFOF γʔϯʹΑͬͯ͸ը໘্ʹෳ਺ ͷΤϯτϦʔΛඳը͢Δ͜ͱ͕͋ Δ /BWJHBUJPOʹ͓͍ͯ͸ɺΤϯ τϦʔʺը໘ public interface

    Scene<T : Any> { // ෳ਺ඳը͢ΔՄೳੑ͕͋ΔͷͰϦετ val entries: List<NavEntry<T>> ... } BOESPJEYOBWJHBUJPOVJ4DFOFLU
  16.  4DFOF4DFOF4USBUFHZ 4DFOF4USBUFHZ /BW%JTQMBZ্ͷΤϯτϦʔ܈Λ Ͳͷ4DFOFͰඳը͢Δ͔Λܭࢉ ͢Δ public fun interface SceneStrategy<T

    : Any> { @Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit ): Scene<T>? } BOESPJEYOBWJHBUJPOVJ4DFOF4USBUFHZLU
  17.  4DFOF4DFOF4USBUFHZ DBMDVMBUF4DFOF PO#BDL /BW%JTQMBZʹઃఆͨ͠ίʔϧόο ΫͷͦΕ ෳ਺ΤϯτϦʔΛඳը࣌ɺҰ౓ʹ ෳ਺ΤϯτϦʔΛόοΫ͢Δ͜ͱ ͕͋Δ ˠγʔϯ಺ͰίʔϧόοΫΛ࣮

    ߦ public fun interface SceneStrategy<T : Any> { @Composable public fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit ): Scene<T>? } BOESPJEYOBWJHBUJPOVJ4DFOF4USBUFHZLU
  18.  4DFOF4DFOF4USBUFHZ 4JOHMF1BOF4DFOF4USBUFHZ /BW%JTQMBZͷσϑΥϧτετϥ ςδʔ @Composable public fun <T :

    Any> NavDisplay( ɹɹɹɹ... sceneStrategy: SceneStrategy<T> = SinglePaneSceneStrategy() ... ) BOESPJEYOBWJHBUJPOVJ/BW%JTQMBZLU
  19.  4DFOF4DFOF4USBUFHZ 4JOHMF1BOF4DFOF4USBUFHZ 4JOHMF1BOFM4DFOFΛੜ੒ public class SinglePaneSceneStrategy<T : Any> :

    SceneStrategy<T> { @Composable override fun calculateScene( ... ): Scene<T> = SinglePaneScene( key = entries.last().contentKey, entry = entries.last(), previousEntries = entries.dropLast(1), ) } BOESPJEYOBWJHBUJPOVJ/BW%JTQMBZLU
  20.  4DFOF4DFOF4USBUFHZ 4JOHMF1BOF4DFOF /BW%JTQMBZͷσϑΥϧτͷγʔ ϯ ը໘্ʹΤϯτϦʔΛඳը͢Δ ͚ͩ internal data class

    SinglePaneScene<T : Any>( override val key: Any, val entry: NavEntry<T>, override val previousEntries: List<NavEntry<T>>, ) : Scene<T> { override val entries: List<NavEntry<T>> = listOf(entry) // ༩͑ΒΕͨ1ΤϯτϦʔΛͦͷ··ඳը͍ͯ͠Δ override val content: @Composable () -> Unit = { entry.Content() } } BOESPJEYOBWJHBUJPOVJ4JOHMF1BOFM4DFOFLU
  21.  4DFOF4DFOF4USBUFHZ ϦετৄࡉϨΠΞ΢τ࣮૷ྫ -JTU%FUBJM4DFOF4USBUFHZͰ͸ɺ όοΫελοΫͷ࠷ޙͷΤϯτ Ϧʔʢݱࡏඳը͢΂͖ը໘ʣ͕ɺ γʔϯͷର৅Ͱ͋Δ͔Λ֬ೝ͢Δ @Composable override fun

    calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>? { val lastPaneMetadata = getPaneMetadata(entries.last()) ?: return null ... } private fun <T : Any> getPaneMetadata(entry: NavEntry<T>): PaneMetadata? = entry.metadata[ListDetailRoleKey] as? PaneMetadata BOESPJEYDPNQPTFNBUFSJBMBEBQUJWFOBWJHBUJPO-JTU%FUBJM4DFOF4USBUFHZLU
  22.  4DFOF4DFOF4USBUFHZ ϦετৄࡉϨΠΞ΢τ࣮૷ྫ ର৅ΤϯτϦʔͰ͋Δ͔ͷ֬ೝख ஈͱͯ͠ɺΤϯτϦʔͷϝλσʔ λΛνΣοΫ͢Δ @Composable override fun calculateScene(

    entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit, ): Scene<T>? { val lastPaneMetadata = getPaneMetadata(entries.last()) ?: return null ... } private fun <T : Any> getPaneMetadata(entry: NavEntry<T>): PaneMetadata? = entry.metadata[ListDetailRoleKey] as? PaneMetadata BOESPJEYDPNQPTFNBUFSJBMBEBQUJWFOBWJHBUJPO-JTU%FUBJM4DFOF4USBUFHZLU
  23.  4DFOF4DFOF4USBUFHZ ϦετৄࡉϨΠΞ΢τ࣮૷ྫ ϝλσʔλ͸ΤϯτϦʔͷҾ਺ʹ ઃఆ NavDisplay( ... entryProvider = entryProvider

    { entry<FoodList>( metadata = ListDetailSceneStrategy.listPane() ) { ... } entry<FoodDetail>( metadata = ListDetailSceneStrategy.detailPane() ) { ... } } )
  24.  4DFOF4DFOF4USBUFHZ ϦετৄࡉϨΠΞ΢τ࣮૷ྫ ΤϯτϦʔ͕γʔϯͷର৅ͳΒɺ ޙଓͷର৅ΤϯτϦʔΛଋͶΔ @Composable override fun calculateScene( ...

    ): Scene<T>? { ... val scaffoldEntries = mutableListOf<NavEntry<T>>() var idx = entries.lastIndex while (idx >= 0) { val paneMetadata = getPaneMetadata(entry) ?: break ... scaffoldEntries.add(0, entries[i]) i-- } ... } BOESPJEYDPNQPTFNBUFSJBMBEBQUJWFOBWJHBUJPO-JTU%FUBJM4DFOF4USBUFHZLU
  25.  4DFOF4DFOF4USBUFHZ ϦετৄࡉϨΠΞ΢τ࣮૷ྫ ଋͶͨΤϯτϦʔΛҾ਺ʹɺ 5ISFF1BOF4DB ff PME4DFOFΛੜ ੒ PO#BDLίʔϧόοΫΛγʔ ϯʹ౉͢

    @Composable override fun calculateScene( ... ): Scene<T>? { ... val scaffoldEntries = mutableListOf<NavEntry<T>>() … val scene = ThreePaneScaffoldScene( onBack = onBack, scaffoldEntries = scaffoldEntries, ... ) ... } BOESPJEYDPNQPTFNBUFSJBMBEBQUJWFOBWJHBUJPO-JTU%FUBJM4DFOF4USBUFHZLU
  26.  0WFSMBZ4DFOF 0WFSMBZ4DFOF ଞͷγʔϯʹ෴͍ඃ͞ΔΑ͏ʹඳ ը͢Δγʔϯ public interface OverlayScene<T : Any>

    : Scene<T> { // ͜ͷγʔϯ͕෴͍ඃ͞ΔΤϯτϦʔ܈ public val overlaidEntries: List<NavEntry<T>> } BOESPJEYOBWJHBUJPOVJ0WFSMBZ4DFOFLU
  27.  0WFSMBZ4DFOF /BW%JTQMBZͰ͸௨ৗͷγʔϯΛ "OJNBUFE$POUFOU಺Ͱඳըޙɺ 0WFSMBZ4DFOFΛඳը͢Δ @Composable public fun <T :

    Any> NavDisplay(...) { ... transition.AnimatedContent(...) { ... targetScene.content() ... } ... overlayScenes.fastForEachReversed { overlayScene -> ... overlayScene.content.invoke() ... } ... } BOESPJEYOBWJHBUJPOVJ/BW%JTQMBZLU
  28.  %JBMPH4DFOF%JBMPH4DFOF4USBUFHZ μΠΞϩάͱͯ͠ඳը͢ΔΤϯτ Ϧʔʹ %JBMPH4DFOF4USBUFHZEJBMPHϝ λσʔλΛઃఆ %JBMPH1SPQFSUJFT͸ɺγʔϯͰ ඳը͢Δ%JBMPHίϯϙʔωϯτ ʹద༻͞ΕΔ NavDisplay(

    entryProvider = entryProvider { ... entry<FoodPopUp>( metadata = DialogSceneStrategy.dialog( dialogProperties = DialogProperties( dismissOnClickOutside = false ) ) ) { ... } } ... )
  29.  %JBMPH4DFOF%JBMPH4DFOF4USBUFHZ %JBMPH4DFOF4USBUFHZͰɺඳըର ৅ͷΤϯτϦʔʹϝλσʔλ͕ઃ ఆ͞Ε͍ͯΔ͔Λ֬ೝ ϝλσʔλ͕ઃఆ͞Ε͍ͯͨΒ %JBMPH1SPQFSUJFTΛऔಘͯ͠γʔ ϯΛ࡞੒ public class

    DialogSceneStrategy<T : Any>() : SceneStrategy<T> { @Composable public override fun calculateScene(...): Scene<T>? { val lastEntry = entries.lastOrNull() val dialogProperties = lastEntry?.metadata?.get(DIALOG_KEY) as? DialogProperties return dialogProperties?.let { properties -> DialogScene( ... ) } } } BOESPJEYOBWJHBUJPOVJ%JBMPH4DFOFLU
  30.  %JBMPH4DFOF%JBMPH4DFOF4USBUFHZ %JBMPH4DFOFͰɺΤϯτϦʔͷί ϯςϯπΛ%JBMPHίϯϙʔωϯ τ্Ͱඳը import androidx.compose.ui.window.Dialog internal class DialogScene<T

    : Any>( private val entry: NavEntry<T>, private val dialogProperties: DialogProperties, private val onBack: (count: Int) -> Unit, ... ) : OverlayScene<T> { ... override val content: @Composable (() -> Unit) = { Dialog( onDismissRequest = { onBack(1) }, properties = dialogProperties ) { entry.Content() } } } BOESPJEYOBWJHBUJPOVJ%JBMPH4DFOFLU
  31.  %JBMPH4DFOF%JBMPH4DFOF4USBUFHZ ϝλσʔλ͔Βऔಘͨ͠ %JBMPH1SPQFSUJFTͱɺPO#BDL ίʔϧόοΫΛ%JBMPHʹઃఆ internal class DialogScene<T : Any>(

    private val entry: NavEntry<T>, private val dialogProperties: DialogProperties, private val onBack: (count: Int) -> Unit, ... ) : OverlayScene<T> { ... override val content: @Composable (() -> Unit) = { Dialog( onDismissRequest = { onBack(1) }, properties = dialogProperties ) { entry.Content() } } } BOESPJEYOBWJHBUJPOVJ%JBMPH4DFOFLU
  32.  /BW&OUSZ%FDPSBUPS /BW&OUSZ%FDPSBUPS ΤϯτϦʔʹঢ়ଶ΍੹຿Λ֦ு͢ Δػೳ public class NavEntryDecorator<T : Any>

    internal constructor( internal val onPop: (key: Any) -> Unit, internal val navEntryDecorator: @Composable (entry: NavEntry<T>) -> Unit, ) BOESPJEYOBWJHBUJPOSVOUJNF/BW&OUSZ%FDPSBUPSLU
  33.  /BW&OUSZ%FDPSBUPS /BW&OUSZ%FDPSBUPSOBW&OUSZ%FD PSBUPS ΤϯτϦʔͷίϯςϯπΛඳը͢ Δͱͱ΋ʹɺ֦ுॲཧΛ࣮૷͢Δ public class NavEntryDecorator<T :

    Any> internal constructor( internal val onPop: (key: Any) -> Unit, internal val navEntryDecorator: @Composable (entry: NavEntry<T>) -> Unit, ) BOESPJEYOBWJHBUJPOSVOUJNF/BW&OUSZ%FDPSBUPSLU
  34.  /BW&OUSZ%FDPSBUPS /BW&OUSZ%FDPSBUPSPO1PQ ΤϯτϦʔ͕όοΫελοΫ͔Β QPQ͞ΕΔͱ͖ʹݺ͹Εɺޙॲཧ Λ࣮૷͢Δ public class NavEntryDecorator<T :

    Any> internal constructor( internal val onPop: (key: Any) -> Unit, internal val navEntryDecorator: @Composable (entry: NavEntry<T>) -> Unit, ) BOESPJEYOBWJHBUJPOSVOUJNF/BW&OUSZ%FDPSBUPSLU
  35.  /BW&OUSZ%FDPSBUPS /BW&OUSZ%FDPSBUPS͸ /BW%JTQMBZͷҾ਺ʹෳ਺ઃఆͰ ͖Δ ઃఆͨ͠શσίϨʔλʔॲཧ͕ఆ ٛॱংͰΤϯτϦʔʹద༻͞ΕΔ @Composable public fun

    <T : Any> NavDisplay( ... entryDecorators: List<NavEntryDecorator<*>> = listOf( rememberSceneSetupNavEntryDecorator(), rememberSavedStateNavEntryDecorator(), ), ... ){ ... } BOESPJEYOBWJHBUJPOVJ/BW%JTQMBZLU
  36.  /BW&OUSZ%FDPSBUPS /BW%JTQMBZ͸σϑΥϧτͰҎԼ ͷσίϨʔλʔΛઃఆ ɹ4DFOF4FUVQ/BW&OUSZ%FDPSBUPS ɹ4BWFE4UBUF/BW&OUSZ%FDPSBUPS @Composable public fun <T

    : Any> NavDisplay( ... entryDecorators: List<NavEntryDecorator<*>> = listOf( rememberSceneSetupNavEntryDecorator(), rememberSavedStateNavEntryDecorator(), ), ... ){ ... } BOESPJEYOBWJHBUJPOVJ/BW%JTQMBZLU
  37.  /BW&OUSZ%FDPSBUPS 4DFOF4FUVQ/BW&OUSZ%FDPSBUPS ΤϯτϦʔͷঢ়ଶ͕γʔϯؒͷҠ ಈͰࣦΘͳ͍Α͏ʹ੍ޚΛ͢Δ public fun SceneSetupNavEntryDecorator(): NavEntryDecorator<Any> {

    val movableContentMap: MutableMap<Any, @Composable (@Composable () -> Unit) -> Unit> = mutableStateMapOf() return navEntryDecorator( onPop = { contentKey -> movableContentMap.remove(contentKey) } ) { entry -> ... val movableContent = remember { movableContentMap.getOrPut(contentKey) { movableContentOf { content -> content() } } } movableContent { entry.Content() } ... } } BOESPJEYOBWJHBUJPOVJ4DFOF4FUVQ/BW&OUSZ%FDPSBUPSLU
  38.  /BW&OUSZ%FDPSBUPS 4DFOF4FUVQ/BW&OUSZ%FDPSBUPS NPWBCMF$POUFOU0GͰΤϯτ ϦʔͷίϯςϯπΛϥοϓ͠ɺ SFNFNCFSঢ়ଶΛอ࣋ return navEntryDecorator( ... )

    { entry -> ... val movableContent = remember { movableContentMap.getOrPut(contentKey) { movableContentOf { content -> content() } } } movableContent { entry.Content() } ... } } BOESPJEYOBWJHBUJPOVJ4DFOF4FUVQ/BW&OUSZ%FDPSBUPSLU
  39.  /BW&OUSZ%FDPSBUPS 4DFOF4FUVQ/BW&OUSZ%FDPSBUPS PO1PQͰQPQ͞ΕͨΤϯτϦʔ ͷίϯςϯπΛϝϞϦղ์ return navEntryDecorator( onPop = {

    contentKey -> ɹɹɹɹɹmovableContentMap.remove(contentKey) } ) {ɹ...ɹ} BOESPJEYOBWJHBUJPOVJ4DFOF4FUVQ/BW&OUSZ%FDPSBUPSLU
  40.  /BW&OUSZ%FDPSBUPS 4BWFE4UBUF/BW&OUSZ%FDPSBUPS ΤϯτϦʔݻ༗ͷ 4BWFE4UBUF3FHJTUSZ 0XOFS Λ ఏڙ SFNFNCFS4BWFBCMFͷੜଘظ ͕ؒΤϯτϦʔʹඥͮ͘Α͏ʹͳ

    Δ public fun SavedStateNavEntryDecorator( saveableStateHolder: SaveableStateHolder ): NavEntryDecorator<Any> { val onPop: (Any) -> Unit ={ contentKey -> saveableStateHolder.removeState(contentKey) } return navEntryDecorator(onPop = onPop) { entry -> saveableStateHolder.SaveableStateProvider( entry.contentKey ) { entry.Content() } } } BOESPJEYOBWJHBUJPOSVOUJNF4BWFE4UBUF/BW&OUSZ%FDPSBUPSLU
  41.  /BW&OUSZ%FDPSBUPS 4BWFE4UBUF/BW&OUSZ%FDPSBUPS 4BWFBCMF4UBUF1SPWJEFSͷLFZ ʹΤϯτϦʔͷࣝผࢠ ʢDPOUFOU,FZʣΛઃఆ͢Δ͜ͱ ͰɺΤϯτϦʔݻ༗ͷ 4BWFE4UBUF3FHJTUSZΛੜ੒ public fun

    SavedStateNavEntryDecorator( saveableStateHolder: SaveableStateHolder ): NavEntryDecorator<Any> { val onPop: (Any) -> Unit ={ contentKey -> saveableStateHolder.removeState(contentKey) } return navEntryDecorator(onPop = onPop) { entry -> saveableStateHolder.SaveableStateProvider( entry.contentKey ) { entry.Content() } } } BOESPJEYOBWJHBUJPOSVOUJNF4BWFE4UBUF/BW&OUSZ%FDPSBUPSLU
  42.  /BW&OUSZ%FDPSBUPS 4BWFE4UBUF/BW&OUSZ%FDPSBUPS PO1PQͰঢ়ଶഁغ public fun SavedStateNavEntryDecorator( saveableStateHolder: SaveableStateHolder ):

    NavEntryDecorator<Any> { val onPop: (Any) -> Unit ={ contentKey -> saveableStateHolder.removeState(contentKey) } return navEntryDecorator(onPop = onPop) { entry -> saveableStateHolder.SaveableStateProvider( entry.contentKey ) { entry.Content() } } } BOESPJEYOBWJHBUJPOSVOUJNF4BWFE4UBUF/BW&OUSZ%FDPSBUPSLU
  43.  /BW&OUSZ%FDPSBUPS 7JFX.PEFM4UPSF/BW&OUSZ%FDPSBUPS ΤϯτϦʔݻ༗ͷ 7JFX.PEFM4UPSF0XOFSΛఏڙ BOESPJEYMJGFDZDMFMJGFDZDMF WJFXNPEFMOBWJHBUJPO public fun ViewModelStoreNavEntryDecorator(

    viewModelStore: ViewModelStore, shouldRemoveStoreOwner: () -> Boolean, ): NavEntryDecorator<Any> { val storeOwnerProvider: EntryViewModel = viewModelStore.getEntryViewModel() val onPop: (Any) -> Unit = { key -> if (shouldRemoveStoreOwner()) { storeOwnerProvider .clearViewModelStoreOwnerForKey(key) } } ... return navEntryDecorator(onPop) { entry -> val childViewModelStoreOwner = remember { ... } CompositionLocalProvider( LocalViewModelStoreOwner provides childViewModelStoreOwner ) { entry.Content() } } } BOESPJEYMJGFDZDMFWJFXNPEFMOBWJHBUJPO7JFX.PEFM4UPSF/BW&OUSZ%FDPSBUPSLU
  44.  /BW&OUSZ%FDPSBUPS 7JFX.PEFM4UPSF/BW&OUSZ%FDPSBUPS $PNQPTJUJPO-PDBM1SPWJEFSͰ -PDBM7JFX.PEFM4UPSF0XOFSΛ ઃఆ public fun ViewModelStoreNavEntryDecorator( ...

    ): NavEntryDecorator<Any> { ... return navEntryDecorator(onPop) { entry -> ... CompositionLocalProvider( LocalViewModelStoreOwner provides childViewModelStoreOwner ) { entry.Content() } } } BOESPJEYMJGFDZDMFWJFXNPEFMOBWJHBUJPO7JFX.PEFM4UPSF/BW&OUSZ%FDPSBUPSLU
  45.  7JFX.PEFM4UPSF/BW&OUSZ%FDPSBUPS 7JFX.PEFM4UPSF/BW&OUSZ%FDPSBUPS 7JFX.PEFMʹ 4BWFE4UBUF)BOEMFSΛఏڙ͢Δ ͨΊͷઃఆ 4BWFE4UBUF/BW&OUSZ%FDPSBUPS ʹΑͬͯఏڙ͞ΕΔ 4BWFE4UBUF3FHJTUSZ0XOFSΛ༻͍ ΔલఏͰઃܭ͞Ε͍ͯΔ

    public fun ViewModelStoreNavEntryDecorator( ... ) { ... val savedStateRegistryOwner = LocalSavedStateRegistryOwner.current val childViewModelStoreOwner = remember { object : ViewModelStoreOwner, SavedStateRegistryOwner by savedStateRegistryOwner, ... { ... init { require( this.lifecycle.currentState == Lifecycle.State.INITIALIZED ) { … } enableSavedStateHandles() } } } } BOESPJEYMJGFDZDMFWJFXNPEFMOBWJHBUJPO7JFX.PEFM4UPSF/BW&OUSZ%FDPSBUPSLU
  46.  7JFX.PEFM4UPSF/BW&OUSZ%FDPSBUPS 7JFX.PEFM4UPSF/BW&OUSZ%FDPSBUPS ͸ඞͣ 4BWFE4UBUF/BW&OUSZ%FDPSBUPSͷ ޙʹఆٛ͢Δ NavDisplay( ... entryDecorators =

    listOf( rememberSceneSetupNavEntryDecorator(), rememberSavedStateNavEntryDecorator(), // ←ઌʹ rememberViewModelStoreNavEntryDecorator(), // ←ޙʹ ), )
  47.  ϢχʔΫΩʔͷରԠ σΟʔϓϦϯΫ΍௨஌ͳͲɺ༧ଌ Ͱ͖ͳ͍ભҠ͕ݟࠐ·ΕΔ৔߹ɺ ΩʔॏෳʹΑΔҙਤ͠ͳ͍ঢ়ଶ෮ ݩ͕ൃੜ͠ͳ͍͔Λ֬ೝ͢Δ ঢ়ଶ෮ݩΛ๷͙৔߹͸ɺର৅ͷ Ωʔ͕౎౓ϢχʔΫͱͳΔΑ͏ʹ ͢Δ //

    ຖճ৽͍͠ΩʔΛੜ੒ // ΩʔͷҰҙੑ͸ NavEntry.contentKey ʹΑܾͬͯ·Δ // contentKey ͷσϑΥϧτ͸ key.toString() data class Form( val id: String = UUID.randomUUID().toString() )
  48.  όοΫελοΫΩʔͷ੍໿ !4FSJBMJ[BCMF ੍໿ʹࡍͯ͠ͷίʔυن໿ 6*ϨΠϠʔ֎ͰͷφϏήʔγϣ ϯ໨తͷΞϊςʔτͷېࢭ // UI ϨΠϠʔ֎ͰͷφϏήʔγϣϯ໨తͷΞϊςʔτΛېࢭ //

    @NavSerializable data class User(val id: String) // ϞσϧσʔλΛɺΩʔσʔλʹม׵ fun UserModel.mapNavKey() = UserNavKey(this.id)ɹ
  49.  ωετάϥϑͷҠߦ ϫϯόϯΫΞϓϦͰ͸ɺભҠϑ ϩʔͷΧϓηϧԽͷ໨తͰɺωε τ͞ΕͨφϏήʔγϣϯάϥϑΛ ࠾༻ ΧʔυͷೖۚϑϩʔͳͲ NavHost(navController, startDestination =

    Home) { composable<Home> { HomeScreen() } /** ↓ ωετ͞ΕͨφϏήʔγϣϯάϥϑ **/ navigation<Deposit>( startDestination = DepositStart ) { composable<DepositStart> { DepositStartScreen( ɹɹɹɹɹɹɹ onComplete = { ɹɹɹɹɹɹɹ navController.navigate(DepositComplete) ɹɹɹɹɹɹɹ } ɹɹɹɹɹɹɹ) } composable<DepositComplete> { DepositComplete() } } }
  50.  ωετάϥϑͷҠߦ ʮϧʔτΤϯτϦʔ͕ 4UPSF0XOFSͷ7JFX.PEFMʯΛ ڞ༻͢Δ͜ͱͰɺάϥϑ಺ͷը໘ ؒͰঢ়ଶڞ༻ navigation<Deposit>(startDestination = DepositStart) {

    composable<DepositStart> { val parentEntry = remember(it) { navController.getBackStackEntry(Deposit) } val parentViewModel = viewModel<DepositViewModel>( viewModelStoreOwner = parentEntry ) DepositStartScreen( onComplete = { depositData -> /* ωετάϥϑͷϥΠϑαΠΫϧʹσʔλΛอଘ */ parentViewModel.saveDepositData(depositData) navController.navigate(DepositComplete) } ) } composable<DepositComplete> { val parentEntry = ... val parentViewModel = ... DepositComplete(data = parentViewModel.depositData) } }
  51.  ωετάϥϑͷҠߦ /BWJHBUJPOͰωετάϥϑΛ࠶ ݱ͢Δʹ͸ɺάϥϑͱ͍ͨ͠Τϯ τϦʔ಺Ͱ/BW%JTQMBZΛωετ ͢Δ͚ͩ NavDisplay( backStack = routeBackStack,

    entryProvider = entryProvider { entry(Home) { HomeScreen() } entry(Deposit) { /* ωετ͞Εͨ NavDisplay */ NavDisplay( ... ) } } )
  52.  ωετάϥϑͷҠߦ entry(Deposit) { val parentViewModel = viewModel<DepositViewModel>() NavDisplay( backStack

    = depositBackStack, entryProvider = entryProvider { entry(DepositStart) { DepositStartScreen( onComplete = { depositData -> parentViewModel.saveDepositData(depositData) depositBackStack.add(DepositComplete) } ) } entry(DepositComplete) { DepositCompleteScreen( data = parentViewModel.depositData ) } } ) ) ωετ/BW%JTQMBZ಺Ͱ 7JFX.PEFMΛڞ༻͍ͨ͠৔߹ ͸ɺωετ/BW%JTQMBZͷඳըΤ ϯτϦʔΛ4UPSF0XOFSͱ͢Δ 7JFX.PEFMΛ༻͍Δ
  53.  ωετάϥϑͷҠߦ navigation<Deposit>( startDestination = DepositStart ) { composable<DepositStart> {

    DepositStartScreen( ... ) } composable<DepositComplete> { DepositCompleteScreen( ... ) } } composable<Deposit> { val depositBackStack = remember { mutableStateListOf<Any>(DepositStart) } NavDisplay( backStack = depositBackStack, entryProvider = entryProvider { entry<DepositStart> { DepositStartScreen( ... ) } entry<DepositComplete> { DepositCompleteScreen( ... ) } } ) }
  54.  ܭըᶃωετ͞ΕͨάϥϑͷҠߦ composable<Deposit> { ... ɹɹ val parentViewModel =ɹ viewModel<DepositViewModel>()

    NavDisplay( backStack = depositBackStack, entryProvider = entryProvider { entry<DepositStart> { ... } entry<DepositComplete> { DepositCompleteScreen( data = parentViewModel.depositData ) } } ) } ΤϯτϦʔؒͰڞ༻͢Δ 7JFX.PEFM͸ /BW#BDL4UBDL&OUSZ௚ԼͰੜ੒ ͠ɺ֤ΤϯτϦʔͰ௚઀ࢀর͢Δ
  55.  )JMU 7JFX.PEFM ͱͷ౷߹ /BWJHBUJPOͰ)JMU7JFX.PEFM Λ࢖༻͢Δ৔߹͸ɺ௨ৗͷ 7JFX.PEFM࢖༻࣌ͱಉ༷ʹɺ 7JFX.PEFM4UPSF/BW&OUSZ%FDP SBUPSΛઃఆ͢Δ NavDisplay(

    entryDecorators = listOf( … rememberViewModelStoreNavEntryDecorator() ), entryProvider = entryProvider { entry(DetailKey) { val viewModel = hiltViewModel<DetailViewModel>() } } ... ) @HiltViewModel class DetailViewModel @AssistedInject constructor( val detailRepository: DetailRepository ) { ... }
  56.  )JMU 7JFX.PEFM ͱͷ౷߹ !"TTJTUFE'BDUPSZܦ༝ͰΩʔΛ %*͢Δྫ NavDisplay( entryProvider = entryProvider

    { entry(DetailKey) { val viewModel = ɹɹɹɹɹ hiltViewModel<DetailViewModel,DetailViewModel.Factory>( creationCallback = { factory -> factory.create(key) } ) } } ... ) @HiltViewModel(assistedFactory = DetailViewModel.Factory::class) class DetailViewModel @AssistedInject constructor( @Assisted val key : DetailKey ) : ViewModel() { @AssistedFactory interface Factory { fun create(navKey: DetailKey): DetailViewModel } }
  57.  σΟʔϓϦϯΫφϏήʔγϣϯ ࣗલͰϋϯυϦϯάॲཧΛ࣮૷͢ Δྫᶃ ΩʔʹରԠ͢Δ63-จࣈྻΛฦ ͢ΠϯλʔϑΣʔε 63-จࣈྻ͔ΒΩʔʹม׵͢Δ ΠϯλʔϑΣʔε Λఆٛ //

    ΩʔͰ࣮૷ fun interface NavDeepLinkKey: NavKey { fun toUrl(): String? } // Ωʔ΋͘͠͸Ωʔ಺ͷ companion object Ͱ࣮૷ fun interface NavDeeplinkParser { fun parse(url: String): NavDeepLinkKey? }
  58.  σΟʔϓϦϯΫφϏήʔγϣϯ ࣗલͰϋϯυϦϯάॲཧΛ࣮૷͢ Δྫᶄ %BUBPCKFDUͰ͸ɺ྆ํͷΠϯ λʔϑΣʔεΛ࣮૷ data object Home :

    NavDeepLinkKey, NavDeeplinkParser { override fun toUrl(): String = "myapp://home" override fun parse(url: String) = if (url == toUrl()) Home else null }
  59.  σΟʔϓϦϯΫφϏήʔγϣϯ ࣗલͰϋϯυϦϯάॲཧΛ࣮૷͢ Δྫᶅ %BUBDMBTTͰ͸ɺΫϥεͱ $PNQBOJPOPCKFDUʹ෼͚࣮ͯ૷ data class Detail(val id:

    String) : NavDeepLinkKey { override fun toUrl(): String = “$URL${id}" // ύʔεॲཧ͸ Companion object Ͱ࣮૷ companion object DeepLinkParser : NavDeeplinkParser { private const val URL = "myapp://detail/" override fun parse(url: String) = if (url.startsWith(URL)) { Detail(id = url.removePrefix(URL)) ɹɹɹɹɹɹɹ} else { ɹɹɹɹɹɹɹ null } } }
  60.  σΟʔϓϦϯΫφϏήʔγϣϯ ࣗલͰϋϯυϦϯάॲཧΛ࣮૷͢ Δྫᶆ ύʔεॲཧΛॱʹ࣮ߦ͠ɺ࠷ॳʹ ݟ͔ͭͬͨΩʔΛόοΫελοΫ ʹ௥Ճ fun MutableList<Any?>.navigateTo( url:

    String ) { val key = parse(url) ?: return add(key) } fun parse(url: String): NavDeepLinkKey? = listOf( Home, Detail.DeepLinkParser, /* ύʔεର৅ͷΩʔ͕૿͑ͨΒ͜͜ʹ௥Ճ */ ).firstNotNullOfOrNull { it.parse(url) }
  61.  #PUUPN4IFFUͷҠߦ 4DFOF4USBUFHZͱϝλσʔλͷઃ ఆ class BottomSheetSceneStrategy<T : Any> : SceneStrategy<T>

    { companion object { internal const val KEY_BOTTOM_SHEET = "bottomSheet" fun bottomSheet(): Map<String, Any> = ɹɹɹɹɹɹɹɹɹmapOf(KEY_BOTTOM_SHEET to Unit) } } NavDisplay( sceneStrategy = remember { BottomSheetSceneStrategy<NavKey>() } entryProvider = entryProvider { entry<Home> { ... } entry<Sheet>( metadata = BottomSheetSceneStrategy.bottomSheet() ) { ... } } )
  62.  #PUUPN4IFFUͷҠߦ ϝλσʔλͷ֬ೝ class BottomSheetSceneStrategy<T : Any> : SceneStrategy<T> {

    @Composable override fun calculateScene( entries: List<NavEntry<T>>, onBack: (count: Int) -> Unit ): Scene<T>? { val last = entries.lastOrNull() val metadata = last?.metadata[KEY_BOTTOM_SHEET] if(metadata == null) return null ... } }
  63.  #PUUPN4IFFUͷҠߦ γʔϯͷ࡞੒ @Composable override fun calculateScene( entries: List<NavEntry<T>>, onBack:

    (count: Int) -> Unit ): Scene<T>? { ... return BottomSheetScene( key = last.contentKey, previousEntries = entries.dropLast(1), overlaidEntries = entries.dropLast(1), entry = last, onBack = onBack, ) }
  64.  ՝୊ᶃ#PUUPN4IFFUͷҠߦ 4DFOFͷ࣮૷ .PEBM#PUUPN4IFFU্ʹΤϯτ ϦʔͷίϯςϯπΛඳը import androidx.compose.material3.ModalBottomSheet internal class BottomSheetScene<T

    : Any>( override val key: Any, override val previousEntries: List<NavEntry<T>>, override val overlaidEntries: List<NavEntry<T>>, private val entry: NavEntry<T>, private val onBack: (count: Int) -> Unit, ) : OverlayScene<T> { override val entries: List<NavEntry<T>> = listOf(entry) @OptIn(ExperimentalMaterial3Api::class) override val content: @Composable () -> Unit = { ModalBottomSheet( onDismissRequest = { onBack(1) }, ) { entry.Content() } } }
  65.  /BWJHBUJPO#BSͷҠߦ όοΫελοΫͷॳظԽ όοΫελοΫʹશͯͷλϒ ΩʔΛੵΉ ࠷ޙͷΩʔʢ࠷ॳʹඳը͢Δը ໘ʣ͕ʮϗʔϜʯͱͳΔΑ͏ʹฒ ͼସ͑Δ ˠॳظը໘͕ʮϗʔϜʯͷঢ়ଶ sealed

    interface TabKey { data object Home : TabKey data object Card : TabKey data object Pocket : TabKey companion object { val all = listOf(Home, Card, Pocket) } } val backStack = remember { mutableStateListOf<Any>().apply { addAll(TabKey.all) remove(TabKey.Home) add(TabKey.Home) } }
  66.  /BWJHBUJPO#BSͷҠߦ λϒબ୒ॲཧ ʮϗʔϜʯλϒΛબ୒࣌ɺϗʔϜ ΩʔΛελοΫͷ࠷ޙʹੵΉ ʮϗʔϜʯҎ֎ͷλϒΛબ୒࣌ɺ ϗʔϜΩʔˠબ୒ͨ͠Ωʔͷॱ൪ ͰελοΫͷ࠷ޙʹੵΉ ʮϗʔϜʯҎ֎ͷλϒͷ໭Γ ઌ͕ʮϗʔϜʯʹͳΔ

    NavigationBarItem( selected = tabKey == backStack.last(), onClick = { backStack.apply { remove(TabKey.Home) if (tabKey != TabKey.Home) remove(tabKey) add(TabKey.Home) if (tabKey != TabKey.Home) add(tabKey) } }, ... )
  67.  /BWJHBUJPO#BSͷҠߦ ϗʔϜλϒΑΓલͷόοΫελο ΫΛແࢹ͢ΔͨΊͷΧελϜ 4DFOFΛ࡞੒͢Δ private class HomeTabSceneStrategy<T : Any>

    : SceneStrategy<T> { @Composable override fun calculateScene( entries: List<NavEntry<T>>, onBack: (Int) -> Unit ): Scene<T>? { return object : Scene<T> { override val key: Any = "HomeTabScene" override val entries: List<NavEntry<T>> = listOf(entries.last) override val previousEntries: List<NavEntry<T>> =ɹ emptyList() override val content: @Composable () -> Unit = { last.Content() } } } }
  68.  /BWJHBUJPO#BSͷҠߦ QSFWJPVT&OUSJFTΛۭ഑ྻͱ͢Δ ͜ͱͰɺ͜ΕҎ্໭ΕΔΤϯτ Ϧʔ͕ແ͍͜ͱΛ/BW%JTQMBZʹ఻ ͑Δ QSFWJPVT&OUSJFT͕ۭͷͱ͖ɺ ໭Δૢ࡞͸ԼҐͷϋϯυϥ ʢ"DUJWJUZͳͲʣʹҕৡ͞ΕΔ private

    class HomeTabSceneStrategy<T : Any> : SceneStrategy<T> { @Composable override fun calculateScene( entries: List<NavEntry<T>>, onBack: (Int) -> Unit ): Scene<T>? { return object : Scene<T> { override val key: Any = "HomeTabScene" override val entries: List<NavEntry<T>> = listOf(entries.last) override val previousEntries: List<NavEntry<T>> =ɹ emptyList() override val content: @Composable () -> Unit = { last.Content() } } } }
  69.  /BWJHBUJPO#BSͷҠߦ ʮϗʔϜʯλϒͷΤϯτϦʔʹ γʔϯઐ༻ͷϝλσʔλΛઃఆ private class HomeTabSceneStrategy<T : Any> :

    SceneStrategy<T> { @Composable override fun calculateScene( ... ): Scene<T>? { val last = entries.lastOrNull() ?: return null if (!last.metadata.containsKey(HOME_TAB_KEY)) return null return object : Scene<T> { ... } } companion object { internal const val HOME_TAB_KEY = "HomeTab" fun homeTab() = mapOf(HOME_TAB_KEY to true) } } entry<TabKey.Home>( metadata = homeTab() ) { ... }
  70.  /BW&OUSZ%FDPSBUPSͱ4DFOFΛΧελϜ͢Δ🛠 ؍఺ /BW&OUSZ%FDPSBUPS 4DFOF ༻్ ΤϯτϦʔ಺෦ͷॲཧΛ֦ு͢Δʢঢ়ଶอ࣋ɺґଘ ੑ஫ೖɺϥΠϑαΠΫϧ؅ཧͳͲʣ ෳ਺ΤϯτϦʔΛ·ͱΊͯɺը໘্ͰͲ͏ߏ੒ɾඳը ͢Δ͔ΛܾΊΔ

    Өڹൣғ ݸʑͷΤϯτϦʔ୯Ґʹݶఆɻہॴతʹ௥Ճ΍ࠩ͠ ସ͕͑Մೳ ը໘શମ΍ෳ਺ΤϯτϦʔʹ·͕ͨΔɻ6*ߏ଄΍Ξ χϝʔγϣϯશൠʹӨڹ ར༻γʔϯ ΤϯτϦʔʹ7JFX.PEFMείʔϓΛ෇༩ 4BWFE4UBUFͷࣗಈ࿈ܞ όοΫελοΫ͔Β֎ΕͨࡍͷϦιʔεղ์ γϯάϧϖΠϯϚϧνϖΠϯ੾Γସ͑ μΠΞϩάɾϘτϜγʔτͷॏͶ߹Θͤ 1SFEJDUJWF#BDL࣌ͷද੍ࣔޚ ੹຿ͷཻ౓ ʮΤϯτϦʔͭʯʹดͨ͡੹຿ɻத਎ΛͲ͏ѻ͏͔ ʮΤϯτϦʔू߹ʯͱͯ͠ͷ੹຿ɻݟͤํɾ഑ஔɾભ ҠΛͲ͏ઃܭ͢Δ͔
  71.  /BW&OUSZ%FDPSBUPSͱ4DFOFΛΧελϜ͢Δ🛠 νϟΠϧυΞΧ΢ϯτͰͷද͕ࣔ ېࢭ͞Ε͍ͯΔը໘ʹ͓͍ͯɺ୅ ΘΓʹϒϩοΫը໘Λඳը͢Δྫ const val KEY_BLOCK_FOR_CHILD = "blockForChild"

    fun blockForChild(): Map<String, Any> = mapOf(KEY_BLOCK_FOR_CHILD to true) @Composable fun rememberChildBlockNavEntryDecorator(isChild: Boolean) = remember<NavEntryDecorator<Any>> { navEntryDecorator { entry -> entry.metadata[KEY_BLOCK_FOR_CHILD] ?: return@navEntryDecorator if (isChild) BlockContent() else entry.Content() } }