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

Modern Compose Architecture with Circuit

Modern Compose Architecture with Circuit

Avatar for Zac Sweers

Zac Sweers

April 14, 2023
Tweet

More Decks by Zac Sweers

Other Decks in Programming

Transcript

  1. Compose • React-style declarative UI framework • Kotlin- fi rst

    • Originally developed for Android • Two parts • Compose compiler/runtime • Compose UI @Composable fun Example() { Text("Hello World!") }
  2. Compose Architecture @Composable fun Example() { val viewModel by viewModel<ExampleViewModel>()

    val text by viewModel.textF l ow().collectAsState() Text(text) }z
  3. Compose Architecture @Composable fun Example() { val viewModel by viewModel<ExampleViewModel>()

    val text by viewModel.textF l ow().collectAsState() Text(text) }
  4. Compose Architecture @Composable fun Example() { val viewModel by viewModel<ExampleViewModel>()

    val text by viewModel.textF l ow().collectAsState() Text(text) }
  5. Architecture class ExamplePresenter { fun state() : StateF l ow<State>

    } @Composable fun Example(state: State) { Text(state.text) }
  6. class ExamplePresenter { fun state( events: F l ow<Event> )

    : StateF l ow<State> } @Composable fun Example( state: State, eventSink: (Event) - > Unit ) { Text(state.text) Button(onClick = { eventSink(Click) }) } Architecture
  7. class ExamplePresenter { fun state( events: F l ow<Event> )

    : StateF l ow<State> } @Composable fun Example( state: State, eventSink: (Event) - > Unit ) Architecture
  8. class ExamplePresenter { fun state( events: F l ow<Event> )

    : StateF l ow<State> } @Composable fun Example( state: State, eventSink: (Event) - > Unit ) Architecture '22
  9. Circuit • Compose- fi rst, compose all the way down

    • Keyed by "Screen"s • UDF- fi rst • Inspired by Cash App's Broadway architecture & others • Multiplatform • DI-oriented https://github.com/slackhq/circuit
  10. Circuit class CounterPresenter { fun state( events: F l ow<Event>

    ) : StateF l ow<State> } data class State(val count: Int)
  11. Circuit class CounterPresenter { private val count = MutableStateF l

    ow(State(0)) fun state( scope: CoroutineScope, events: F l ow<Event> ) : StateF l ow<State> }
  12. Circuit class CounterPresenter { private val count = MutableStateF l

    ow(State(0)) fun state( scope: CoroutineScope, events: F l ow<Event> ) : StateF l ow<State> { return count } }
  13. Circuit class CounterPresenter { private val count = MutableStateF l

    ow(State(0)) fun state( scope: CoroutineScope, events: F l ow<Event> ) : StateF l ow<State> { scope.launch { events.collect { count.emit(State(count.value.count + + )) } } return count } }
  14. Circuit class CounterPresenter { @Composable fun state() : State {

    var count by remember { mutableStateOf(0) } return State(count) { event - > count + + } } }
  15. Circuit class CounterPresenter { @Composable fun state() : State {

    var count by rememberRetained { mutableStateOf(0) } return State(count) { count + + } } }
  16. Circuit class CounterPresenter { @Composable fun state() : State {

    var count by rememberSaveable { mutableStateOf(0) } return State(count) { count + + } } }
  17. Circuit class CounterPresenter : Presenter<State> { @Composable override fun present()

    : State { var count by rememberSaveable { mutableStateOf(0) } return State(count) { count + + } } }
  18. Circuit interface Presenter<UiState> { @Composable fun present() : UiState fun

    interface Factory { fun create( screen: Screen, navigator: Navigator, context: CircuitContext ) : Presenter < * > ? } }
  19. Circuit interface Presenter<UiState> { @Composable fun present() : UiState fun

    interface Factory { fun create( screen: Screen, navigator: Navigator, context: CircuitContext ) : Presenter < * > ? } } interface Ui<UiState> { @Composable fun Content( state: UiState, modif i er: Modif i er ) fun interface Factory { fun create( screen: Screen, context: CircuitContext ) : Ui < * > ? } }
  20. Circuit interface Presenter<UiState> { @Composable fun present() : UiState fun

    interface Factory { fun create( screen: Screen, navigator: Navigator, context: CircuitContext ) : Presenter < * > ? } } interface Ui<UiState> { @Composable fun Content( state: UiState, modif i er: Modif i er ) fun interface Factory { fun create( screen: Screen, context: CircuitContext ) : Ui < * > ? } }
  21. Circuit class CounterPresenter @AssistedInject constructor( @Assisted private val screen: CounterScreen

    ) : Presenter<State> { @AssistedFactory interface Factory { fun create(screen: CounterScreen) : CounterPresenter } }
  22. Circuit interface Presenter<UiState> { @Composable fun present() : UiState fun

    interface Factory { fun create( screen: Screen, navigator: Navigator, context: CircuitContext ) : Presenter < * > ? } }
  23. Circuit interface Presenter<UiState> { @Composable fun present() : UiState fun

    interface Factory { fun create( screen: Screen, navigator: Navigator, context: CircuitContext ) : Presenter < * > ? } }
  24. Navigation interface Navigator { fun goTo(screen: Screen) fun pop() :

    Screen? fun resetRoot(newRoot: Screen) : List<Screen> }
  25. Navigation interface Navigator { fun goTo(screen: Screen) fun pop() :

    Screen? fun resetRoot(newRoot: Screen) : List<Screen> }
  26. Navigation interface Navigator { fun goTo(screen: Screen) fun pop() :

    Screen? fun resetRoot(newRoot: Screen) : List<Screen> }
  27. Navigation interface Navigator { fun goTo(screen: Screen) fun pop() :

    Screen? fun resetRoot(newRoot: Screen) : List<Screen> }
  28. Navigation val backstack = rememberSaveableBackStack { push(HomeScreen) } val navigator

    = rememberCircuitNavigator(backstack) / / . . . NavigableCircuitContent(navigator, backstack) gist.github.com/adamp/17b4e5cfafc7d44a0023dc2fbdb972e8
  29. Circuit class CounterPresenter @AssistedInject constructor( @Assisted private val screen: CounterScreen,

    @Assisted private val navigator: Navigator, ) : Presenter<State> { @Composable override fun present() : State { } }
  30. Circuit class CounterPresenter @AssistedInject constructor( @Assisted private val screen: CounterScreen,

    @Assisted private val navigator: Navigator, ) : Presenter<State> { @Composable override fun present() : State { navigator.goTo(LoginScreen) } }
  31. Circuit interface Presenter<UiState> { @Composable fun present() : UiState fun

    interface Factory { fun create( screen: Screen, navigator: Navigator, context: CircuitContext ) : Presenter < * > ? } }
  32. Circuit interface Presenter<UiState> { @Composable fun present() : UiState fun

    interface Factory { fun create( screen: Screen, navigator: Navigator, context: CircuitContext ) : Presenter < * > ? } }
  33. Circuit interface Presenter<UiState> { @Composable fun present() : UiState fun

    interface Factory { fun create( screen: Screen, navigator: Navigator, context: CircuitContext ) : Presenter < * > ? } }
  34. @Parcelize object CounterScreen : Screen { data class State( val

    count: Int, val eventSink: (Event) - > Unit ) : CircuitUiState object Event : CircuitUiEvent } Circuit
  35. State object NoState : CircuitUiState data class State( val count:

    Int, val eventSink: (Click) - > Unit ) : CircuitUiState
  36. State object NoState : CircuitUiState data class State( val count:

    Int, val eventSink: (Click) - > Unit ) : CircuitUiState
  37. State object NoState : CircuitUiState data class State( val count:

    Int, val eventSink: (Event) - > Unit ) : CircuitUiState
  38. State sealed interface State : CircuitUiState { object Loading :

    State data class Count( val count: Int, val eventSink: (Event) - > Unit ) : State }
  39. State sealed interface State : CircuitUiState { object Loading :

    State data class Count( val count: Int, val eventSink: (Event) - > Unit ) : State }
  40. State sealed interface State : CircuitUiState { object Loading :

    State data class Count( val count: Int, val eventSink: (Event) - > Unit ) : State }
  41. State sealed interface State : CircuitUiState { object Loading :

    State data class Count( val count: Int, val eventSink: (Event) - > Unit ) : State }
  42. State and Events data class State( val count: Int, val

    eventSink: (Event) - > Unit ) : CircuitUiState sealed interface Event { object Increment : Event object Decrement : Event }
  43. State and Events data class State( val count: Int, val

    eventSink: (Event) - > Unit ) : CircuitUiState sealed interface Event { object Increment : Event object Decrement : Event }
  44. Events in State class CounterPresenter : Presenter<State> { @Composable override

    fun present() : State { var count by remember { mutableStateOf(0) } return State(count) { event - > when (event) { is Increment - > count + + is Decrement - > count - - } } } }
  45. Events in State class CounterPresenter : Presenter<State> { @Composable override

    fun present() : State { var count by remember { mutableStateOf(0) } return State(count) { event - > when (event) { is Increment - > count + + is Decrement - > count - - } } } }
  46. Events in State @Composable fun CounterUi( state: CounterScreen.State, ) {

    val sink = state.eventSink / / . . . Button(onClick = { sink(Event.Increment) }) }
  47. Events in State @Composable fun CounterUi( state: CounterScreen.State, ) {

    val sink = state.eventSink / / . . . Button(onClick = { sink(Event.Increment) }) }
  48. Events in State @Composable override fun present() : State {

    var count by remember { mutableStateOf(0) } return State(count) { event - > when (event) { is Increment - > count + + is Decrement - > count - - } } } @Composable fun CounterUi(state: State) { val sink = state.eventSink Text("Count: ${state.count}") Button(onClick = { sink(Event.Increment) }) }
  49. Events in State @Composable override fun present() : State {

    var count by remember { mutableStateOf(0) } return State(count) { event - > when (event) { is Increment - > count + + is Decrement - > count - - } } } @Composable fun CounterUi(state: State) { val sink = state.eventSink Text("Count: ${state.count}") Button(onClick = { sink(Event.Increment) }) }
  50. Events in State @Composable override fun present() : State {

    var count by remember { mutableStateOf(0) } return State(count) { event - > when (event) { is Increment - > count + + is Decrement - > count - - } } } @Composable fun CounterUi(state: State) { val sink = state.eventSink Text("Count: ${state.count}") Button(onClick = { sink(Event.Increment) }) }
  51. Events in State @Composable override fun present() : State {

    var count by remember { mutableStateOf(0) } return State(count) { event - > when (event) { is Increment - > count + + is Decrement - > count - - } } } @Composable fun CounterUi(state: State) { val sink = state.eventSink Text("Count: ${state.count}") Button(onClick = { sink(Event.Increment) }) }
  52. Events in State @Composable override fun present() : State {

    var count by remember { mutableStateOf(0) } return State(count) { event - > when (event) { is Increment - > count + + is Decrement - > count - - } } } @Composable fun CounterUi(state: State) { val sink = state.eventSink Text("Count: ${state.count}") Button(onClick = { sink(Event.Increment) }) }
  53. Events in State @Composable override fun present() : State {

    var count by remember { mutableStateOf(0) } return State(count) { event - > when (event) { is Increment - > count + + is Decrement - > count - - } } } @Composable fun CounterUi(state: State) { val sink = state.eventSink Text("Count: ${state.count}") Button(onClick = { sink(Event.Increment) }) }
  54. Events in State @Composable override fun present() : State {

    var count by remember { mutableStateOf(0) } return State(count) { event - > when (event) { is Increment - > count + + is Decrement - > count - - } } } @Composable fun CounterUi(state: State) { val sink = state.eventSink Text("Count: ${state.count}") Button(onClick = { sink(Event.Increment) }) }
  55. Events in State @Composable override fun present() : State {

    var count by remember { mutableStateOf(0) } return State(count) { event - > when (event) { is Increment - > count + + is Decrement - > count - - } } } @Composable fun CounterUi(state: State) { val sink = state.eventSink Text("Count: ${state.count}") Button(onClick = { sink(Event.Increment) }) }
  56. Events in State @Composable override fun present() : State {

    var count by remember { mutableStateOf(0) } return State(count) { event - > when (event) { is Increment - > count + + is Decrement - > count - - } } } @Composable fun CounterUi(state: State) { val sink = state.eventSink Text("Count: ${state.count}") Button(onClick = { sink(Event.Increment) }) }
  57. Why is testing hard? • It shouldn’t be 😬 •

    Historic best practices (on Android): • Advocate for patterns that make testing hard • Encourage asserting called methods instead of verifying behaviour • Use of Android components in business logic encourages mocking
  58. UDF

  59. Presenter Tests class CounterPresenter : Presenter<State> { @Composable override fun

    present() : State { var count by remember { mutableStateOf(0) } return State(count) { event - > when (event) { is Increment - > count + + is Decrement - > count - - } } } }
  60. Presenter Tests @Test fun `present - verify state and event`()

    = runTest { val presenter = CounterPresenter() }a
  61. Presenter Tests @Test fun `present - verify state and event`()

    = runTest { val presenter = CounterPresenter() presenter.test { / / . . . } }a https://github.com/cashapp/turbine
  62. Presenter Tests @Test fun `present - verify state and event`()

    = runTest { val presenter = CounterPresenter() presenter.test { val f i rst = awaitItem() assertThat(f i rst.count).isEqualTo(0) } }a
  63. Presenter Tests @Test fun `present - verify state and event`()

    = runTest { val presenter = CounterPresenter() presenter.test { val f i rst = awaitItem() assertThat(f i rst.count).isEqualTo(0) f i rst.eventSink(Event.Increment) } }a
  64. Presenter Tests @Test fun `present - verify state and event`()

    = runTest { val presenter = CounterPresenter() presenter.test { val f i rst = awaitItem() assertThat(f i rst.count).isEqualTo(0) f i rst.eventSink(Event.Increment) assertThat(awaitItem().count).isEqualTo(1) } }a
  65. UI Tests @Composable fun CounterUi(state: State) { val sink =

    state.eventSink / / . . . Button(onClick = { sink(Event.Increment) }) }
  66. UI Tests @Test fun display_count_message() { composeTestRule.run { setContent {

    CounterUi( CounterScreen.State(5) ) } onNode(hasText("Count: 5")) .assertIsDisplayed() } }
  67. Circuit val conf i g = CircuitConf i g.Builder() /

    / . . . .build() val backstack = rememberSaveableBackStack { push(HomeScreen) }
  68. Circuit val conf i g = CircuitConf i g.Builder() /

    / . . . .build() val backstack = rememberSaveableBackStack { push(HomeScreen) } val navigator = rememberCircuitNavigator(backstack)
  69. Circuit val conf i g = CircuitConf i g.Builder() /

    / . . . .build() val backstack = rememberSaveableBackStack { push(HomeScreen) } val navigator = rememberCircuitNavigator(backstack) CircuitCompositionLocals(conf i g) { NavigableCircuitContent(navigator, backstack) }
  70. Circuit val conf i g = CircuitConf i g.Builder() /

    / . . . .build() val backstack = rememberSaveableBackStack { push(HomeScreen) } val navigator = rememberCircuitNavigator(backstack) CircuitCompositionLocals(conf i g) { NavigableCircuitContent(navigator, backstack) }
  71. Circuit val conf i g = CircuitConf i g.Builder() /

    / . . . .build() CircuitCompositionLocals(conf i g) { CircuitContent(HomeScreen) }
  72. Circuit override fun onCreate(savedInstanceState: Bundle?) { setContent { val conf

    i g = CircuitConf i g.Builder() / / . . . .build() CircuitCompositionLocals(conf i g) { CircuitContent(HomeScreen) } } }
  73. Circuit fun main() = singleWindowApplication("Circuit") { val conf i g

    = CircuitConf i g.Builder() / / . . . .build() CircuitCompositionLocals(conf i g) { CircuitContent(HomeScreen) } }
  74. Overlays val overlayHost = LocalOverlayHost.current LaunchedEffect(Unit) { / / ☇

    suspending! val result = overlayHost.show( BottomSheetOverlay( model = . . . , onDismiss = { . . . }, ) { model, overlayNavigator - > / / Content } ) / / Do something with the result }
  75. Overlays val overlayHost = LocalOverlayHost.current LaunchedEffect(Unit) { / / ☇

    suspending! val result = overlayHost.show( BottomSheetOverlay( model = . . . , onDismiss = { . . . }, ) { model, overlayNavigator - > / / Content } ) / / Do something with the result }
  76. Overlays val overlayHost = LocalOverlayHost.current LaunchedEffect(Unit) { val newFilters =

    overlayHost.show( FiltersOverlay() ) eventSink(UpdateFilters(newFilters)) }
  77. Composite Presenters class TabletHomePresenter @Inject constructor( private val listPresenter: ListPresenter,

    private val detailPresenter: DetailPresenter, ) : Presenter<CompositeState> { @Composable override fun present() : CompositeState { val listState = listPresenter.present() val detailState = detailPresenter.present() return CompositeState(listState, detailState) } }
  78. DI

  79. DI @Provides fun provideCircuit( presenterFactories: Set<Presenter.Factory>, uiFactories: Set<Ui.Factory>, ) :

    CircuitConf i g { return CircuitConf i g.Builder() .addPresenterFactories(presenterFactories) .addUiFactories(uiFactories) .build() }
  80. DI (w/ Anvil) @CircuitInject(CounterScreen : : class, AppScope : :

    class) class CounterPresenter @Inject constructor( private val repository: CounterRespository ) : Presenter<State> { / / . . . } @CircuitInject(CounterScreen : : class, AppScope : : class) @Composable fun Counter(state: State, modif i er: Modif i er) { / / . . . }
  81. Advanced Use Cases • Navigate to legacy Activity or Fragment

    using Interceptors • Tracing using EventListener and CircuitContext tags • Extract value from running Circuit environment using • Taking control of con fi g changes