Share
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
Transcript
200 slides
-
Ongoing conversations •@Composable fun Foo() / Dependency Injection: fight! •Sweet,
sweet state management outside of view code? -
Ongoing conversations •@Composable fun Foo() / Dependency Injection: fight! •Sweet,
sweet state management outside of view code? •Why does it hurt when I navigate? -
Ray Ryan Navigation & DI in Compose Molecular Theory of
Presenter Composition -
Protocol Buffers Wire shmoo.com interface ShmooService Retrofit interface ShmooDao Room
/ SQLDelight @Composable fun Shmoo( model: ShmooModel ) -
Protocol Buffers Wire shmoo.com interface ShmooService Retrofit interface ShmooRepo Room
/ SQLDelight @Composable fun Shmoo( model: ShmooModel ) -
Protocol Buffers Wire shmoo.com interface ShmooService Retrofit interface ShmooRepo SQLDelight
Dagger + Anvil @Inject ShmooRepo @Composable fun Shmoo( model: ShmooModel ) -
Protocol Buffers Wire shmoo.com interface ShmooService Retrofit SQLDelight Dagger +
Anvil ? interface ShmooRepo @Composable fun Shmoo( model: ShmooModel ) -
? interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil
@Composable fun Shmoo( model: ShmooModel ) -
? interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil
@Composable fun Shmoo( model: ShmooModel ) -
? interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil
@Composable fun Shmoo( model: ShmooModel ) -
? interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil
@Composable fun Shmoo( model: ShmooModel ) -
? interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil
@Composable fun Shmoo( model: ShmooModel ) -
? ViewModel interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger +
Anvil @Composable fun Shmoo( model: ShmooModel ) -
? ViewModel Lifecycle interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger
+ Anvil @Composable fun Shmoo( model: ShmooModel ) -
? ViewModel Lifecycle Navigation interface ShmooServ Retrofi interface ShmooRepo SQLDelight
Dagger + Anvil @Composable fun Shmoo( model: ShmooModel ) -
? ViewModel Lifecycle Navigation Fragment interface ShmooServ Retrofi interface ShmooRepo
SQLDelight Dagger + Anvil @Composable fun Shmoo( model: ShmooModel ) -
? ViewModel Lifecycle Navigation Fragment CoroutineScope interface ShmooServ Retrofi interface
ShmooRepo SQLDelight Dagger + Anvil @Composable fun Shmoo( model: ShmooModel ) -
? ViewModel Lifecycle Navigation Fragment CoroutineScope Suspend interface ShmooServ Retrofi
interface ShmooRepo SQLDelight Dagger + Anvil @Composable fun Shmoo( model: ShmooModel ) -
? ViewModel Lifecycle Navigation Fragment CoroutineScope Suspend StateFlow interface ShmooServ
Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable fun Shmoo( model: ShmooModel ) -
? ViewModel Lifecycle Navigation Fragment CoroutineScope Suspend StateFlow LaunchEffect interface
ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable fun Shmoo( model: ShmooModel ) -
? ViewModel Lifecycle Navigation Fragment CoroutineScope Suspend StateFlow LaunchEffect remember
{ } interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable fun Shmoo( model: ShmooModel ) -
? ViewModel Lifecycle Navigation Fragment CoroutineScope Suspend StateFlow LaunchEffect remember
{ } MutableState interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable fun Shmoo( model: ShmooModel ) -
? ViewModel Lifecycle Navigation Fragment CoroutineScope Suspend StateFlow LaunchEffect remember
{ } MutableState interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable fun Shmoo( model: ShmooModel ) -
Presenter Props Mode l output event repo.getItem(i d) .collect {
… } @Inject Repo -
? interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil
@Composable fun Shmoo( model: ShmooModel ) -
interface ShmooServ Retrofi interface ShmooRepo SQLDelight @Composable fun Shmoo( model:
ShmooModel ) Shmoo Presenter Dagger + Anvil -
interface ShmooServ Retrofi interface ShmooRepo SQLDelight @Composable fun Shmoo( model:
ShmooModel ) Shmoo Presenter StateFlow< ShmooModel > Dagger + Anvil -
interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable
fun AppUi( models: StateFlow<ShmooModel> ) { val model by models.collectAsState() Shmoo(model) } Shmoo Presenter Workflow -
Presenter Props Model event repo.getItem(id ) .collect { … }
@Inject Repo 👈 -
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 { … } -
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 { … } -
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 { … } -
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 { … } -
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 { … } -
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 { … } -
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 { … } -
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 { … } -
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 { … } -
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 { } } 👈 -
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 { … } -
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 { … } -
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 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() } } -
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() } } -
interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable
fun AppUi( models: StateFlow<ShmooModel> ) { val model by models.collectAsState() Shmoo(model) } Shmoo Presenter Workflow -
interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable
fun AppUi( models: StateFlow<ShmooModel> ) { val model by models.collectAsState() Shmoo(model) } Shmoo Presenter 🥳 Molecule 🥳 -
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 -
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 -
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 ) -
Shmoo Browser Presenter val shmooIndex by repo.getIndex() .collectAsState() @Inject Index
Presenter @Inject ShmooRepo @Inject Shmoo Presenter -
Shmoo Browser Presenter val shmooIndex by repo.getIndex() .collectAsState() @Inject Index
Presenter var selection by remember { mutableState(-1) } @Inject ShmooRepo @Inject Shmoo Presenter -
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 -
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 -
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 -
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 -
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 -
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) } -
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 -
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 -
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 -
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 -
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 -
interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable
fun AppUi( models: StateFlow<ShmooModel> ) { val model by models.collectAsState() Shmoo(model) } Shmoo Presenter -
interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable
fun AppUi( models: StateFlow<ShmooModel> ) { val model by models.collectAsState() Shmoo(model) } Shmoo Presenter 👈 -
interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable
fun AppUi( models: StateFlow<ShmooModel> ) { val model by models.collectAsState() Shmoo(model) } Shmoo Presenter 👈 👈 -
interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable
fun AppUi( models: StateFlow<ShmooModel> ) { val model by models.collectAsState() Shmoo(model) } Shmoo Presenter 👈 👈 👈 -
interface ShmooServ Retrofi interface ShmooRepo SQLDelight Dagger + Anvil @Composable
fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by models.collectAsState() // MAGIC? } Shmoo Browser Presenter 👈 👈 👈 -
interface UiModel { @Composable fun Content() } Showing Arbitrary UI
Models 1. Quick & dirty interface -
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 -
@Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by
models.collectAsState() // MAGIC? } Showing Arbitrary UI Models 1. Quick & dirty interface -
@Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by
models.collectAsState() model.Content() } Showing Arbitrary UI Models 1. Quick & dirty interface -
@Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by
models.collectAsState() model.Content() } Showing Arbitrary UI Models 1. Quick & dirty interface -
@Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by
models.collectAsState() model.Content() } Showing Arbitrary UI Models 1. Quick & dirty interface -
@Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by
models.collectAsState() model.Content() } Showing Arbitrary UI Models 1. Quick & dirty interface -
@Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by
models.collectAsState() model.Content() } + Simple Showing Arbitrary UI Models 1. Quick & dirty interface -
@Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by
models.collectAsState() model.Content() } + Simple + Compiler enforced Showing Arbitrary UI Models 1. Quick & dirty interface -
@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 -
@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 -
@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 -
@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 -
interface UiModel Showing Arbitrary UI Models 1. Quick & dirty
interface 2. Runtime lookup -
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 -
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 -
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 -
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 -
data class SplitScreenModel( val first: UiModel, val second: UiModel )
: UiModel Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup -
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 -
@Composable fun AppUi( models: StateFlow<SplitScreenModel> ) { val model by
models.collectAsState() // MAGIC? } Showing Arbitrary UI Models 1. Quick & dirty interface 2. Runtime lookup -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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. -
@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. -
@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. -
interface UiModel Showing Arbitrary UI Models 1. Quick & dirty
interface 2. Runtime lookup 3. All both of the above -
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 -
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 -
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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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 -
@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… -
@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 -
@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 -
@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. -
@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. -
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 -
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 -
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 -
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 -
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! 🥳 -
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 -
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 -
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 -
@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() } } } } } -
@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() } } } } } -
@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() } } } } } -
data class BackstackModel<K: Any>( val backstackKeys: List<K>, val modelForKey: (K)
-> UiModel ) : ComposeUiModel { override fun Content() { Backstack(backstackKeys) { ShowUi(modelForKey(it)) } } } -
data class BackstackModel<K: Any>( val backstackKeys: List<K>, val modelForKey: (K)
-> UiModel ) : ComposeUiModel { override fun Content() { Backstack(backstackKeys) { ShowUi(modelForKey(it)) } } } ⚠ -
data class BackbuttonWrapper( val wrapped: UiModel, val enabled: Boolean =
true, val onBackPressed: () -> Unit ) : ComposeUiModel { override fun Content() { BackHandler(enabled) { onBackPressed } ShowUi(wrapped) } } -
@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 ) } -
@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 ) } -
@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 ) } -
@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 ) } -
@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 ) } -
@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 ) } -
@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 ) } -
@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 ) } -
@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) } ) } -
Ongoing conversations •@Composable fun Foo() / Dependency Injection: fight! •Sweet,
sweet state management outside of view code? •Why does it hurt when I navigate? -
Ongoing conversations •@Composable fun Foo() / Dependency Injection: fight! •Sweet,
sweet state management outside of view code? •Why does it hurt when I navigate? ✔ -
Ongoing conversations •@Composable fun Foo() / Dependency Injection: fight! •Sweet,
sweet state management outside of view code? •Why does it hurt when I navigate? ✔ ✔ -
Ongoing conversations •@Composable fun Foo() / Dependency Injection: fight! •Sweet,
sweet state management outside of view code? •Why does it hurt when I navigate? ✔ ✔ ✔ -
Recap •An injected tree of @Composable Presenters can make •a
tree of @Composable ViewModel structs which -
Recap •An injected tree of @Composable Presenters can make •a
tree of @Composable ViewModel structs which •Molecule can stream through a StateFlow which can -
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 -
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 -
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, -
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.