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. Modern Compose Architecture
    with Circuit
    Kieran Elliott — Zac Sweers

    View Slide

  2. Compose

    View Slide

  3. 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!")


    }

    View Slide

  4. Compose

    View Slide

  5. Compose Architecture

    View Slide

  6. Compose Architecture
    @Composable


    fun Example()z{


    Text("Hello World!")


    }z

    View Slide

  7. Compose Architecture
    @Composable


    fun Example(viewModel:zExampleViewModel) {


    Text(viewModel.text)


    }z

    View Slide

  8. Compose Architecture
    @Composable


    fun Example() {


    val viewModel by viewModel()


    Text(viewModel.text)


    }z

    View Slide

  9. Compose Architecture
    @Composable


    fun Example() {


    val viewModel by viewModel()


    val text by viewModel.textF
    l
    ow().collectAsState()


    Text(text)


    }z

    View Slide

  10. Compose Architecture
    @Composable


    fun Example() {


    val viewModel by viewModel()


    val text by viewModel.textF
    l
    ow().collectAsState()


    Text(text)


    }

    View Slide

  11. Compose Architecture
    @Composable


    fun Example() {


    val viewModel by viewModel()


    val text by viewModel.textF
    l
    ow().collectAsState()


    Text(text)


    }

    View Slide

  12. Compose Architecture
    @Composable


    fun Example() {


    val viewModel by viewModel()


    Text(viewModel.text)


    }

    View Slide

  13. Compose Architecture
    @Composable


    fun Example(viewModel: ExampleViewModel) {


    Text(viewModel.text)


    }

    View Slide

  14. Compose Architecture
    viewModel: ExampleViewModel


    @Composable


    fun Example(text: String) {


    Text(text)


    }

    View Slide

  15. Compose Architecture
    viewModel: ExampleViewModel


    @Composable


    fun Example(text: String) {


    Text(text)


    }

    View Slide

  16. Compose Architecture
    viewModel: ExampleViewModel

    View Slide

  17. Architecture
    viewModel: ExampleViewModel

    View Slide

  18. Architecture
    class ExampleViewModel {


    / / . . .

    }

    View Slide

  19. Architecture
    class ExampleReducer {


    / / . . .

    }

    View Slide

  20. Architecture
    class ExampleController {


    / / . . .

    }

    View Slide

  21. Architecture
    class ExamplePresenter {


    / / . . .

    }

    View Slide

  22. Architecture
    class ExamplePresenter {


    fun state()
    :
    String


    }

    View Slide

  23. Architecture
    class ExamplePresenter {


    fun setOnStateChangedListener(listener: Listener)


    }

    View Slide

  24. Architecture
    class ExamplePresenter {


    fun state()
    :
    Observable


    }

    View Slide

  25. Architecture
    class ExamplePresenter {


    fun state()
    :
    F
    l
    ow


    }

    View Slide

  26. Architecture
    class ExamplePresenter {


    fun state()
    :
    StateF
    l
    ow


    }

    View Slide

  27. Architecture
    class ExamplePresenter {


    fun state()
    :
    StateF
    l
    ow


    }
    @Composable


    fun Example(state: State) {


    Text(state.text)


    }

    View Slide

  28. class ExamplePresenter {


    fun state(


    events: F
    l
    ow


    )
    :
    StateF
    l
    ow


    }
    @Composable


    fun Example(


    state: State,


    eventSink: (Event)
    - >
    Unit


    ) {


    Text(state.text)


    Button(onClick = { eventSink(Click) })


    }
    Architecture

    View Slide

  29. class ExamplePresenter {


    fun state(


    events: F
    l
    ow


    )
    :
    StateF
    l
    ow


    }
    @Composable


    fun Example(


    state: State,


    eventSink: (Event)
    - >
    Unit


    )
    Architecture

    View Slide

  30. class ExamplePresenter {


    fun state(


    events: F
    l
    ow


    )
    :
    StateF
    l
    ow


    }
    @Composable


    fun Example(


    state: State,


    eventSink: (Event)
    - >
    Unit


    )
    Architecture '22

    View Slide

  31. Architecture '23
    class ExamplePresenter {


    @Composable


    fun state()
    :
    State


    }
    @Composable


    fun Example(state: State)

    View Slide

  32. Compose Architecture
    with Circuit

    View Slide

  33. 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

    View Slide

  34. Circuit
    class ExamplePresenter {


    fun state(


    events: F
    l
    ow


    )
    :
    StateF
    l
    ow


    }

    View Slide

  35. Circuit
    class CounterPresenter {


    fun state(


    events: F
    l
    ow


    )
    :
    StateF
    l
    ow


    }

    View Slide

  36. Circuit
    class CounterPresenter {


    fun state(


    events: F
    l
    ow


    )
    :
    StateF
    l
    ow


    }
    data class State(val count: Int)


    View Slide

  37. Circuit
    class CounterPresenter {


    fun state(


    events: F
    l
    ow


    )
    :
    StateF
    l
    ow


    }

    View Slide

  38. Circuit
    class CounterPresenter {


    fun state(


    scope: CoroutineScope,


    events: F
    l
    ow


    )
    :
    StateF
    l
    ow


    }

    View Slide

  39. Circuit
    class CounterPresenter {


    private val count = MutableStateF
    l
    ow(State(0))


    fun state(


    scope: CoroutineScope,


    events: F
    l
    ow


    )
    :
    StateF
    l
    ow


    }

    View Slide

  40. Circuit
    class CounterPresenter {


    private val count = MutableStateF
    l
    ow(State(0))


    fun state(


    scope: CoroutineScope,


    events: F
    l
    ow


    )
    :
    StateF
    l
    ow {


    return count


    }


    }

    View Slide

  41. Circuit
    class CounterPresenter {


    private val count = MutableStateF
    l
    ow(State(0))


    fun state(


    scope: CoroutineScope,


    events: F
    l
    ow


    )
    :
    StateF
    l
    ow {


    scope.launch {


    events.collect {


    count.emit(State(count.value.count
    + +
    ))


    }


    }


    return count


    }


    }

    View Slide

  42. Circuit
    class CounterPresenter {


    @Composable


    fun state()
    :
    State {


    var count by remember { mutableStateOf(0) }


    return State(count) { event
    - >

    count
    + +

    }


    }


    }

    View Slide

  43. Circuit
    class CounterPresenter {


    @Composable


    fun state()
    :
    State {


    var count by rememberRetained { mutableStateOf(0) }


    return State(count) { count
    + +
    }


    }


    }

    View Slide

  44. Circuit
    class CounterPresenter {


    @Composable


    fun state()
    :
    State {


    var count by rememberSaveable { mutableStateOf(0) }


    return State(count) { count
    + +
    }


    }


    }

    View Slide

  45. Circuit
    class CounterPresenter : Presenter {


    @Composable


    override fun present()
    :
    State {


    var count by rememberSaveable { mutableStateOf(0) }


    return State(count) { count
    + +
    }


    }


    }

    View Slide

  46. Circuit
    interface Presenter {


    @Composable


    fun present()
    :
    UiState


    }

    View Slide

  47. Circuit
    interface Presenter {


    @Composable


    fun present()
    :
    UiState


    fun interface Factory {


    fun create(


    screen: Screen,


    navigator: Navigator,


    context: CircuitContext


    )
    :
    Presenter
    < * >
    ?


    }


    }

    View Slide

  48. Circuit
    interface Presenter {


    @Composable


    fun present()
    :
    UiState


    fun interface Factory {


    fun create(


    screen: Screen,


    navigator: Navigator,


    context: CircuitContext


    )
    :
    Presenter
    < * >
    ?


    }


    }
    interface Ui {


    @Composable


    fun Content(


    state: UiState,


    modif
    i
    er: Modif
    i
    er


    )


    fun interface Factory {


    fun create(


    screen: Screen,


    context: CircuitContext


    )
    :
    Ui
    < * >
    ?


    }


    }

    View Slide

  49. Circuit
    interface Presenter {


    @Composable


    fun present()
    :
    UiState


    fun interface Factory {


    fun create(


    screen: Screen,


    navigator: Navigator,


    context: CircuitContext


    )
    :
    Presenter
    < * >
    ?


    }


    }
    interface Ui {


    @Composable


    fun Content(


    state: UiState,


    modif
    i
    er: Modif
    i
    er


    )


    fun interface Factory {


    fun create(


    screen: Screen,


    context: CircuitContext


    )
    :
    Ui
    < * >
    ?


    }


    }

    View Slide

  50. Circuit
    interface Screen

    View Slide

  51. Circuit
    interface Screen : Parcelable

    View Slide

  52. Circuit
    interface Screen

    View Slide

  53. Circuit
    object CounterScreen(
    :
    Screen

    View Slide

  54. Circuit
    data class CounterScreen(val initialCount: Int) : Screen

    View Slide

  55. Circuit
    class CounterPresenter @AssistedInject constructor(


    @Assisted private val screen: CounterScreen


    ) : Presenter {


    @AssistedFactory


    interface Factory {


    fun create(screen: CounterScreen)
    :
    CounterPresenter


    }


    }

    View Slide

  56. Circuit
    interface Presenter {


    @Composable


    fun present()
    :
    UiState


    fun interface Factory {


    fun create(


    screen: Screen,


    navigator: Navigator,


    context: CircuitContext


    )
    :
    Presenter
    < * >
    ?


    }


    }

    View Slide

  57. Circuit
    interface Presenter {


    @Composable


    fun present()
    :
    UiState


    fun interface Factory {


    fun create(


    screen: Screen,


    navigator: Navigator,


    context: CircuitContext


    )
    :
    Presenter
    < * >
    ?


    }


    }

    View Slide

  58. Navigation
    interface Navigator {


    fun goTo(screen: Screen)


    fun pop()
    :
    Screen?


    fun resetRoot(newRoot: Screen)
    :
    List


    }

    View Slide

  59. Navigation
    interface Navigator {


    fun goTo(screen: Screen)


    fun pop()
    :
    Screen?


    fun resetRoot(newRoot: Screen)
    :
    List


    }

    View Slide

  60. Navigation
    interface Navigator {


    fun goTo(screen: Screen)


    fun pop()
    :
    Screen?


    fun resetRoot(newRoot: Screen)
    :
    List


    }

    View Slide

  61. Navigation
    interface Navigator {


    fun goTo(screen: Screen)


    fun pop()
    :
    Screen?


    fun resetRoot(newRoot: Screen)
    :
    List


    }

    View Slide

  62. Navigation
    val backstack = rememberSaveableBackStack { push(HomeScreen) }


    val navigator = rememberCircuitNavigator(backstack)


    / / . . .

    NavigableCircuitContent(navigator, backstack)
    gist.github.com/adamp/17b4e5cfafc7d44a0023dc2fbdb972e8

    View Slide

  63. Circuit
    class CounterPresenter @AssistedInject constructor(


    @Assisted private val screen: CounterScreen,


    @Assisted private val navigator: Navigator,


    ) : Presenter {


    @Composable


    override fun present()
    :
    State {




    }


    }

    View Slide

  64. Circuit
    class CounterPresenter @AssistedInject constructor(


    @Assisted private val screen: CounterScreen,


    @Assisted private val navigator: Navigator,


    ) : Presenter {


    @Composable


    override fun present()
    :
    State {


    navigator.goTo(LoginScreen)


    }


    }

    View Slide

  65. Circuit
    interface Presenter {


    @Composable


    fun present()
    :
    UiState


    fun interface Factory {


    fun create(


    screen: Screen,


    navigator: Navigator,


    context: CircuitContext


    )
    :
    Presenter
    < * >
    ?


    }


    }

    View Slide

  66. Circuit
    interface Presenter {


    @Composable


    fun present()
    :
    UiState


    fun interface Factory {


    fun create(


    screen: Screen,


    navigator: Navigator,


    context: CircuitContext


    )
    :
    Presenter
    < * >
    ?


    }


    }

    View Slide

  67. Circuit
    interface Presenter {


    @Composable


    fun present()
    :
    UiState


    fun interface Factory {


    fun create(


    screen: Screen,


    navigator: Navigator,


    context: CircuitContext


    )
    :
    Presenter
    < * >
    ?


    }


    }

    View Slide

  68. @Parcelize


    object CounterScreen : Screen {


    data class State(


    val count: Int,


    val eventSink: (Event)
    - >
    Unit


    ) : CircuitUiState


    object Event : CircuitUiEvent


    }
    Circuit

    View Slide

  69. State and Events
    @Stable


    interface CircuitUiState


    @Immutable


    interface CircuitUiEvent

    View Slide

  70. State
    object NoState : CircuitUiState


    data class State(


    val count: Int,


    val eventSink: (Click)
    - >
    Unit


    )
    :
    CircuitUiState

    View Slide

  71. State
    object NoState : CircuitUiState


    data class State(


    val count: Int,


    val eventSink: (Click)
    - >
    Unit


    )
    :
    CircuitUiState

    View Slide

  72. State
    object NoState : CircuitUiState


    data class State(


    val count: Int,


    val eventSink: (Event)
    - >
    Unit


    )
    :
    CircuitUiState

    View Slide

  73. State
    sealed interface State : CircuitUiState {


    object Loading : State


    data class Count(


    val count: Int,


    val eventSink: (Event)
    - >
    Unit


    )
    :
    State


    }

    View Slide

  74. State
    sealed interface State : CircuitUiState {


    object Loading : State


    data class Count(


    val count: Int,


    val eventSink: (Event)
    - >
    Unit


    )
    :
    State


    }

    View Slide

  75. State
    sealed interface State : CircuitUiState {


    object Loading : State


    data class Count(


    val count: Int,


    val eventSink: (Event)
    - >
    Unit


    )
    :
    State


    }

    View Slide

  76. State
    sealed interface State : CircuitUiState {


    object Loading : State


    data class Count(


    val count: Int,


    val eventSink: (Event)
    - >
    Unit


    )
    :
    State


    }

    View Slide

  77. Events
    object Reset : CircuitUiEvent


    data class Click(


    val amount: Int = 1


    ) : CircuitUiEvent

    View Slide

  78. Events
    object Reset : CircuitUiEvent


    data class Click(


    val amount: Int = 1


    ) : CircuitUiEvent

    View Slide

  79. Events
    object Reset : CircuitUiEvent


    data class Click(


    val amount: Int = 1


    ) : CircuitUiEvent

    View Slide

  80. Events
    sealed interface Event : CircuitUiEvent {


    object Increment : Event


    object Decrement : Event


    }

    View Slide

  81. Events
    sealed interface Event : CircuitUiEvent {


    object Increment : Event


    object Decrement : Event


    }

    View Slide

  82. State and Events
    data class State(


    val count: Int,


    val eventSink: (Event)
    - >
    Unit


    )
    :
    CircuitUiState
    sealed interface Event {


    object Increment : Event


    object Decrement : Event


    }

    View Slide

  83. State and Events
    data class State(


    val count: Int,


    val eventSink: (Event)
    - >
    Unit


    )
    :
    CircuitUiState
    sealed interface Event {


    object Increment : Event


    object Decrement : Event


    }

    View Slide

  84. Events in State?

    View Slide

  85. What’s going on here?

    View Slide

  86. Events in State
    class CounterPresenter : Presenter {


    @Composable


    override fun present()
    :
    State {


    var count by remember { mutableStateOf(0) }


    return State(count) { event
    - >

    when (event) {


    is Increment
    - >
    count
    + +

    is Decrement
    - >
    count
    - -

    }


    }


    }


    }

    View Slide

  87. Events in State
    class CounterPresenter : Presenter {


    @Composable


    override fun present()
    :
    State {


    var count by remember { mutableStateOf(0) }


    return State(count) { event
    - >

    when (event) {


    is Increment
    - >
    count
    + +

    is Decrement
    - >
    count
    - -

    }


    }


    }


    }

    View Slide

  88. Events in State
    @Composable


    fun CounterUi(


    state: CounterScreen.State,


    ) {


    val sink = state.eventSink


    / / . . .

    Button(onClick = { sink(Event.Increment) })


    }

    View Slide

  89. Events in State
    @Composable


    fun CounterUi(


    state: CounterScreen.State,


    ) {


    val sink = state.eventSink


    / / . . .

    Button(onClick = { sink(Event.Increment) })


    }

    View Slide

  90. 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) })


    }

    View Slide

  91. 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) })


    }

    View Slide

  92. 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) })


    }

    View Slide

  93. 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) })


    }

    View Slide

  94. 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) })


    }

    View Slide

  95. 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) })


    }

    View Slide

  96. 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) })


    }

    View Slide

  97. 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) })


    }

    View Slide

  98. 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) })


    }

    View Slide

  99. Testing

    View Slide

  100. 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

    View Slide

  101. UDF

    View Slide

  102. UDF + Compose

    View Slide

  103. UDF + Compose =

    View Slide

  104. Presenter Tests
    class CounterPresenter : Presenter {


    @Composable


    override fun present()
    :
    State {


    var count by remember { mutableStateOf(0) }


    return State(count) { event
    - >

    when (event) {


    is Increment
    - >
    count
    + +

    is Decrement
    - >
    count
    - -

    }


    }


    }


    }

    View Slide

  105. Presenter Tests
    @Test


    fun `present - verify state and event`() = runTest {


    }a

    View Slide

  106. Presenter Tests
    @Test


    fun `present - verify state and event`() = runTest {


    val presenter = CounterPresenter()


    }a

    View Slide

  107. Presenter Tests
    @Test


    fun `present - verify state and event`() = runTest {


    val presenter = CounterPresenter()


    presenter.test {


    / / . . .

    }


    }a
    https://github.com/cashapp/turbine

    View Slide

  108. 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

    View Slide

  109. 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

    View Slide

  110. 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

    View Slide

  111. UI Tests
    @Composable


    fun CounterUi(state: State) {


    val sink = state.eventSink


    / / . . .

    Button(onClick = { sink(Event.Increment) })


    }

    View Slide

  112. UI Tests
    @Test


    fun display_count_message() {


    composeTestRule.run {


    setContent {


    / / . . .

    }


    }


    }a

    View Slide

  113. UI Tests
    @Test


    fun display_count_message() {


    composeTestRule.run {


    setContent {


    CounterUi(


    CounterScreen.State(5)


    )


    }


    }


    }

    View Slide

  114. UI Tests
    @Test


    fun display_count_message() {


    composeTestRule.run {


    setContent {


    CounterUi(


    CounterScreen.State(5)


    )


    }


    onNode(hasText("Count: 5"))


    .assertIsDisplayed()


    }


    }

    View Slide

  115. UI Tests: Snapshots

    View Slide

  116. UI Tests: Previews

    View Slide

  117. How do I use Circuit?

    View Slide

  118. Circuit
    val conf
    i
    g = CircuitConf
    i
    g.Builder()


    / / . . .

    .build()

    View Slide

  119. Circuit
    val conf
    i
    g = CircuitConf
    i
    g.Builder()


    / / . . .

    .build()


    val backstack = rememberSaveableBackStack { push(HomeScreen) }

    View Slide

  120. Circuit
    val conf
    i
    g = CircuitConf
    i
    g.Builder()


    / / . . .

    .build()


    val backstack = rememberSaveableBackStack { push(HomeScreen) }


    val navigator = rememberCircuitNavigator(backstack)

    View Slide

  121. 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)


    }

    View Slide

  122. 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)


    }

    View Slide

  123. Circuit
    val conf
    i
    g = CircuitConf
    i
    g.Builder()


    / / . . .

    .build()


    CircuitCompositionLocals(conf
    i
    g) {


    CircuitContent(HomeScreen)


    }

    View Slide

  124. Circuit
    override fun onCreate(savedInstanceState: Bundle?) {


    setContent {


    val conf
    i
    g = CircuitConf
    i
    g.Builder()


    / / . . .

    .build()


    CircuitCompositionLocals(conf
    i
    g) {


    CircuitContent(HomeScreen)


    }


    }


    }

    View Slide

  125. Circuit
    fun main() = singleWindowApplication("Circuit") {


    val conf
    i
    g = CircuitConf
    i
    g.Builder()


    / / . . .

    .build()


    CircuitCompositionLocals(conf
    i
    g) {


    CircuitContent(HomeScreen)


    }


    }

    View Slide

  126. Other Bells and Whistles

    View Slide

  127. Overlays

    View Slide

  128. Overlays

    View Slide

  129. Overlays
    CircuitContent(HomeScreen)a

    View Slide

  130. Overlays
    ContentWithOverlays {


    CircuitContent(HomeScreen)


    }a

    View Slide

  131. Overlays
    val overlayHost = LocalOverlayHost.current


    LaunchedEffect(Unit) {


    / /
    ☇ suspending!


    val result = overlayHost.show(


    BottomSheetOverlay(


    model =
    . . .
    ,


    onDismiss = {
    . . .
    },


    ) { model, overlayNavigator
    - >

    / /
    Content


    }


    )


    / /
    Do something with the result


    }

    View Slide

  132. Overlays
    val overlayHost = LocalOverlayHost.current


    LaunchedEffect(Unit) {


    / /
    ☇ suspending!


    val result = overlayHost.show(


    BottomSheetOverlay(


    model =
    . . .
    ,


    onDismiss = {
    . . .
    },


    ) { model, overlayNavigator
    - >

    / /
    Content


    }


    )


    / /
    Do something with the result


    }

    View Slide

  133. Overlays
    val overlayHost = LocalOverlayHost.current


    LaunchedEffect(Unit) {


    val newFilters = overlayHost.show(


    FiltersOverlay()


    )


    eventSink(UpdateFilters(newFilters))


    }

    View Slide

  134. Overlays
    interface Overlay {


    @Composable


    fun Content(navigator: OverlayNavigator)


    }

    View Slide

  135. State Restoration

    View Slide

  136. State Restoration
    val compositionState by remember {
    . . .
    }

    View Slide

  137. State Restoration
    val presenterState by rememberRetained {
    . . .
    }

    View Slide

  138. State Restoration
    val processDeath by rememberSaveable {
    . . .
    }

    View Slide

  139. Composite Presenters

    View Slide

  140. Composite Presenters
    class ListPresenter
    class DetailPresenter

    View Slide

  141. Composite Presenters
    ListPresenter DetailPresenter
    https://developer.android.com/guide/topics/large-screens/large-screen-canonical-layouts#list-detail

    View Slide

  142. Composite Presenters
    ListPresenter DetailPresenter
    https://developer.android.com/guide/topics/large-screens/large-screen-canonical-layouts#list-detail

    View Slide

  143. Composite Presenters
    class TabletHomePresenter @Inject constructor(


    private val listPresenter: ListPresenter,


    private val detailPresenter: DetailPresenter,


    ) : Presenter {


    @Composable


    override fun present()
    :
    CompositeState {


    val listState = listPresenter.present()


    val detailState = detailPresenter.present()


    return CompositeState(listState, detailState)


    }


    }

    View Slide

  144. DI

    View Slide

  145. DI
    @Module


    interface CircuitModule {


    @Multibinds


    fun presenterFactories()
    :
    Set


    @Multibinds


    fun viewFactories()
    :
    Set


    }

    View Slide

  146. DI
    @Provides


    fun provideCircuit(


    presenterFactories: Set,


    uiFactories: Set,


    )
    :
    CircuitConf
    i
    g {


    return CircuitConf
    i
    g.Builder()


    .addPresenterFactories(presenterFactories)


    .addUiFactories(uiFactories)


    .build()


    }

    View Slide

  147. DI (w/ Anvil)
    @CircuitInject(CounterScreen
    : :
    class, AppScope
    : :
    class)


    class CounterPresenter @Inject constructor(


    private val repository: CounterRespository


    ) : Presenter {


    / / . . .

    }


    @CircuitInject(CounterScreen
    : :
    class, AppScope
    : :
    class)


    @Composable


    fun Counter(state: State, modif
    i
    er: Modif
    i
    er) {


    / / . . .

    }

    View Slide

  148. Interop

    View Slide

  149. :samples:interop

    View Slide

  150. Multiplatform

    View Slide

  151. Multiplatform
    • Goal is multiplatform presentation logic, not multiplatform UI


    • Current focus is JVM and Desktop use cases

    View Slide

  152. Advanced Use Cases

    View Slide

  153. 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

    View Slide

  154. Examples!
    CatchUp
    STAR
    Counter
    Tacos
    github.com/zacsweers/catchup

    View Slide

  155. View Slide

  156. Future
    • Improved KMP/KMM support


    • Inter-Screen transitions API


    • Lint checks


    • ???

    View Slide

  157. Thank you,

    and don’t forget

    to vote
    @ZacSweers
    @kierse
    KotlinConf’23
    Amsterdam

    View Slide