$30 off During Our Annual Pro Sale. View Details »

Molecule: Using Compose for presentation logic

Chris Horner
September 27, 2022

Molecule: Using Compose for presentation logic

Jetpack Compose can be used for more than just emitting a user interface. Molecule is a library that allows `Flow` or `StateFlow` streams to be built using Compose.

This talk covers:
- Some historical approaches to presentation logic on Android
- Comparing Compose to Rx and Flow APIs
- The benefits Compose can have on readability
- How Molecule helps decouple Compose from Compose UI
- How to build a StateFlow using Compose
- Testing strategies and gotchas

Chris Horner

September 27, 2022
Tweet

More Decks by Chris Horner

Other Decks in Technology

Transcript

  1. @chris_h_codes Molecule Using Compose for presentation logic Chris Horner

  2. Compose. Not Compose UI.

  3. Presentation logic? MV Whatever Model Event

  4. What does Molecule enable? @Composable fun models(events: Flow<Event>): Model {

    // Calculate Model here. } Why is this interesting?
  5. 2017

  6. 2017 RxJava

  7. class MyActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) {

    button.setOnClickListener { } } } 2010
  8. class MyActivity : Activity() { override fun onCreate(savedInstanceState: Bundle?) {

    button.setOnClickListener { object : AsyncTask() { override fun doInBackground(vararg arg: Void?) { // Update "state" somehow? } } } } } 2010
  9. <> Observable<Model> Observable<Event> Observable<Action> Observable<Result> The State of Managing State

    with RxJava https://youtu.be/0IKHxjkgop4 Jake Wharton 2017
  10. 2017 Single.just(input) .flatMap { longRunningOp(it) } .map { it.asSomethingElse() }

    .filter { someCheck(it) } .subscribe { … }
  11. 2017 Single.just(input) .flatMap { longRunningOp(it) } .map { it.asSomethingElse() }

    .filter { someCheck(it) } .subscribe { … } val result = longRunningOp(input) val output = result.asSomethingElse() return if someCheck(output) output else null
  12. 2017 Single.just(input) .flatMap { longRunningOp(it) } .map { it.asSomethingElse() }

    .filter { someCheck(it) } .subscribeOn(Schedulers.Io) .observeOn(Schedulers.Main) .subscribe { … } val result = longRunningOp(input) val output = result.asSomethingElse() return if someCheck(output) output else null
  13. Single Maybe Completable suspend 2017

  14. 2019 fun map(mapper: (T) -> R): Flowable<R> fun flatMapSingle(mapper: (T)

    -> SingleSource<R>): Flowable<R> Mapper Asynchronous Data Streams with Kotlin Flow https://youtu.be/tYcqn48SMT8 Roman Elizarov
  15. 2019 Sync Async fun map(mapper: (T) -> R): Flowable<R> fun

    flatMapSingle(mapper: (T) -> SingleSource<R>): Flowable<R> Mapper Asynchronous Data Streams with Kotlin Flow https://youtu.be/tYcqn48SMT8 Roman Elizarov
  16. fun map(mapper: (T) -> R): Flowable<R> fun flatMapSingle(mapper: (T) ->

    SingleSource<R>): Flowable<R> Mapper 2019 fun filter(predicate: (T) -> Boolean): Flowable<T> fun … 🤯 Predicate Sync Async Sync Async Asynchronous Data Streams with Kotlin Flow https://youtu.be/tYcqn48SMT8 Roman Elizarov Asynchronous Data Streams with Kotlin Flow https://youtu.be/tYcqn48SMT8 Roman Elizarov
  17. flow.map { }

  18. Operator Avoidance startWith(observable) startWith(value) delaySubscription(time) onStart { emitAll(flow) } onStart

    { emit(value) } onStart { delay(time) }
  19. Operator Avoidance RxJava API surface Flow API surface

  20. Operator Avoidance RxJava API surface Flow API surface

  21. Operator Power val queries: Flowable<String> sealed interface Model { object

    Loading : Model data class Loaded( val results: List<Result> ): Model }
  22. Operator Power Mel val queries: Flowable<String> sealed interface Model {

    object Loading : Model data class Loaded( val results: List<Result> ): Model }
  23. Operator Power Mel val queries: Flowable<String> sealed interface Model {

    object Loading : Model data class Loaded( val results: List<Result> ): Model }
  24. Operator Power queries.switchMap { query - > search(query) .delaySubscription(300, TimeUnit.MILLISECONDS)

    .toObservable() .startWith { Model.Loading } }
  25. Operator Power queries.switchMap { query - > search(query) .delaySubscription(300, TimeUnit.MILLISECONDS)

    .toObservable() .startWith { Model.Loading } } queries.transformLatest { query -> emit(Model.Loading) delay(300) emit(search(query)) }
  26. Operator Power queries.switchMap { query - > search(query) .delaySubscription(300, TimeUnit.MILLISECONDS)

    .toObservable() .startWith { Model.Loading } } queries.transformLatest { query -> emit(Model.Loading) delay(300) emit(search(query)) } 1
  27. Operator Power queries.switchMap { query - > search(query) .delaySubscription(300, TimeUnit.MILLISECONDS)

    .toObservable() .startWith { Model.Loading } } queries.transformLatest { query -> emit(Model.Loading) delay(300) emit(search(query)) } 2
  28. Operator Power queries.switchMap { query - > search(query) .delaySubscription(300, TimeUnit.MILLISECONDS)

    .toObservable() .startWith { Model.Loading } } queries.transformLatest { query -> emit(Model.Loading) delay(300) emit(search(query)) } 3
  29. Operator Power queries.switchMap { query - > search(query) .delaySubscription(300, TimeUnit.MILLISECONDS)

    .toObservable() .startWith { Model.Loading } } queries.transformLatest { query -> emit(Model.Loading) delay(300) emit(search(query)) } 1
  30. Operator Power queries.switchMap { query - > search(query) .delaySubscription(300, TimeUnit.MILLISECONDS)

    .toObservable() .startWith { Model.Loading } } queries.transformLatest { query -> emit(Model.Loading) delay(300) emit(search(query)) } 2
  31. Operator Power queries.switchMap { query - > search(query) .delaySubscription(300, TimeUnit.MILLISECONDS)

    .toObservable() .startWith { Model.Loading } } queries.transformLatest { query -> emit(Model.Loading) delay(300) emit(search(query)) } 3
  32. So suspend + Flow wins. End of story? • suspend

    is great because we can write imperative code • Flow has advantages over Rx because it’s powered by coroutines • It still has many operators to learn • It’s still a slightly different way of writing code
  33. None
  34. queries.transformLatest { query -> emit(State.Loading) delay(300) emit(search(query)) }

  35. queries .map { ... } .transformLatest { query -> emit(State.Loading)

    delay(300) emit(search(query)) } .flowOn( .. . ) .onEmpty { ... } .catch { .. . } .scan(emptyList()) { list, items -> list + items .map { ... } .zip() } .flatMapMerge(concurrency = 4) { .. . } .distinctUntilChanged()
  36. What are we doing? We’re composing a model in response

    to events and values changing over time.
  37. What if we actually used Compose to build that model?

    Molecule asks the question: What are we doing? We’re composing a model in response to events and values changing over time.
  38. Column Text Row Column Image Text Text

  39. Column Text Row Column Image Text Text @Composable fun UserInterface(model:

    Model) { }
  40. Column Text Row Box Image Image Icon @Composable fun UserInterface(model:

    Model) { }
  41. Column Text Row Image @Composable fun UserInterface(model: Model) { var

    someState: Int by remember { mutableStateOf(1) } } Box Image Icon
  42. @Composable fun UserInterface(model: Model) { val someState: MutableState<Int> = remember

    { mutableStateOf(1) } }
  43. @Composable fun UserInterface(model: Model) { val state: State<Int> } Reactive!

  44. @Composable fun UserInterface(model: Model) { var someState: Int by remember

    { mutableStateOf(1) } }
  45. @Composable fun UserInterface() { var someState: Int by remember {

    mutableStateOf(1) } LaunchedEffect(Unit) { while (true) { delay(1_000) someState ++ } } }
  46. @Composable fun UserInterface() { var someState: Int by remember {

    mutableStateOf(1) } LaunchedEffect(Unit) { while (true) { delay(1_000) someState ++ } } Text(someState.toString()) }
  47. @Composable fun UserInterface() { var someState: Int by remember {

    mutableStateOf(1) } LaunchedEffect(Unit) { while (true) { delay(1_000) someState ++ } } Text(someState.toString()) } Presentation logic
  48. @Composable fun UserInterface() { } Flow Example

  49. @Composable fun UserInterface() { } sealed interface Model { object

    Loading : Model data class Loaded( val results: List<Result> ) : Model }
  50. @Composable fun UserInterface(queries: Flow<String>) { }

  51. @Composable fun UserInterface(queries: Flow<String>) { val models = queries .onStart

    { emit("") } }
  52. @Composable fun UserInterface(queries: Flow<String>) { val models = queries .onStart

    { emit("") } .transformLatest { query -> emit(Model.Loading) } }
  53. @Composable fun UserInterface(queries: Flow<String>) { val models = queries .onStart

    { emit("") } .transformLatest { query -> emit(Model.Loading) delay(300) val results = search(query) emit(Model.Loaded(results)) } }
  54. e terface(queries: Flow<String>) { ls = queries rt { emit("")

    } formLatest { query -> (Model.Loading) y(300) results = search(query) (Model.Loaded(results)) interface Database { fun observeDataset(): Flow<Dataset> }
  55. e terface(queries: Flow<String>) { ls = queries rt { emit("")

    } formLatest { query -> (Model.Loading) y(300) results = search(query, dataset) (Model.Loaded(results)) interface Database { fun observeDataset(): Flow<Dataset> }
  56. @Composable fun UserInterface(queries: Flow<String>) { val models = queries .onStart

    { emit("") } .transformLatest { query -> emit(Model.Loading) delay(300) val results = search(query, dataset) emit(Model.Loaded(results)) } }
  57. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { val models =

    queries .onStart { emit("") } .transformLatest { query -> emit(Model.Loading) delay(300) val results = search(query, dataset) emit(Model.Loaded(results)) } }
  58. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { combineTransform(queries, datasets) val

    models = queries .onStart { emit("") } .transformLatest { query -> emit(Model.Loading) delay(300) val results = search(query, dataset) emit(Model.Loaded(results)) } } ?
  59. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { combineTransformLatest(queries, dataset) val

    models = queries .onStart { emit("") } .transformLatest { query -> emit(Model.Loading) delay(300) val results = search(query, dataset) emit(Model.Loaded(results)) } } ?
  60. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { val models =

    datasets.flatMapLatest { dataset -> queries .onStart { emit("") } .transformLatest { query - > emit(Model.Loading) delay(300) val results = search(query, dataset) emit(Model.Loaded(results)) } } }
  61. What if we used Compose?

  62. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { val models =

    datasets.flatMapLatest { dataset -> queries .onStart { emit("") } .transformLatest { query - > emit(Model.Loading) delay(300) val results = search(query, dataset) emit(Model.Loaded(results)) } } } What if we used Compose?
  63. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { val query by

    queries.collectAsState("") val models = dataset s​ .flatMapLatest { dataset -> queries .onStart { emit("") } .transformLatest { query - > emit(Model.Loading) delay(300) val results = search(query, dataset) emit(Model.Loaded(results)) } } }
  64. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { val query by

    queries.collectAsState("") val dataset by datasets.collectAsState(InitialSet) val models = dataset s​ .flatMapLatest { dataset -> queries .onStart { emit("") } .transformLatest { query - > emit(Model.Loading) delay(300) val results = search(query, dataset) emit(Model.Loaded(results)) } } }
  65. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { val query by

    queries.collectAsState("") val dataset by datasets.collectAsState(InitialSet) var model by remember { mutableStateOf(Model.Loaded(emptyList())) } val models = dataset s​ .flatMapLatest { dataset -> queries .onStart { emit("") } .transformLatest { query - > emit(Model.Loading) delay(300) val results = search(query, dataset) emit(Model.Loaded(results)) } } }
  66. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { val query by

    queries.collectAsState("") val dataset by datasets.collectAsState(InitialSet) var model by remember { mutableStateOf(Model.Loaded(emptyList())) } val models = dataset s​ .flatMapLatest { dataset -> queries .onStart { emit("") } .transformLatest { query - > emit(Model.Loading) delay(300) val results = search(query, dataset) emit(Model.Loaded(results )​ ) } } }
  67. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { val query by

    queries.collectAsState("") val dataset by datasets.collectAsState(InitialSet) var model by remember { mutableStateOf(Model.Loaded(emptyList())) } LaunchedEffect(query, dataset) { emit(Model.Loading) delay(300) val results = search(query, dataset) emit(Model.Loaded(results )​ ) } }
  68. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { val query by

    queries.collectAsState("") val dataset by datasets.collectAsState(InitialSet) var model by remember { mutableStateOf(Model.Loaded(emptyList())) } LaunchedEffect(query, dataset) { model = Model.Loading delay(300) val results = search(query, dataset) model = Model.Loaded(results) } } Still reactive!
  69. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { } • flatMap

    • scan • debounce • zip
  70. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { } • remember

    • LaunchedEffect • collectAsState • mutableStateOf
  71. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { } • remember

    • LaunchedEffect • collectAsState • mutableStateOf • if, else, when, for, while
  72. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { val query by

    queries.collectAsState("") val dataset by datasets.collectAsState(InitialSet) var model by remember { mutableStateOf(Model.Loaded(emptyList())) } LaunchedEffect(query, dataset) { model = Model.Loading delay(300) val results = search(query, dataset) model = Model.Loaded(results) } }
  73. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { val query by

    queries.collectAsState("") val dataset by datasets.collectAsState(InitialSet) var model by remember { mutableStateOf(Model.Loaded(emptyList())) } LaunchedEffect(query, dataset) { model = ​ Model.Loadin g​ delay(300) val results = search(query, dataset) model = ​ Model.Loaded(results) } Column { // .. . } }
  74. @Composable fun UserInterface(queries: Flow<String>, datasets: Flow<Dataset>) { val query by

    queries.collectAsState("") val dataset by datasets.collectAsState(InitialSet) var model by remember { mutableStateOf(Model.Loaded(emptyList())) } LaunchedEffect(query, dataset) { model = Model.Loading delay(300) val results = search(query, dataset) model = Model.Loaded(results) } Column { // .. . } } Presentation logic
  75. Why do we need Molecule?

  76. Why do we need Molecule? presentation-logic ui @Composable StateFlow<State> junit

  77. Why do we need Molecule? presentation-logic ui @Composable StateFlow<State> junit

  78. How do we use it?

  79. How do we use it? apply plugin: 'app.cash.molecule'

  80. How do we use it? Flow<Model> StateFlow<Model>

  81. How do we use it? Flow<Model> StateFlow<Model>

  82. How do we use it? val scope = CoroutineScope(AndroidUiDispatcher.Main)

  83. How do we use it? val scope = CoroutineScope(AndroidUiDispatcher.Main) val

    models: StateFlow<Model> = scope.launchMolecule() { }
  84. How do we use it? val scope = CoroutineScope(AndroidUiDispatcher.Main) val

    models: StateFlow<Model> = scope.launchMolecule() { var model by remember { mutableStateOf(Model(A)) } }
  85. val scope = CoroutineScope(AndroidUiDispatcher.Main) val models: StateFlow<Model> = scope.launchMolecule() {

    var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() } }
  86. val scope = CoroutineScope(AndroidUiDispatcher.Main) val models: StateFlow<Model> = scope.launchMolecule() {

    var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() } model }
  87. val scope = CoroutineScope(AndroidUiDispatcher.Main) val models: StateFlow<Model> = scope.launchMolecule() {

    var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() } model } 1
  88. val scope = CoroutineScope(AndroidUiDispatcher.Main) val models: StateFlow<Model> = scope.launchMolecule() {

    var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() } model } 2
  89. val scope = CoroutineScope(AndroidUiDispatcher.Main) val models: StateFlow<Model> = scope.launchMolecule() {

    var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() } model } 3 A Emissions
  90. val scope = CoroutineScope(AndroidUiDispatcher.Main) val models: StateFlow<Model> = scope.launchMolecule() {

    var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() } model } 4 A Emissions
  91. val scope = CoroutineScope(AndroidUiDispatcher.Main) val models: StateFlow<Model> = scope.launchMolecule() {

    var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() } model } 5 A Emissions
  92. 6 A B val scope = CoroutineScope(AndroidUiDispatcher.Main) val models: StateFlow<Model>

    = scope.launchMolecule() { var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() } model } Emissions
  93. val scope = CoroutineScope(AndroidUiDispatcher.Main) val models: StateFlow<Model> = scope.launchMolecule() {

    var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() model = Model(C) } model } A B Emissions ?
  94. val scope = CoroutineScope(AndroidUiDispatcher.Main) val models: StateFlow<Model> = scope.launchMolecule() {

    var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() model = Model(C) } model } A B C Emissions
  95. val scope = CoroutineScope(AndroidUiDispatcher.Main) val models: StateFlow<Model> = scope.launchMolecule() {

    model } • Some State is invalidated • A MonotonicFrameClock ticks For a new emission, two things must happen Emissions
  96. val scope = CoroutineScope(AndroidUiDispatcher.Main) val models: StateFlow<Model> = scope.launchMolecule() {

    var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() model = Model(C) } model } A B C Emissions
  97. A B C Emissions val scope = CoroutineScope(AndroidUiDispatcher.Main) val models:

    StateFlow<Model> = scope.launchMolecule() { var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() model = Model(C) } model }
  98. A B C Frame tick Emissions val scope = CoroutineScope(AndroidUiDispatcher.Main)

    val models: StateFlow<Model> = scope.launchMolecule() { var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() model = Model(C) } model }
  99. A B C Emissions val scope = CoroutineScope(AndroidUiDispatcher.Main) val models:

    StateFlow<Model> = scope.launchMolecule() { var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() model = Model(C) } model }
  100. A B C Emissions C val scope = CoroutineScope(AndroidUiDispatcher.Main) val

    models: StateFlow<Model> = scope.launchMolecule() { var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() model = Model(C) } model }
  101. A B C Emissions val scope = CoroutineScope(AndroidUiDispatcher.Main) val models:

    StateFlow<Model> = scope.launchMolecule() { var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() model = Model(C) } model }
  102. val scope = CoroutineScope(AndroidUiDispatcher.Main) val models: StateFlow<Model> = scope.launchMolecule() {

    var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() model = Model(C) } model }
  103. val scope = CoroutineScope(AndroidUiDispatcher.Main) val models: StateFlow<Model> = scope.launchMolecule( clock

    = RecompositionClock.ContextClock ) { var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() model = Model(C) } model }
  104. val scope = CoroutineScope(AndroidUiDispatcher.Main) val models: StateFlow<Model> = scope.launchMolecule( clock

    = RecompositionClock.ContextClock ) { var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() model = Model(C) } model }
  105. val scope = CoroutineScope (​ ) val models: StateFlow<Model> =

    scope.launchMolecule( clock = RecompositionClock.ContextClock ) { var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() model = Model(C) } model }
  106. fun unitTest() = runBlocking { val scope = this val

    models: StateFlow<Model> = scope.launchMolecule( clock = RecompositionClock.ContextClock ) { var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() model = Model(C) } model }
  107. fun unitTest() = runBlocking { val scope = this val

    models: StateFlow<Model> = scope.launchMolecule( clock = RecompositionClock.Immediate ) { var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() model = Model(C) } model }
  108. fun unitTest() = runBlocking { val scope = this val

    models: StateFlow<Model> = scope.launchMolecule( clock = RecompositionClock.Immediate ) { var model by remember { mutableStateOf(Model(A)) } LaunchedEffect(Unit) { model = loadModelB() model = Model(C) } model }
  109. ContextClock Immediate • Emissions match Android’s built in frame clock

    • Need to control emissions using BroadcastFrameClock • Unit tests requiring time manipulation Choosing a RecompositionClock • Frames tick automatically when snapshot state changes • Unit tests that don’t require time manipulation
  110. You only get one emission per frame

  111. Realistic example

  112. Realistic example class Presenter { @Composable fun models(events: Flow<Event>): Model

    { // Calculate Model here. } }
  113. Realistic example class Presenter { @Composable fun models(events: Flow<Event>): Model

    { // Calculate Model here. } }
  114. class SearchPresenter : Presenter<Event, Model> { @Composable fun models(events: Flow<Event>):

    Model { // Calculate Model here. } } Realistic example
  115. sealed interface Event { data class EnterText(val text: String) :

    Event } sealed interface Model { object Loading : Model data class Loaded( val results: List<Result> = emptyList() ) : Model }
  116. class SearchPresenter : Presenter<Event, Model> { @Composable fun models(events: Flow<Event>):

    Model { // Calculate Model here. } } Realistic example
  117. class SearchPresenter( val service: Service ) : Presenter<Event, Model> {

    @Composable fun models(events: Flow<Event>): Model { // Calculate Model here. } } Realistic example
  118. class SearchPresenter( val service: Service ) : Presenter<Event, Model> {

    @Composable fun models(events: Flow<Event>): Model { val modelState = remember { mutableStateOf(Model.Loaded()) } } } Realistic example
  119. class SearchPresenter( val service: Service ) : Presenter<Event, Model> {

    @Composable fun models(events: Flow<Event>): Model { val modelState: MutableState<Model> = remember { mutableStateOf(Mod } } Realistic example
  120. class SearchPresenter( val service: Service ) : Presenter<Event, Model> {

    @Composable fun models(events: Flow<Event>): Model { val modelState = remember { mutableStateOf(Model.Loaded()) } } } Realistic example
  121. class SearchPresenter( val service: Service ) : Presenter<Event, Model> {

    @Composable fun models(events: Flow<Event>): Model { val modelState = remember { mutableStateOf(Model.Loaded()) } return modelState.value } } Realistic example
  122. class SearchPresenter( val service: Service ) : Presenter<Event, Model> {

    @Composable fun models(events: Flow<Event>): Model { val modelState = remember { mutableStateOf(Model.Loaded()) } LaunchedEffect(Unit) { events.collect { event -> when (event) { ... } } } return modelState.value } }
  123. class SearchPresenter( val service: Service ) : Presenter<Event, Model> {

    @Composable fun models(events: Flow<Event>): Model { val modelState = remember { mutableStateOf(Model.Loaded()) } var query by remember { mutableStateOf("") } LaunchedEffect(Unit) { events.collect { event -> when (event) { EnterText -> query = event.text } } } return modelState.value }
  124. @Composable fun models(events: Flow<Event>): Model { val modelState = remember

    { mutableStateOf(Model.Loaded()) } var query by remember { mutableStateOf("") } LaunchedEffect(Unit) { events.collect { event -> when (event) { EnterText -> query = event.text } } } LaunchedEffect(query) { runSearch(query, modelState) } return modelState.value }
  125. private suspend fun runSearch( query: String, state: MutableState<Model>, ) {

    state.value = Model.Loading val results = service.search(query) state.value = Model.Loaded(results) } LaunchedEffect(query) { runSearch(query, modelState) }
  126. private suspend fun runSearch( query: String, state: MutableState<Model>, ) {

    state.value = Model.Loading val results = service.search(query) state.value = Model.Loaded(results) } LaunchedEffect(query) { runSearch(query, modelState) }
  127. private suspend fun runSearch( query: String, state: MutableState<Model>, ) {

    state.value = Model.Loading val results = service.search(query) state.value = Model.Loaded(results) } LaunchedEffect(query) { runSearch(query, modelState) }
  128. Writing a test cashapp/turbine

  129. cashapp/turbine Writing a test flowOf("one", "two").test { assertEquals("one", awaitItem()) assertEquals("two",

    awaitItem()) awaitComplete() }
  130. Writing a test @Test fun `entering text runs search`() =

    runBlocking { }
  131. Writing a test @Test fun `entering text runs search`() =

    runBlocking { val events = MutableSharedFlow<Event>(replay = 1) }
  132. Writing a test @Test fun `entering text runs search`() =

    runBlocking { val events = MutableSharedFlow<Event>(replay = 1) launchMolecule(RecompositionClock.Immediate) { presenter.models(events) }.test { } }
  133. Writing a test @Test fun `entering text runs search`() =

    runBlocking { val events = MutableSharedFlow<Event>(replay = 1) launchMolecule(RecompositionClock.Immediate) { presenter.models(events) }.test { assertThat(awaitItem()).isEqualTo(Model.Loading) } }
  134. Writing a test @Test fun `entering text runs search`() =

    runBlocking { val events = MutableSharedFlow<Event>(replay = 1) launchMolecule(RecompositionClock.Immediate) { presenter.models(events) }.test { assertThat(awaitItem()).isEqualTo(Model.Loading) events.emit(Event.EnterText("query")) fakeService.setResults( .. . ) } }
  135. Writing a test @Test fun `entering text runs search`() =

    runBlocking { val events = MutableSharedFlow<Event>(replay = 1) launchMolecule(RecompositionClock.Immediate) { presenter.models(events) }.test { assertThat(awaitItem()).isEqualTo(Model.Loading) events.emit(Event.EnterText("query")) fakeService.setResults( .. . ) assertThat(awaitItem()).isEqualTo(Model.Loaded( .. . )) } }
  136. Writing a test @Test fun `entering text runs search`() =

    runBlocking { presenter.test { assertThat(awaitItem()).isEqualTo(Model.Loading) sendEvent(Event.EnterText("query")) fakeService.setResults( .. . ) assertThat(awaitItem()).isEqualTo(Model.Loaded( .. . )) } }
  137. Takeaways

  138. Takeaways • Compose manages a tree of nodes - it

    doesn’t have to be UI • Managing state involves tying together streams • Compose can do to streams what suspend did to Single • State is reactive. Think of it like a stream • There’s still a learning curve, but it’s less steep compared to Rx/Flow • Tricks you’ve learnt in Compose UI work in Molecule too
  139. Interested in more? Building StateFlows with Jetpack Compose droidcon.com/2022/09/29/building-stateflows-in-android-with-jetpack-compose Mohit

    Sarveiya Demystifying Molecule droidcon.com/2022/09/29/demystifying-molecule-running-your-own-compositions-for-fun-and-profit Bill Phillips & Ash Davies Opening the Shutter on Snapshots droidcon.com/2022/09/29/opening-the-shutter-on-snapshots Zach Klippenstein
  140. chris_h_codes github.com/cashapp/molecule Molecule Using Compose for presentation logic