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

Molecular Theory — Navigation and Dependency Injection in Compose

rjrjr
September 06, 2022
330

Molecular Theory — Navigation and Dependency Injection in Compose

A discussion of how to use Cash App's Molecule to build a dependency injection friendly presenter layer with the Compose core runtime, nicely decoupled from view code.

One warning, I made a mistake in this preso: I said that Compose's support for novel form factors requires paying the old configuration change tax, and coping with multiple Activity instances.

This is false, and the strong guidance of the #Compose team continues to be to set your manifest to handle all config changes manually. With that done, an #Android app rooted in Compose UI has no reason to use #Molecule for anything outside of testing.

Delivered at droidcon NYC, September 2022.: https://www.droidcon.com/2022/09/29/navigation-and-dependency-injection-in-compose/

Original slide deck, with speaker notes: https://docs.google.com/presentation/d/1yXXjJ2n0IzZZBP3MoIxmBEp_pXW6lohh9TnWCNGGDYQ/edit#slide=id.g4088c7565d_2_5

rjrjr

September 06, 2022
Tweet

Transcript

  1. Ongoing conversations •@Composable fun Foo() / Dependency Injection: fight! •Sweet,

    sweet state management outside of view code? •Why does it hurt when I navigate?
  2. Protocol Buffers Wire shmoo.com interface ShmooService Retrofit interface ShmooDao Room

    / SQLDelight @Composable fun Shmoo( model: ShmooModel )
  3. Protocol Buffers Wire shmoo.com interface ShmooService Retrofit interface ShmooRepo SQLDelight

    Dagger + Anvil @Inject ShmooRepo @Composable fun Shmoo( model: ShmooModel )
  4. Protocol Buffers Wire shmoo.com interface ShmooService Retrofit SQLDelight Dagger +

    Anvil ? interface ShmooRepo @Composable fun Shmoo( model: ShmooModel )
  5. ? ViewModel Lifecycle Navigation Fragment interface ShmooServ Retrofi interface ShmooRepo

    SQLDelight Dagger + Anvil @Composable fun Shmoo( model: ShmooModel )
  6. ? ViewModel Lifecycle Navigation Fragment CoroutineScope interface ShmooServ Retrofi interface

    ShmooRepo SQLDelight Dagger + Anvil @Composable fun Shmoo( model: ShmooModel )
  7. ? ViewModel Lifecycle Navigation Fragment CoroutineScope Suspend interface ShmooServ Retrofi

    interface ShmooRepo SQLDelight Dagger + Anvil @Composable fun Shmoo( model: ShmooModel )
  8. ? ViewModel Lifecycle Navigation Fragment CoroutineScope Suspend StateFlow interface ShmooServ

    Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable fun Shmoo( model: ShmooModel )
  9. ? ViewModel Lifecycle Navigation Fragment CoroutineScope Suspend StateFlow LaunchEffect interface

    ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable fun Shmoo( model: ShmooModel )
  10. ? ViewModel Lifecycle Navigation Fragment CoroutineScope Suspend StateFlow LaunchEffect remember

    { } interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable fun Shmoo( model: ShmooModel )
  11. ? ViewModel Lifecycle Navigation Fragment CoroutineScope Suspend StateFlow LaunchEffect remember

    { } MutableState interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable fun Shmoo( model: ShmooModel )
  12. ? ViewModel Lifecycle Navigation Fragment CoroutineScope Suspend StateFlow LaunchEffect remember

    { } MutableState interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable fun Shmoo( model: ShmooModel )
  13. interface ShmooServ Retrofi interface ShmooRepo SQLDelight @Composable fun Shmoo( model:

    ShmooModel ) Shmoo Presenter StateFlow< ShmooModel > Dagger + Anvil
  14. interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable

    fun AppUi( models: StateFlow<ShmooModel> ) { val model by models.collectAsState() Shmoo(model) } Shmoo Presenter Workflow
  15. data class ShmooModel( val loading: Boolean, val onEaten: (() ->

    Unit)? ) { val eaten: Boolean = !loading && onEaten != null } 👈 Presenter Props ShmooMode l event @Inject Repo repo.getItem(id ) .collect { … }
  16. data class ShmooModel( val loading: Boolean, val onEaten: (() ->

    Unit)? ) { val eaten: Boolean = !loading && onEaten != null } Presenter Props ShmooMode l event @Inject Repo 👈 repo.getItem(id ) .collect { … }
  17. Presenter ShmooId ShmooMode l event @Inject Repo data class ShmooModel(

    val loading: Boolean, val onEaten: (() -> Unit)? ) { val eaten: Boolean = !loading && onEaten != null } @Composable fun presentShmoo( id: ShmooId ): ShmooModel { } 👈 repo.getItem(id ) .collect { … }
  18. Presenter ShmooId ShmooMode l event @Inject Repo data class ShmooModel(

    val loading: Boolean, val onEaten: (() -> Unit)? ) { val eaten: Boolean = !loading && onEaten != null } @Composable fun presentShmoo( id: ShmooId ): ShmooModel { } 👈 repo.getItem(id ) .collect { … }
  19. Shmoo Presenter Props ShmooMode l event @Inject ShmooRepo data class

    ShmooModel( val loading: Boolean, val onEaten: (() -> Unit)? ) { val eaten: Boolean = !loading && onEaten != null } class ShmooPresenter @Inject constructor( private val repo: ShmooRepo, ) { @Composable fun present( id: ShmooId ): ShmooModel { } } 👈 repo.getItem(id ) .collect { … }
  20. Shmoo Presenter Props ShmooMode l event @Inject ShmooRepo data class

    ShmooModel( val loading: Boolean, val onEaten: (() -> Unit)? ) { val eaten: Boolean = !loading && onEaten != null } class RealShmooPresenter @Inject constructor( private val repo: ShmooRepo, ): ShmooPresenter { @Composable override fun present( id: ShmooId ): ShmooModel { } } 👈 repo.getItem(id ) .collect { … }
  21. Shmoo Presenter Props ShmooMode l event @Inject ShmooRepo data class

    ShmooModel( val loading: Boolean, val onEaten: (() -> Unit)? ) { val eaten: Boolean = !loading && onEaten != null } class ShmooPresenter @Inject constructor( private val repo: ShmooRepo, ): Presenter<ShmooId, ShmooModel> { @Composable override fun present( id: ShmooId ): ShmooModel { } } 👈 repo.getItem(id ) .collect { … }
  22. Shmoo Presenter Props ShmooMode l event @Inject ShmooRepo data class

    ShmooModel( val loading: Boolean, val onEaten: (() -> Unit)? ) { val eaten: Boolean = !loading && onEaten != null } class ShmooPresenter @Inject constructor( private val repo: ShmooRepo, ) { @Composable fun present( id: ShmooId ): ShmooModel { } } 👈 repo.getItem(id ) .collect { … }
  23. Shmoo Presenter Props ShmooMode l event @Inject ShmooRepo data class

    ShmooModel( val loading: Boolean, val onEaten: (() -> Unit)? ) { val eaten: Boolean = !loading && onEaten != null } class ShmooPresenter @Inject constructor( private val repo: ShmooRepo, ) { @Composable fun present( id: ShmooId ): ShmooModel { } } repo.getItem(id ) .collect { … }
  24. repo.getItem(id ) .collect { … } Shmoo Presenter Props ShmooMode

    l event @Inject ShmooRepo class ShmooPresenter @Inject constructor( private val repo: ShmooRepo, ) { @Composable fun present( id: ShmooId ): ShmooModel { } } 👈
  25. Shmoo Presenter Props ShmooMode l event @Inject ShmooRepo class ShmooPresenter

    @Inject constructor( private val repo: ShmooRepo, ) { @Composable fun present( id: ShmooId ): ShmooModel { var shmooOrNull by repo.getShmoo(id) .collectAsState(initial = null) } } 👈 repo.getShmoo(i d) .collect { … }
  26. Shmoo Presenter Props ShmooMode l event @Inject ShmooRepo class ShmooPresenter

    @Inject constructor( private val repo: ShmooRepo, ) { @Composable fun present( id: ShmooId ): ShmooModel { var shmooOrNull by repo.getShmoo(id) .collectAsState(initial = null) } } 👈 repo.getShmoo(i d) .collect { … }
  27. class ShmooPresenter @Inject constructor( private val repo: ShmooRepo, ) {

    @Composable fun present( id: ShmooId ): ShmooModel { var shmooOrNull by repo.getShmoo(id) .collectAsState(initial = null) return ShmooModel( loading = shmooOrNull == null, onEaten = shmooOrNull?.let { it.reportEaten() } ) Shmoo Presenter Props ShmooMode l onEaten @Inject ShmooRepo 👈 repo.getShmoo(i d) .collect { … }
  28. class ShmooPresenter @Inject constructor( private val repo: ShmooRepo, ) {

    @Composable fun present( id: ShmooId ): ShmooModel { var shmooOrNull by repo.getShmoo(id) .collectAsState(initial = null) return ShmooModel( loading = shmooOrNull == null, onEaten = shmooOrNull?.let { it.reportEaten() } ) Shmoo Presenter Props ShmooMode l onEaten @Inject ShmooRepo repo.getShmoo(i d) .collect { … } class Continuity : ViewModel() { private val scope = viewModelScope + Main val models: StateFlow<ShmooModel> = scope.launchMolecule( clock = RecompositionClock.ContextClock ) { val rootPresenter = remember { DaggerComponent().rootPresenter() } rootPresenter.present() } }
  29. class ShmooPresenter @Inject constructor( private val repo: ShmooRepo, ) {

    @Composable fun present( id: ShmooId ): ShmooModel { var shmooOrNull by repo.getShmoo(id) .collectAsState(initial = null) return ShmooModel( loading = shmooOrNull == null, onEaten = shmooOrNull?.let { it.reportEaten() } ) Shmoo Presenter Props ShmooMode l onEaten @Inject ShmooRepo repo.getShmoo(i d) .collect { … } class Continuity : ViewModel() { private val scope = viewModelScope + Main val models: StateFlow<ShmooModel> = scope.launchMolecule( clock = RecompositionClock.ContextClock ) { val rootPresenter = remember { DaggerComponent().rootPresenter() } rootPresenter.present() } } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) val continuity: Continuity by viewModels() val models = continuity.models setContent { AppUi(models) } } class Continuity : ViewModel() { private val scope = viewModelScope + Main val models: StateFlow<ShmooModel> = scope.launchMolecule( clock = RecompositionClock.ContextClock ) { val rootPresenter = remember { DaggerComponent().rootPresenter() } rootPresenter.present() } }
  30. interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable

    fun AppUi( models: StateFlow<ShmooModel> ) { val model by models.collectAsState() Shmoo(model) } Shmoo Presenter Workflow
  31. interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable

    fun AppUi( models: StateFlow<ShmooModel> ) { val model by models.collectAsState() Shmoo(model) } Shmoo Presenter 🥳 Molecule 🥳
  32. Charles Shmoo nick: Charlie born: 30 Aug 1948 Eat Abner

    Shmoo Baker Shmoo Charlie Shmoo Delta Shmoo Echo Shmoo Foxtrot Shmoo George Shmoo Herman Shmoo
  33. Charles Shmoo nick: Charlie born: 30 Aug 1948 Eat Abner

    Shmoo Baker Shmoo Charlie Shmoo Delta Shmoo Echo Shmoo Foxtrot Shmoo George Shmoo Herman Shmoo Charlie Shmoo
  34. Charles Shmoo nick: Charlie born: 30 Aug 1948 Eat Abner

    Shmoo Baker Shmoo Charlie Shmoo Delta Shmoo Echo Shmoo Foxtrot Shmoo George Shmoo Herman Shmoo Charlie Shmoo SplitScreenModel ( IndexModel, ShmooModel )
  35. Shmoo Browser Presenter val shmooIndex by repo.getIndex() .collectAsState() @Inject Index

    Presenter var selection by remember { mutableState(-1) } @Inject ShmooRepo @Inject Shmoo Presenter
  36. Shmoo Browser Presenter val shmooIndex by repo.getIndex() .collectAsState() @Inject Index

    Presenter selection: Int, names: List<String> onSelect: (Int) -> Unit var selection by remember { mutableState(-1) } @Inject ShmooRepo @Inject Shmoo Presenter
  37. Shmoo Browser Presenter val shmooIndex by repo.getIndex() .collectAsState() @Inject Index

    Presenter IndexModel selection: Int, names: List<String> onSelect: (Int) -> Unit var selection by remember { mutableState(-1) } @Inject ShmooRepo @Inject Shmoo Presenter
  38. SplitScreenModel( IndexModel, EmptyModel ) Shmoo Browser Presenter val shmooIndex by

    repo.getIndex() .collectAsState() @Inject Index Presenter IndexModel selection: Int, names: List<String> onSelect: (Int) -> Unit var selection by remember { mutableState(-1) } @Inject ShmooRepo @Inject Shmoo Presenter
  39. SplitScreenModel( IndexModel, EmptyModel ) Shmoo Browser Presenter @Inject Shmoo Presenter

    val shmooIndex by repo.getIndex() .collectAsState() @Inject Index Presenter IndexModel onClick selection: Int, names: List<String> onSelect: (Int) -> Unit var selection by remember { mutableState(-1) } @Inject ShmooRepo
  40. SplitScreenModel( IndexModel, EmptyModel ) Shmoo Browser Presenter @Inject Shmoo Presenter

    val shmooIndex by repo.getIndex() .collectAsState() @Inject Index Presenter IndexModel onClick selection: Int, names: List<String> onSelect: (Int) -> Unit var selection by remember { mutableState(-1) } @Inject ShmooRepo onSelect
  41. SplitScreenModel( IndexModel, EmptyModel ) Shmoo Browser Presenter @Inject Shmoo Presenter

    val shmooIndex by repo.getIndex() .collectAsState() @Inject Index Presenter IndexModel onClick selection: Int, names: List<String> onSelect: (Int) -> Unit @Inject ShmooRepo onSelect var selection by remember { mutableState(0) }
  42. SplitScreenModel( IndexModel, EmptyModel ) Shmoo Browser Presenter @Inject Shmoo Presenter

    val shmooIndex by repo.getIndex() .collectAsState() @Inject Index Presenter IndexModel selection: Int, names: List<String> onSelect: (Int) -> Unit @Inject ShmooRepo var selection by remember { mutableState(0) } onClick onSelect
  43. SplitScreenModel( IndexModel, EmptyModel ) Shmoo Browser Presenter @Inject Shmoo Presenter

    id: ShmooId val shmooIndex by repo.getIndex() .collectAsState() @Inject Index Presenter IndexModel selection: Int, names: List<String> onSelect: (Int) -> Unit @Inject ShmooRepo var selection by remember { mutableState(0) } onClick onSelect
  44. SplitScreenModel( IndexModel, EmptyModel ) Shmoo Browser Presenter @Inject Shmoo Presenter

    id: ShmooId val shmooIndex by repo.getIndex() .collectAsState() key(id) { repo.get(id) .collectAsState() } @Inject Index Presenter IndexModel selection: Int, names: List<String> onSelect: (Int) -> Unit @Inject ShmooRepo var selection by remember { mutableState(0) } onClick onSelect
  45. SplitScreenModel( IndexModel, EmptyModel ) Shmoo Browser Presenter @Inject Shmoo Presenter

    id: ShmooId ShmooModel val shmooIndex by repo.getIndex() .collectAsState() key(id) { repo.get(id) .collectAsState() } @Inject Index Presenter IndexModel selection: Int, names: List<String> onSelect: (Int) -> Unit @Inject ShmooRepo var selection by remember { mutableState(0) } onClick onSelect
  46. Shmoo Browser Presenter @Inject Shmoo Presenter id: ShmooId ShmooModel val

    shmooIndex by repo.getIndex() .collectAsState() key(id) { repo.get(id) .collectAsState() } @Inject Index Presenter IndexModel selection: Int, names: List<String> onSelect: (Int) -> Unit @Inject ShmooRepo var selection by remember { mutableState(0) } SplitScreenModel( IndexModel, ShmooModel ) onClick onSelect
  47. interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable

    fun AppUi( models: StateFlow<ShmooModel> ) { val model by models.collectAsState() Shmoo(model) } Shmoo Presenter
  48. interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable

    fun AppUi( models: StateFlow<ShmooModel> ) { val model by models.collectAsState() Shmoo(model) } Shmoo Presenter 👈
  49. interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable

    fun AppUi( models: StateFlow<ShmooModel> ) { val model by models.collectAsState() Shmoo(model) } Shmoo Presenter 👈 👈
  50. interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable

    fun AppUi( models: StateFlow<ShmooModel> ) { val model by models.collectAsState() Shmoo(model) } Shmoo Presenter 👈 👈 👈
  51. interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable

    fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by models.collectAsState() // MAGIC? } Shmoo Browser Presenter 👈 👈 👈
  52. interface UiModel { @Composable fun Content() } data class SplitScreenModel(

    val first: UiModel, val second: UiModel ) : UiModel { @Composable override fun Content() { BoxWithConstraints { if (maxWidth > maxHeight) { Row { first.Content() second.Content() } } else { // … Showing Arbitrary UI Models 1. Quick & dirty interface
  53. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() // MAGIC? } Showing Arbitrary UI Models 1. Quick & dirty interface
  54. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() model.Content() } Showing Arbitrary UI Models 1. Quick & dirty interface
  55. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() model.Content() } Showing Arbitrary UI Models 1. Quick & dirty interface
  56. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() model.Content() } Showing Arbitrary UI Models 1. Quick & dirty interface
  57. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() model.Content() } Showing Arbitrary UI Models 1. Quick & dirty interface
  58. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() model.Content() } + Simple Showing Arbitrary UI Models 1. Quick & dirty interface
  59. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() model.Content() } + Simple + Compiler enforced Showing Arbitrary UI Models 1. Quick & dirty interface
  60. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() model.Content() } + Simple + Compiler enforced - Presenter & view coupled Showing Arbitrary UI Models 1. Quick & dirty interface
  61. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() model.Content() } + Simple + Compiler enforced - Presenter & view coupled - Rigid, hard to customize Showing Arbitrary UI Models 1. Quick & dirty interface
  62. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() model.Content() } + Simple + Compiler enforced - Presenter & view coupled - Rigid, hard to customize Showing Arbitrary UI Models 1. Quick & dirty interface
  63. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() model.Content() } + Simple + Compiler enforced - Presenter & view coupled - Rigid, hard to customize Probably just fine for many. Showing Arbitrary UI Models 1. Quick & dirty interface
  64. interface UiModel data class UiBinding<S : UiModel>( val type: KClass<S>,

    val content: @Composable (S) -> Unit ) Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup
  65. interface UiModel data class UiBinding<S : UiModel>( val type: KClass<S>,

    val content: @Composable (S) -> Unit ) @Composable fun ProvideLocalUiBindings( bindings: List<UiBinding<*>>, content: @Composable () -> Unit ) Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup
  66. interface UiModel data class UiBinding<S : UiModel>( val type: KClass<S>,

    val content: @Composable (S) -> Unit ) @Composable fun ProvideLocalUiBindings( bindings: List<UiBinding<*>>, content: @Composable () -> Unit ) @Composable fun ShowUi( uiModel: UiModel, modifier: Modifier = Modifier ) Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup
  67. internal class UiContentRegistry( private val bindings: Map<KClassBinding<*>, UiBinding<*>> ) {

    @Composable fun ContentFor( uiModel: UiModel, modifier: Modifier ) { Box( modifier, propagateMinConstraints = true ) { bindings[uiModel::class] ?.Content(uiModel) ?: Text("What’s a $uiModel?") } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup
  68. data class SplitScreenModel( val first: UiModel, val second: UiModel )

    : UiModel Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup
  69. data class SplitScreenModel( val first: UiModel, val second: UiModel )

    : UiModel val SPLIT_SCREEN_BINDING = UiBinding( SplitScreenModel::class ) { split -> BoxWithConstraints { if (maxWidth > maxHeight) { Row { ShowUi(split.firstView) ShowUi(split.secondView) } } else { // … Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup
  70. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() // MAGIC? } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup
  71. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( listOf( SPLIT_SCREEN_BINDING, SHMOO_INDEX_BINDING, SHMOO_BINDING ) ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup
  72. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( listOf( SPLIT_SCREEN_BINDING, SHMOO_INDEX_BINDING, SHMOO_BINDING ) ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup
  73. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( listOf( SPLIT_SCREEN_BINDING, SHMOO_INDEX_BINDING, SHMOO_BINDING ) ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup
  74. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( listOf( SPLIT_SCREEN_BINDING, SHMOO_INDEX_BINDING, SHMOO_BINDING ) ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup
  75. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( listOf( SPLIT_SCREEN_BINDING, SHMOO_INDEX_BINDING, SHMOO_BINDING ) ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup + Decoupled
  76. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( listOf( SPLIT_SCREEN_BINDING, SHMOO_INDEX_BINDING, SHMOO_BINDING ) ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup + Decoupled + Easy to customize
  77. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( listOf( SPLIT_SCREEN_BINDING, SHMOO_INDEX_BINDING, SHMOO_BINDING ) ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup + Decoupled + Easy to customize - Runtime errors
  78. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( listOf( SPLIT_SCREEN_BINDING, SHMOO_INDEX_BINDING, SHMOO_BINDING ) ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup + Decoupled + Easy to customize - Runtime errors - Boiler. Plate.
  79. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( listOf( SPLIT_SCREEN_BINDING, SHMOO_INDEX_BINDING, SHMOO_BINDING ) ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup + Decoupled + Easy to customize - Runtime errors - Boiler. Plate.
  80. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( listOf( SPLIT_SCREEN_BINDING, SHMOO_INDEX_BINDING, SHMOO_BINDING ) ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup + Decoupled + Easy to customize - Runtime errors - Boiler. Plate. Kind of a PITA, kind of needed.
  81. interface UiModel Showing Arbitrary UI Models 1. Quick & dirty

    interface 2. Runtime lookup 3. All both of the above
  82. interface UiModel interface ComposeUiModel : UiModel { @Composable fun Content()

    } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup 3. All both of the above
  83. interface UiModel interface ComposeUiModel : UiModel { @Composable fun Content()

    } bindings[uiModel::class] ?.Content(uiModel) ?: Text("What’s a $uiModel?") Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup 3. All both of the above
  84. interface UiModel interface ComposeUiModel : UiModel { @Composable fun Content()

    } bindings[uiModel::class] ?.Content(uiModel) ?: (uiModel as? ComposeUiModel) ?.Content() ?: Text("What’s a $uiModel?") Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup 3. All both of the above
  85. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() // MAGIC? } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup 3. All both of the above
  86. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ShowUi(model) } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup 3. All both of the above
  87. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( A_HANDFUL_OF_CUSTOM_BINDINGS ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup 3. All both of the above
  88. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( A_HANDFUL_OF_CUSTOM_BINDINGS ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup 3. All both of the above
  89. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( A_HANDFUL_OF_CUSTOM_BINDINGS ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup 3. All both of the above
  90. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( A_HANDFUL_OF_CUSTOM_BINDINGS ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup 3. All both of the above
  91. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( A_HANDFUL_OF_CUSTOM_BINDINGS ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup 3. All both of the above + Usually compile time
  92. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( A_HANDFUL_OF_CUSTOM_BINDINGS ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup 3. All both of the above + Usually compile time + Boiler plate is opt in for…
  93. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( A_HANDFUL_OF_CUSTOM_BINDINGS ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup 3. All both of the above + Usually compile time + Boiler plate is opt in for… + Flexible customization
  94. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( A_HANDFUL_OF_CUSTOM_BINDINGS ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup 3. All both of the above + Usually compile time + Boiler plate is opt in for… + Flexible customization
  95. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( A_HANDFUL_OF_CUSTOM_BINDINGS ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup 3. All both of the above + Usually compile time + Boiler plate is opt in for… + Flexible customization Have your cake. Eat your cake.
  96. @Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by

    models.collectAsState() ProvideLocalUiBindings( A_HANDFUL_OF_CUSTOM_BINDINGS ) { ShowUi(model) } } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup 3. All both of the above + Usually compile time + Boiler plate is opt in for… + Flexible customization Have your cake. Eat your cake. Pick two.
  97. Delta Shmoo nick: Charlie born: 12 July 1948 eaten: Yesterday

    Charles Shmoo nick: Charlie born: 30 Aug 1948 Eat Abner Shmoo Baker Shmoo Charlie Shmoo Delta Shmoo Echo Shmoo Foxtrot Shmoo George Shmoo Herman Shmoo
  98. Delta Shmoo nick: Charlie born: 12 July 1948 eaten: Yesterday

    Charles Shmoo nick: Charlie born: 30 Aug 1948 Eat Abner Shmoo Baker Shmoo Charlie Shmoo Delta Shmoo Echo Shmoo Foxtrot Shmoo George Shmoo Herman Shmoo Charlie Shmoo
  99. Delta Shmoo nick: Charlie born: 12 July 1948 eaten: Yesterday

    Abner Shmoo Baker Shmoo Charlie Shmoo Delta Shmoo Echo Shmoo Foxtrot Shmoo George Shmoo Herman Shmoo Delta Shmoo
  100. Delta Shmoo nick: Charlie born: 12 July 1948 eaten: Yesterday

    Charles Shmoo nick: Charlie born: 30 Aug 1948 Eat Abner Shmoo Baker Shmoo Charlie Shmoo Delta Shmoo Echo Shmoo Foxtrot Shmoo George Shmoo Herman Shmoo
  101. Delta Shmoo nick: Charlie born: 12 July 1948 eaten: Yesterday

    Charles Shmoo nick: Charlie born: 30 Aug 1948 Eat Abner Shmoo Baker Shmoo Charlie Shmoo Delta Shmoo Echo Shmoo Foxtrot Shmoo George Shmoo Herman Shmoo 🥳 Navigation! 🥳
  102. Abner Shmoo Baker Shmoo Charlie Shmoo Delta Shmoo Echo Shmoo

    Foxtrot Shmoo George Shmoo Herman Shmoo Charlie Shmoo Charles Shmoo nick: Charlie born: 30 Aug 1948 Eat
  103. Abner Shmoo Baker Shmoo Charlie Shmoo Delta Shmoo Echo Shmoo

    Foxtrot Shmoo George Shmoo Herman Shmoo Delta Shmoo Delta Shmoo nick: Charlie born: 12 July 1948 eaten: Yesterday
  104. Abner Shmoo Baker Shmoo Charlie Shmoo Delta Shmoo Echo Shmoo

    Foxtrot Shmoo George Shmoo Herman Shmoo Charlie Shmoo Charles Shmoo nick: Charlie born: 30 Aug 1948 Eat
  105. @Composable fun <K : Any> Backstack( frames: List<BackstackFrame<K>>, modifier: Modifier

    = Modifier, frameController: FrameController<K> ) { val stateHolder = rememberSaveableScreenStateHolder<K>() frameController.updateBackstack(frames) Box(modifier = modifier.clip(RectangleShape)) { frameController.activeFrames.forEach { (frame, frameControlModifier) -> key(frame.key) { stateHolder.SaveableStateProvider(frame.key) { Box(frameControlModifier) { frame.Content() } } } } }
  106. @Composable fun <K : Any> Backstack( frames: List<BackstackFrame<K>>, modifier: Modifier

    = Modifier, frameController: FrameController<K> ) { val stateHolder = rememberSaveableScreenStateHolder<K>() frameController.updateBackstack(frames) Box(modifier = modifier.clip(RectangleShape)) { frameController.activeFrames.forEach { (frame, frameControlModifier) -> key(frame.key) { stateHolder.SaveableStateProvider(frame.key) { Box(frameControlModifier) { frame.Content() } } } } }
  107. @Composable fun <K : Any> Backstack( frames: List<BackstackFrame<K>>, modifier: Modifier

    = Modifier, frameController: FrameController<K> ) { val stateHolder = rememberSaveableScreenStateHolder<K>() frameController.updateBackstack(frames) Box(modifier = modifier.clip(RectangleShape)) { frameController.activeFrames.forEach { (frame, frameControlModifier) -> key(frame.key) { stateHolder.SaveableStateProvider(frame.key) { Box(frameControlModifier) { frame.Content() } } } } }
  108. data class BackstackModel<K: Any>( val backstackKeys: List<K>, val modelForKey: (K)

    -> UiModel ) : ComposeUiModel { override fun Content() { Backstack(backstackKeys) { ShowUi(modelForKey(it)) } } }
  109. data class BackstackModel<K: Any>( val backstackKeys: List<K>, val modelForKey: (K)

    -> UiModel ) : ComposeUiModel { override fun Content() { Backstack(backstackKeys) { ShowUi(modelForKey(it)) } } } ⚠
  110. data class BackbuttonWrapper( val wrapped: UiModel, val enabled: Boolean =

    true, val onBackPressed: () -> Unit ) : ComposeUiModel { override fun Content() { BackHandler(enabled) { onBackPressed } ShowUi(wrapped) } }
  111. @Composable fun present(): SplitScreenModel { val shmooIndex = repo.shmooIndex().collectAsState(emptyList()) var

    showing by remember { mutableStateOf(-1) } val index = indexPresenter.present( showing, shmooIndex.map { it.name } ) { showing = it } val shmoo = shmooPresenter.present(shmooIndex[showing].id) return SplitScreenModel( first = index, second = shmoo ) }
  112. @Composable fun present(): SplitScreenModel { val shmooIndex = repo.shmooIndex().collectAsState(emptyList()) var

    backstack by remember { mutableStateOf(emptyList<ShmooId>()) } val index = indexPresenter.present( showing, shmooIndex.map { it.name } ) { showing = it } val shmoo = shmooPresenter.present(shmooIndex[showing].id) return SplitScreenModel( first = index, second = shmoo ) }
  113. @Composable fun present(): SplitScreenModel { val shmooIndex = repo.shmooIndex().collectAsState(emptyList()) var

    backstack by remember { mutableStateOf(emptyList<ShmooId>()) } val index = indexPresenter.present( showing, shmooIndex.map { it.name } ) { showing = it } val shmoo = shmooPresenter.present(shmooIndex[showing].id) return SplitScreenModel( first = index, second = shmoo ) }
  114. @Composable fun present(): SplitScreenModel { val shmooIndex = repo.shmooIndex().collectAsState(emptyList()) var

    backstack by remember { mutableStateOf(emptyList<ShmooId>()) } val index = indexPresenter.present( backstack.lastOrNull() ?: -1, shmooIndex.map { it.name } ) { backstack = backstack + shmooIndex[it] } val shmoo = shmooPresenter.present(shmooIndex[showing].id) return SplitScreenModel( first = index, second = shmoo ) }
  115. @Composable fun present(): SplitScreenModel { val shmooIndex = repo.shmooIndex().collectAsState(emptyList()) var

    backstack by remember { mutableStateOf(emptyList<ShmooId>()) } val index = indexPresenter.present( backstack.lastOrNull() ?: -1, shmooIndex.map { it.name } ) { backstack = backstack + shmooIndex[it] } val shmoo = shmooPresenter.present(shmooIndex[showing].id) return SplitScreenModel( first = index, second = shmoo ) }
  116. @Composable fun present(): SplitScreenModel { val shmooIndex = repo.shmooIndex().collectAsState(emptyList()) var

    backstack by remember { mutableStateOf(emptyList<ShmooId>()) } val index = indexPresenter.present( backstack.lastOrNull() ?: -1, shmooIndex.map { it.name } ) { backstack = backstack + shmooIndex[it] } val shmooMap = backstack.associateWith { BackButtonWrapper(shmooPresenter.present(it)) { backstack = backstack.dropLast(1) } } return SplitScreenModel( first = index, second = shmoo ) }
  117. @Composable fun present(): SplitScreenModel { val shmooIndex = repo.shmooIndex().collectAsState(emptyList()) var

    backstack by remember { mutableStateOf(emptyList<ShmooId>()) } val index = indexPresenter.present( backstack.lastOrNull() ?: -1, shmooIndex.map { it.name } ) { backstack = backstack + shmooIndex[it] } val shmooMap = backstack.associateWith { BackButtonWrapper(shmooPresenter.present(it)) { backstack = backstack.dropLast(1) } } return SplitScreenModel( first = index, second = shmoo ) }
  118. @Composable fun present(): SplitScreenModel { val shmooIndex = repo.shmooIndex().collectAsState(emptyList()) var

    backstack by remember { mutableStateOf(emptyList<ShmooId>()) } val index = indexPresenter.present( backstack.lastOrNull() ?: -1, shmooIndex.map { it.name } ) { backstack = backstack + shmooIndex[it] } val shmooMap = backstack.associateWith { BackButtonWrapper(shmooPresenter.present(it)) { backstack = backstack.dropLast(1) } } return SplitScreenModel( first = index, second = shmoo ) }
  119. @Composable fun present(): SplitScreenModel { val shmooIndex = repo.shmooIndex().collectAsState(emptyList()) var

    backstack by remember { mutableStateOf(emptyList<ShmooId>()) } val index = indexPresenter.present( backstack.lastOrNull() ?: -1, shmooIndex.map { it.name } ) { backstack = backstack + shmooIndex[it] } val shmooMap = backstack.associateWith { BackButtonWrapper(shmooPresenter.present(it)) { backstack = backstack.dropLast(1) } } return SplitScreenModel( first = index, second = BackstackModel(backstack) { shmooMap.getValue(it) } ) }
  120. Ongoing conversations •@Composable fun Foo() / Dependency Injection: fight! •Sweet,

    sweet state management outside of view code? •Why does it hurt when I navigate?
  121. Ongoing conversations •@Composable fun Foo() / Dependency Injection: fight! •Sweet,

    sweet state management outside of view code? •Why does it hurt when I navigate? ✔
  122. Ongoing conversations •@Composable fun Foo() / Dependency Injection: fight! •Sweet,

    sweet state management outside of view code? •Why does it hurt when I navigate? ✔ ✔
  123. Ongoing conversations •@Composable fun Foo() / Dependency Injection: fight! •Sweet,

    sweet state management outside of view code? •Why does it hurt when I navigate? ✔ ✔ ✔
  124. Recap •An injected tree of @Composable Presenters can make •a

    tree of @Composable ViewModel structs which
  125. Recap •An injected tree of @Composable Presenters can make •a

    tree of @Composable ViewModel structs which •Molecule can stream through a StateFlow which can
  126. Recap •An injected tree of @Composable Presenters can make •a

    tree of @Composable ViewModel structs which •Molecule can stream through a StateFlow which can •hang off a Jetpack ViewModel to survive config change
  127. Recap •An injected tree of @Composable Presenters can make •a

    tree of @Composable ViewModel structs which •Molecule can stream through a StateFlow which can •hang off a Jetpack ViewModel to survive config change
  128. Recap •An injected tree of @Composable Presenters can make •a

    tree of @Composable ViewModel structs which •Molecule can stream through a StateFlow which can •hang off a Jetpack ViewModel to survive config change And navigation is just a bit more presenter state,
  129. Recap •An injected tree of @Composable Presenters can make •a

    tree of @Composable ViewModel structs which •Molecule can stream through a StateFlow which can •hang off a Jetpack ViewModel to survive config change And navigation is just a bit more presenter state, with a bit of crafty view code.