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. MVI with Jetpack Compose

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

    Health • Based in London @luca_nicolett
  3. MVI Jetpack Compose Topics

  4. MVI

  5. MVI MODEL INTENT VIEW USER Updates Sees Manipulates Interacts

  6. MVI Core principles: • Unidirectional • Immutability • Reactiveness •

    Functional
  7. MVI

  8. MVI ACTIONS BUSINESS LOGIC EVENTS REDUCER STATE STATE UI LISTENS

    UI
  9. MVI ACTIONS ACTION RELAY TRANSFORMERS REDUCER STATE UPDATES SOMETHING ACTIONS

    EVENTS STATE STATE RELAY
  10. MVI • Actions • Transformers • Reducers • State

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

    • Binds actions to transformers • Transformations to reducers • Or actions to reducers directly
  12. MVI perform("rating selection") .on<ActionFeedbackAction.RatingSelected>() .withReducer(reducers::reduceRatingSelected)

  13. MVI perform("load logged in patient data") .on( LifecycleAction.Created::class.java, ProfileScreenAction.RetryConnection::class.java, ProfileScreenAction.MonitorReady::class.java

    ) .transform { transformers.loadPatient(this) } .withReducer(reducers::reduceLoggedInPatient)
  14. MVI perform("load logged in patient") .on( LifecycleAction.Created::class.java, ProfileScreenAction.RetryConnection::class.java, ProfileScreenAction.MonitorReady::class.java )

    .transform { transformers.loadPatient(this) } .withReducer(reducers::reduceLoggedInPatient)
  15. MVI How does it help? • Decoupling • Testing

  16. MVI

  17. MVI

  18. MVI ACTIVITY FRAGMENT render() RENDERER

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

    unable_to_connect_error_container .show(state.loadingState == LoadingState.Error) /* ... */ }
  20. MVI fun View.show(isVisible: Boolean = true) { this.visibility = if

    (isVisible) View.VISIBLE else View.GONE }
  21. MVI private fun render(state: EditMoodState) { submitEditsButton?.isEnabled = state.hasAppliedChanges /*

    ... */ }
  22. MVI private fun render(state: AddMoodState) { if (state.showUrgentHelpDialog) { showUrgentHelpDialog()

    return } /* ... */ }
  23. MVI

  24. Jetpack Compose

  25. Jetpack Compose

  26. Jetpack Compose

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

    Stateless or Stateful components • Reusable components • Compatible
  28. 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"
  29. 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"
  30. 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"
  31. Jetpack Compose • Declarative UI • Concise and idiomatic •

    Stateless or Stateful components • Reusable components • Compatible • Unbundled from OS
  32. Jetpack Compose

  33. mkdir ~/bin PATH=~/bin:$PATH curl https://storage.googleapis.com/git-repo-downloads/repo > ~/bin/repo chmod a+x ~/bin/repo

    Jetpack Compose
  34. 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
  35. 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)
  36. cd path/to/checkout/frameworks/support/ ./studiow Jetpack Compose

  37. Jetpack Compose

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

    setContent { MyComposableFunction() } } } Jetpack Compose
  39. fun Activity.setContent(content: @Composable() () -> Unit) : CompositionContext? { /*

    ... */ } Jetpack Compose
  40. fun Activity.setContent(content: @Composable() () -> Unit) : CompositionContext? { /*

    ... */ } Jetpack Compose
  41. val craneView = window.decorView .findViewById<ViewGroup>(android.R.id.content) .getChildAt(0) as? AndroidCraneView ?: AndroidCraneView(this).also

    { setContentView(it) } Jetpack Compose
  42. val coroutineContext = Dispatchers.Main return Compose.composeInto(craneView.root, this) { WrapWithAmbients(craneView, this,

    coroutineContext) { content() } } Jetpack Compose
  43. @Composable fun Greetings(name: String) { Text("Hello $name") } Jetpack Compose

  44. @Composable fun Text(/* ... */) { /* ... */ Draw

    { canvas, _ -> internalSelection.value?.let { textDelegate.paintBackground( it.min, it.max, selectionColor, canvas ) } textDelegate.paint(canvas) } /* ... */ } Jetpack Compose
  45. Jetpack Compose

  46. Jetpack Compose

  47. @Composable fun TextField(/* ... */) { // States val hasFocus

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

    = memo { State(init()) } Jetpack Compose
  49. /** * 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
  50. /** * [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
  51. @Model data class TaskModel( var isDone: Boolean, val description: String

    ) Jetpack Compose
  52. @Model data class TaskModel( var isDone: Boolean, val description: String

    ): BaseModel() Jetpack Compose
  53. @Model data class TaskModel( var isDone: Boolean, val description: String

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

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

    Column { RallyAlertCard() HeightSpacer(height = 10.dp) RallyAccountsCard() } } } } Jetpack Compose
  56. Jetpack Compose

  57. Jetpack Compose

  58. @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
  59. Jetpack Compose

  60. Text(text = "Row") ContainerWithBackground( width = ExampleSize, color = lightGrey

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

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

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

    { Column( crossAxisAlignment = CrossAxisAlignment.End ) { PurpleSquare() CyanSquare() } }
  64. Jetpack Compose

  65. @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
  66. @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
  67. @Composable fun RippleRect() { /* ... */ val onRelease: ()

    -> Unit = { toState.value = ButtonStatus.Released } /* ... */ } Jetpack Compose
  68. PressGestureDetector(onPress, onRelease) { Container(expanded = true) { Transition(toState = toState.value)

    { s-> RippleRectFromState(s) } } } Jetpack Compose
  69. @Composable fun RippleRectFromState(state: TransitionState) { Draw { canvas, _ ->

    canvas.drawCircle( Offset(x, y), radius, paint ) } } Jetpack Compose
  70. Jetpack Compose

  71. @Composable fun AlertDialog( onCloseRequest: () -> Unit, title: (@Composable() ()

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

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

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

    @Composable() () -> Unit ) { +onCommit { dialog.setContent(children) } }
  75. @RunWith(JUnit4::class) @get:Rule val composeTestRule = createComposeRule() Jetpack Compose

  76. Jetpack Compose @Test fun topAppBar_expandsToScreen() { val dm = composeTestRule.displayMetrics

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

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

    composeTestRule .setMaterialContentAndCollectSizes { TopAppBar<Nothing>() } .assertHeightEqualsTo(appBarHeight) .assertWidthEqualsTo { dm.widthPixels.ipx } }
  79. All together

  80. All together Functional approach OOP approach

  81. All together interface BaseViewState { @Composable fun buildUI() }

  82. All together object InProgress : BaseViewState(true, null) { @Composable override

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

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

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

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

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

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

    +observe(ViewState.Idle, observableState) state.buildUI() } All together
  89. 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
  90. 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
  91. 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
  92. 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
  93. 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
  94. All together override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // viewModel

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

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

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

    = /* ... */ not needed anymore setContent { PostListScreen( processIntents(intents()), reloadPostListIntentPublisher ) } }
  98. All together fun processIntents(intents: Observable<PostListIntent>): PostListViewState { intents.subscribe(intentsObserver) return modelState()

    } }
  99. All together fun modelState() = +effectOf<PostListViewState> { val result =

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

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

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

    +state { PostListViewState.Idle } var disposable: Disposable? = null +onActive { /* ... */ } +onDispose { disposable?.dispose() } result.value }
  103. All together +onActive { disposable = intentsObserver .applyBusinessLogic() .subscribe {

    result.value = it } }
  104. All together override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // viewModel

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

    (state) { PostListViewState.InProgress -> { /* ... */ } } }
  106. All together PostListScreen(processIntents(intents()))

  107. All together

  108. All together PostListScreen( processIntents( intents() ) )

  109. References MVI http://hannesdorfmann.com/android/model-view-intent https://www.novatec-gmbh.de/en/blog/mvi-in-android/ https://cycle.js.org/model-view-intent.html Jetpack https://www.youtube.com/watch?v=VsStyq4Lzxo https://medium.com/q42-engineering/android-jetpack-compose-895b7fd04bf4 https://blog.karumi.com/android-jetpack-compose-review/

  110. Thank you @luca_nicolett

  111. Questions?