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

MVI with Jetpack Compose

Luca Nicoletti
September 09, 2019

MVI with Jetpack Compose

Slides for Droidcon Lisbon talk

Luca Nicoletti

September 09, 2019
Tweet

More Decks by Luca Nicoletti

Other Decks in Programming

Transcript

  1. About me • Luca • Italian • Android Engineer @Babylon

    Health • Based in London @luca_nicolett
  2. MVI

  3. MVI

  4. MVI We also added middleware • Works as a glue

    • Binds actions to transformers • Transformations to reducers • Or actions to reducers directly
  5. MVI

  6. MVI

  7. MVI private fun render(state: HomeScreenRedesignState) { loading_container .show(state.loadingState == LoadingState.Loading)

    unable_to_connect_error_container .show(state.loadingState == LoadingState.Error) /* ... */ }
  8. MVI

  9. Jetpack Compose • Declarative UI • Concise and idiomatic •

    Stateless or Stateful components • Reusable components • Compatible
  10. Jetpack Compose @Composable @GenerateView fun Greetings(name: String) { /* ...

    */ } <GreetingsView android:id="@+id/greetings" app:name="Luca" /> val greetingsView = findViewById<GreetingsView>(R.id.greetings) greetingsView.name = "Luca"
  11. Jetpack Compose @Composable @GenerateView fun Greetings(name: String) { /* ...

    */ } <GreetingsView android:id="@+id/greetings" app:name="Luca" /> val greetingsView = findViewById<GreetingsView>(R.id.greetings) greetingsView.name = "Luca"
  12. Jetpack Compose @Composable @GenerateView fun Greetings(name: String) { /* ...

    */ } <GreetingsView android:id="@+id/greetings" app:name="Luca" /> val greetingsView = findViewById<GreetingsView>(R.id.greetings) greetingsView.name = "Luca"
  13. Jetpack Compose • Declarative UI • Concise and idiomatic •

    Stateless or Stateful components • Reusable components • Compatible • Unbundled from OS
  14. Jetpack Compose mkdir androidx-master-dev cd androidx-master-dev repo init -u https://android.googlesource.com/platform/manifest

    -b androidx-master-dev repo sync -j8 -c Download the code (and grab a coffee while we pull down 6GB)
  15. @Composable fun Text(/* ... */) { /* ... */ Draw

    { canvas, _ -> internalSelection.value?.let { textDelegate.paintBackground( it.min, it.max, selectionColor, canvas ) } textDelegate.paint(canvas) } /* ... */ } Jetpack Compose
  16. @Composable fun TextField(/* ... */) { // States val hasFocus

    = +state { false } val coords = +state<LayoutCoordinates?> { null } /* ... */ } Jetpack Compose
  17. @CheckResult("+") /* inline */ fun <T> state(init: () -> T)

    = memo { State(init()) } Jetpack Compose
  18. /** * The State class is an @Model class meant

    to * wrap around a single value. * It is used in the `+state` and `+stateFor` effects. */ @Model class State<T> @PublishedApi internal constructor(value:T) : Framed { /* ... */ } Jetpack Compose
  19. /** * [Model] can be applied to a class which

    represents your * application's data model, and will cause instances of the * class to become observable, such that a read of a property * of an instance of this class during the invocation of a * composable function will cause that component to be * "subscribed" to mutations of that instance. Composable * functions which directly or indirectly read properties of the * model class, the composables will be recomposed whenever any * properties of the the model are written to. */ Jetpack Compose
  20. @Model data class TaskModel( var isDone: Boolean, val description: String

    ): BaseModel() TaskModel.kt: (14, 3): Model objects do not support inheritance Jetpack Compose
  21. @Composable fun RallyBody() { Padding(padding = 16.dp) { Column {

    // TODO: scrolling container RallyAlertCard() HeightSpacer(height = 10.dp) RallyAccountsCard() } } } Jetpack Compose
  22. @Composable fun RallyBody() { Padding(padding = 16.dp) { VerticalScroller {

    Column { RallyAlertCard() HeightSpacer(height = 10.dp) RallyAccountsCard() } } } } Jetpack Compose
  23. @Composable fun DrawSeekBar(x: Float) { var paint = +memo {

    Paint() } Draw { canvas, parentSize -> /* ... */ canvas.drawRect(Rect(/* ... */), paint) canvas.drawRect(Rect(/* ... */), paint) canvas.drawCircle(/* ... */, paint) } } Jetpack Compose
  24. Text(text = "Row") ContainerWithBackground( width = ExampleSize, color = lightGrey

    ) { Row { PurpleSquare() CyanSquare() } } Jetpack Compose
  25. Jetpack Compose ContainerWithBackground( width = ExampleSize, color = lightGrey )

    { Row( mainAxisAlignment = MainAxisAlignment.End ) { PurpleSquare() CyanSquare() } }
  26. Text(text = "Column") ContainerWithBackground( height = ExampleSize, color = lightGrey

    ) { Row { Column { PurpleSquare() CyanSquare() } /* ... */ } } Jetpack Compose
  27. Jetpack Compose ContainerWithBackground( height = ExampleSize, color = lightGrey )

    { Column( crossAxisAlignment = CrossAxisAlignment.End ) { PurpleSquare() CyanSquare() } }
  28. @Composable fun RippleRect() { val toState = +state{ ButtonStatus.Initial }

    val onPress:(PxPosition) -> Unit = { p -> down.x = p.x.value down.y = p.y.value toState.value = ButtonStatus.Pressed } /* ... */ } Jetpack Compose
  29. @Composable fun RippleRect() { val toState = +state{ ButtonStatus.Initial }

    val onPress:(PxPosition) -> Unit = { p -> down.x = p.x.value down.y = p.y.value toState.value = ButtonStatus.Pressed } /* ... */ } Jetpack Compose
  30. @Composable fun RippleRect() { /* ... */ val onRelease: ()

    -> Unit = { toState.value = ButtonStatus.Released } /* ... */ } Jetpack Compose
  31. @Composable fun RippleRectFromState(state: TransitionState) { Draw { canvas, _ ->

    canvas.drawCircle( Offset(x, y), radius, paint ) } } Jetpack Compose
  32. @Composable fun AlertDialog( onCloseRequest: () -> Unit, title: (@Composable() ()

    -> Unit), text: (@Composable() () -> Unit), confirmButton: (@Composable() () -> Unit), dismissButton: (@Composable() () -> Unit), buttonLayout: AlertDialogButtonLayout ) { /* ... */ } Jetpack Compose
  33. Jetpack Compose @Composable fun Dialog( onCloseRequest: () -> Unit, children:

    @Composable() () -> Unit ) { val context = +ambient(ContextAmbient) val dialog = +memo { DialogWrapper(context, onCloseRequest) } }
  34. Jetpack Compose @Composable fun Dialog( onCloseRequest: () -> Unit, children:

    @Composable() () -> Unit ) { +onActive { dialog.show() onDispose { dialog.dismiss() dialog.disposeComposition() } } }
  35. Jetpack Compose @Composable fun Dialog( onCloseRequest: () -> Unit, children:

    @Composable() () -> Unit ) { +onCommit { dialog.setContent(children) } }
  36. Jetpack Compose @Test fun topAppBar_expandsToScreen() { val dm = composeTestRule.displayMetrics

    composeTestRule .setMaterialContentAndCollectSizes { TopAppBar<Nothing>() } .assertHeightEqualsTo(appBarHeight) .assertWidthEqualsTo { dm.widthPixels.ipx } }
  37. Jetpack Compose @Test fun topAppBar_expandsToScreen() { val dm = composeTestRule.displayMetrics

    composeTestRule .setMaterialContentAndCollectSizes { TopAppBar<Nothing>() } .assertHeightEqualsTo(appBarHeight) .assertWidthEqualsTo { dm.widthPixels.ipx } }
  38. Jetpack Compose @Test fun topAppBar_expandsToScreen() { val dm = composeTestRule.displayMetrics

    composeTestRule .setMaterialContentAndCollectSizes { TopAppBar<Nothing>() } .assertHeightEqualsTo(appBarHeight) .assertWidthEqualsTo { dm.widthPixels.ipx } }
  39. All together object InProgress : BaseViewState(true, null) { @Composable override

    fun buildUI() { Container(expanded = true) { CircularProgressIndicator() } } }
  40. override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = /* view

    model init */ setContent { CustomTheme { render(viewModel.states()) } } /* .. */ } All together
  41. override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = /* view

    model init */ setContent { CustomTheme { render(viewModel.states()) } } /* .. */ } All together
  42. override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) viewModel = /* view

    model init */ setContent { CustomTheme { render(viewModel.states()) } } /* .. */ } All together
  43. override fun render( observableState: Observable<BaseViewState> ) { val state =

    +observe(ViewState.Idle, observableState) state.buildUI() } All together
  44. override fun render( observableState: Observable<BaseViewState> ) { val state =

    +observe(ViewState.Idle, observableState) state.buildUI() } All together
  45. override fun render( observableState: Observable<BaseViewState> ) { val state =

    +observe(ViewState.Idle, observableState) state.buildUI() } All together
  46. fun <T> observe(initialState: T, data: Observable<T>) = effectOf<T> { val

    result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } return result.value } All together
  47. fun <T> observe(initialState: T, data: Observable<T>) = effectOf<T> { val

    result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } return result.value } All together
  48. fun <T> observe(initialState: T, data: Observable<T>) = effectOf<T> { val

    result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } return result.value } All together
  49. fun <T> observe(initialState: T, data: Observable<T>) = effectOf<T> { val

    result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } return result.value } All together
  50. fun <T> observe(initialState: T, data: Observable<T>) = effectOf<T> { val

    result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } return result.value } All together
  51. All together override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // viewModel

    = /* ... */ not needed anymore setContent { PostListScreen( processIntents(intents()), reloadPostListIntentPublisher ) } }
  52. All together override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // viewModel

    = /* ... */ not needed anymore setContent { PostListScreen( processIntents(intents()), reloadPostListIntentPublisher ) } }
  53. All together override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // viewModel

    = /* ... */ not needed anymore setContent { PostListScreen( processIntents(intents()), reloadPostListIntentPublisher ) } }
  54. All together override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // viewModel

    = /* ... */ not needed anymore setContent { PostListScreen( processIntents(intents()), reloadPostListIntentPublisher ) } }
  55. All together fun modelState() = +effectOf<PostListViewState> { val result =

    +state { PostListViewState.Idle } var disposable: Disposable? = null +onActive { /* ... */ } +onDispose { disposable?.dispose() } result.value }
  56. All together fun modelState() = +effectOf<PostListViewState> { val result =

    +state { PostListViewState.Idle } var disposable: Disposable? = null +onActive { /* ... */ } +onDispose { disposable?.dispose() } result.value }
  57. All together fun modelState() = +effectOf<PostListViewState> { val result =

    +state { PostListViewState.Idle } var disposable: Disposable? = null +onActive { /* ... */ } +onDispose { disposable?.dispose() } result.value }
  58. All together fun modelState() = +effectOf<PostListViewState> { val result =

    +state { PostListViewState.Idle } var disposable: Disposable? = null +onActive { /* ... */ } +onDispose { disposable?.dispose() } result.value }
  59. All together override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // viewModel

    = /* ... */ not needed anymore setContent { PostListScreen( processIntents(intents()) ) } }
  60. All together @Composable fun PostListScreen( state: PostListViewState ) { when

    (state) { PostListViewState.InProgress -> { /* ... */ } } }