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

Modern Compose Architecture with Circuit

Modern Compose Architecture with Circuit

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