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

MVI with Jetpack Compose

MVI with Jetpack Compose

Slides for my talk @ GDG Dev Fest London

Luca Nicoletti

November 16, 2019
Tweet

More Decks by Luca Nicoletti

Other Decks in Programming

Transcript

  1. luca_nicolett MVI fun render(state: ViewState) { /* ... */ //

    show or hide some views based on state // add items to recycler // show a dialog /* ... */ }
  2. luca_nicolett Android - “old” <LinearLayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:layout_gravity="center_horizontal"> <TextView

    android:layout_width="match_parent" android:layout_height="wrap_content" android:text="First block" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Second block" /> <TextView android:layout_width="match_parent" android:layout_height="wrap_content" android:text="Third block" /> </LinearLayout>
  3. luca_nicolett Android - Jetpack Compose Column ( crossAxisAlignment = Center

    ) { Text("First block") Text("Second block") Text("Third block") }
  4. luca_nicolett Jetpack Compose • Declarative UI • Concise & Idiomatic

    • Stateless or Stateful components • Reusable components • Compatible
  5. luca_nicolett 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"
  6. luca_nicolett 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"
  7. luca_nicolett 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"
  8. luca_nicolett Jetpack Compose • Declarative UI • Concise & Idiomatic

    • Stateless or Stateful components • Reusable components • Compatible • Unbundled from the OS
  9. luca_nicolett Jetpack Compose buildFeatures { // Enables Jetpack Compose for

    this module compose true } // Set both the Java and Kotlin compilers to target Java 8. compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 } kotlinOptions { jvmTarget = "1.8" }
  10. luca_nicolett Jetpack Compose dependencies { // Include the following Compose

    toolkit dependencies. implementation "androidx.ui:ui-tooling:0.1.0-dev02" implementation "androidx.ui:ui-layout:0.1.0-dev02" implementation "androidx.ui:ui-material:0.1.0-dev02" }
  11. luca_nicolett Jetpack Compose class MyActivity: Activity() { override fun onCreate(savedInstanceState:

    Bundle?) { super.onCreate(savedInstanceState) setContent { MyComposableFunction() } } }
  12. luca_nicolett Jetpack Compose class MyActivity: Activity() { override fun onCreate(savedInstanceState:

    Bundle?) { super.onCreate(savedInstanceState) setContent { MyComposableFunction() } } }
  13. luca_nicolett Jetpack Compose fun Activity.setContent(content: @Composable() () -> Unit) :

    CompositionContext? { val craneView = window.decorView .findViewById<ViewGroup>(android.R.id.content) .getChildAt(0) as? AndroidCraneView ?: AndroidCraneView(this) .also { setContentView(it) } val coroutineContext = Dispatchers.Main return Compose.composeInto(craneView.root, this) { WrapWithAmbients( craneView, this, coroutineContext ) { content() }
  14. luca_nicolett val craneView = window.decorView .findViewById<ViewGroup>(android.R.id.content) .getChildAt(0) as? AndroidCraneView ?:

    AndroidCraneView(this) .also { setContentView(it) } val coroutineContext = Dispatchers.Main return Compose.composeInto(craneView.root, this) { WrapWithAmbients( craneView, this, coroutineContext ) { content() } } } Jetpack Compose
  15. luca_nicolett Jetpack Compose @Composable fun Greeting(name: String) { Text (text

    = "Hello $name!") } @Preview @Composable fun PreviewGreeting() { Greeting("Android") }
  16. luca_nicolett Jetpack Compose @Composable fun Text(/* ... */) { /*

    ... */ Draw { canvas, _ -> internalSelection.value?.let { textDelegate.paintBackground( it.min, it.max, selectionColor, canvas ) } textDelegate.paint(canvas) } /* ... */ }
  17. luca_nicolett Jetpack Compose @Composable fun Text(/* ... */) { /*

    ... */ Draw { canvas, _ -> internalSelection.value?.let { textDelegate.paintBackground( it.min, it.max, selectionColor, canvas ) } textDelegate.paint(canvas) } /* ... */ }
  18. luca_nicolett Jetpack Compose @Composable fun Text(/* ... */) { /*

    ... */ Draw { canvas, _ -> internalSelection.value?.let { textDelegate.paintBackground( it.min, it.max, selectionColor, canvas ) } textDelegate.paint(canvas) // Paints the text onto the given canvas. } /* ... */ }
  19. luca_nicolett Jetpack Compose @Composable fun TextField(/* ... */) { //

    States val hasFocus = +state { false } val coords = +state<LayoutCoordinates?> { null } val inputSession = +state { NO_SESSION } /* ... */ }
  20. luca_nicolett Jetpack Compose /** * 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 { }
  21. luca_nicolett Jetpack Compose /** * [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. */
  22. luca_nicolett Jetpack Compose RawDragGestureDetector(DragObserver { distance -> //update animation value

    }) { PressGestureDetector({ position -> // animate to position }) { Container(60.dp, expanded = true) { DrawSeekBar(animValue.value) } } }
  23. luca_nicolett Jetpack Compose RawDragGestureDetector(DragObserver { distance -> //update animation value

    }) { PressGestureDetector({ position -> // animate to position }) { Container(60.dp, expanded = true) { DrawSeekBar(animValue.value) } } }
  24. luca_nicolett Jetpack Compose RawDragGestureDetector(DragObserver { distance -> //update animation value

    }) { PressGestureDetector({ position -> // animate to position }) { Container(60.dp, expanded = true) { DrawSeekBar(animValue.value) } } }
  25. luca_nicolett Jetpack Compose RawDragGestureDetector(DragObserver { distance -> //update animation value

    }) { PressGestureDetector({ position -> // animate to position }) { Container(60.dp, expanded = true) { DrawSeekBar(animValue.value) } } }
  26. luca_nicolett Jetpack Compose @Composable fun DrawSeekBar(x: Float) { var paint

    = +memo { Paint() } Draw { canvas, parentSize -> /* ... */ canvas.drawRect(Rect(/* ... */), paint) canvas.drawRect(Rect(/* ... */), paint) canvas.drawCircle(/* ... */, paint) } }
  27. luca_nicolett Jetpack Compose @Composable fun DrawSeekBar(x: Float) { var paint

    = +memo { Paint() } Draw { canvas, parentSize -> /* ... */ canvas.drawRect(Rect(/* ... */), paint) canvas.drawRect(Rect(/* ... */), paint) canvas.drawCircle(/* ... */, paint) } }
  28. luca_nicolett Jetpack Compose @Composable fun DrawSeekBar(x: Float) { var paint

    = +memo { Paint() } Draw { canvas, parentSize -> /* ... */ canvas.drawRect(Rect(/* ... */), paint) canvas.drawRect(Rect(/* ... */), paint) canvas.drawCircle(/* ... */, paint) } }
  29. luca_nicolett Jetpack Compose @Composable fun RippleRect() { val radius =

    withDensity(+ambientDensity()) { TargetRadius.toPx() } val toState = +state { ButtonStatus.Initial } val rippleTransDef = +memo { createTransDef(radius.value) } val onPress: (PxPosition) -> Unit = { p -> down.x = p.x.value down.y = p.y.value toState.value = ButtonStatus.Pressed }
  30. luca_nicolett TargetRadius.toPx() } val toState = +state { ButtonStatus.Initial }

    val rippleTransDef = +memo { createTransDef(radius.value) } val onPress: (PxPosition) -> Unit = { p -> down.x = p.x.value down.y = p.y.value toState.value = ButtonStatus.Pressed } val onRelease: () -> Unit = { toState.value = ButtonStatus.Released } PressGestureDetector(onPress, onRelease) { Container(true) { Transition( definition = rippleTransDef, toState = toState.value Jetpack Compose
  31. luca_nicolett } val onRelease: () -> Unit = { toState.value

    = ButtonStatus.Released } PressGestureDetector(onPress, onRelease) { Container(true) { Transition( definition = rippleTransDef, toState = toState.value ) { state -> RippleRectFromState(state) } } } } Jetpack Compose
  32. luca_nicolett } val onRelease: () -> Unit = { toState.value

    = ButtonStatus.Released } PressGestureDetector(onPress, onRelease) { Container(true) { Transition( definition = rippleTransDef, toState = toState.value ) { state -> RippleRectFromState(state) } } } } Jetpack Compose
  33. luca_nicolett Jetpack Compose @Composable fun RippleRectFromState(state: TransitionState) { // TODO:

    file bug for when "down" is not a // file level val, it's not memoized correctly val x = down.x val y = down.y val paint = Paint().apply { color = Color( alpha = getAlpha(), red = 0, green = 235, blue = 224 )
  34. luca_nicolett // file level val, it's not memoized correctly val

    x = down.x val y = down.y val paint = Paint().apply { color = Color( alpha = getAlpha(), red = 0, green = 235, blue = 224 ) } val radius = state[radius] Draw { canvas, _ -> canvas.drawCircle( Offset(x, y), Jetpack Compose
  35. luca_nicolett blue = 224 ) } val radius = state[radius]

    Draw { canvas, _ -> canvas.drawCircle( Offset(x, y), radius, paint ) } } Jetpack Compose
  36. luca_nicolett Jetpack Compose @Composable fun AlertDialog( onCloseRequest: () -> Unit,

    title: (@Composable() () -> Unit)? = null, text: (@Composable() () -> Unit), buttons: @Composable() () -> Unit ) { val currentColors = +ambient(Colors) val currentTypography = +ambient(Typography) Dialog(onCloseRequest = onCloseRequest) { /* ... */ } }
  37. luca_nicolett Jetpack Compose @Composable fun AlertDialog( onCloseRequest: () -> Unit,

    title: (@Composable() () -> Unit)? = null, text: (@Composable() () -> Unit), buttons: @Composable() () -> Unit ) { val currentColors = +ambient(Colors) val currentTypography = +ambient(Typography) Dialog(onCloseRequest = onCloseRequest) { /* ... */ } }
  38. luca_nicolett Jetpack Compose @Composable fun AlertDialog( onCloseRequest: () -> Unit,

    title: (@Composable() () -> Unit)? = null, text: (@Composable() () -> Unit), buttons: @Composable() () -> Unit ) { val currentColors = +ambient(Colors) val currentTypography = +ambient(Typography) Dialog(onCloseRequest = onCloseRequest) { /* ... */ } }
  39. luca_nicolett Jetpack Compose @Composable fun Dialog( onCloseRequest: () -> Unit,

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

    children: @Composable() () -> Unit ) { val context = +ambient(ContextAmbient) val dialog = +memo { DialogWrapper(context, onCloseRequest) } +onActive { dialog.show() onDispose {
  41. luca_nicolett val dialog = +memo { DialogWrapper(context, onCloseRequest) } +onActive

    { dialog.show() onDispose { dialog.dismiss() dialog.disposeComposition() } } +onCommit { dialog.setContent(children) } } Jetpack Compose
  42. luca_nicolett val dialog = +memo { DialogWrapper(context, onCloseRequest) } +onActive

    { dialog.show() onDispose { dialog.dismiss() dialog.disposeComposition() } } +onCommit { dialog.setContent(children) } } Jetpack Compose
  43. luca_nicolett Jetpack Compose data class PaddingModifier( val left: IntPx =

    0.ipx, val top: IntPx = 0.ipx, val right: IntPx = 0.ipx, val bottom: IntPx = 0.ipx ) : LayoutModifier { /* ... */ }
  44. luca_nicolett Jetpack Compose /** * An immutable chain of [modifier

    elements][Modifier.Element] * for use with Composables. A Composable that has a `Modifier` * can be considered decorated or wrapped by that `Modifier`. * * Composables that accept a [Modifier] as a parameter to be * applied to the whole component represented by the composable * function should name the parameter `modifier` and assign the * parameter a default value of [Modifier.None] */
  45. luca_nicolett Jetpack Compose Padding(16.dp) { Text("Hello ") } Text( text

    = "Hello ", modifier = PaddingModifier(16.dp) ) VS
  46. luca_nicolett All together fun modelState() = +effectOf<ViewState> { var result

    by +state { ViewState.Idle } var disposable: Disposable? = null +onActive { disposable = intentsObserver .applyBusinessLogic() .subscribe { result.value = it } } +onDispose { disposable?.dispose() } result.value
  47. luca_nicolett All together fun modelState() = +effectOf<ViewState> { var result

    by +state { ViewState.Idle } var disposable: Disposable? = null +onActive { disposable = intentsObserver .applyBusinessLogic() .subscribe { result = it } } +onDispose { disposable?.dispose() } result
  48. luca_nicolett fun modelState() = +effectOf<ViewState> { var result by +state

    { ViewState.Idle } var disposable: Disposable? = null +onActive { disposable = intentsObserver .applyBusinessLogic() .subscribe { result = it } } +onDispose { disposable?.dispose() } result All together
  49. luca_nicolett var result by +state { ViewState.Idle } var disposable:

    Disposable? = null +onActive { disposable = intentsObserver .applyBusinessLogic() .subscribe { result = it } } +onDispose { disposable?.dispose() } result } All together
  50. luca_nicolett var result by +state { ViewState.Idle } var disposable:

    Disposable? = null +onActive { disposable = intentsObserver .applyBusinessLogic() .subscribe { result = it } } +onDispose { disposable?.dispose() } result } All together
  51. luca_nicolett All together /** * Disposes of a composition that

    was started using * [setContent]. This is a convenience method around * [Compose.disposeComposition] */ fun Activity.disposeComposition() { val view = window .decorView .findViewById<ViewGroup>(android.R.id.content) .getChildAt(0) as? ViewGroup ?: error("No root view found") Compose.disposeComposition(view, null) }
  52. luca_nicolett +onActive { disposable = intentsObserver .applyBusinessLogic() .subscribe { result

    = it } } +onDispose { disposable?.dispose() } result } All together
  53. luca_nicolett All together @Composable fun MyActivityScreen( state: ViewState ) {

    when (state) { ViewState.InProgress -> { Container(expanded = true) { CircularProgressIndicator() } } /* ... */ } }
  54. luca_nicolett 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 Compose https://www.youtube.com/watch?v=VsStyq4Lzxo https://medium.com/q42-engineering/android-jetpack-compose-895b7fd04bf4

    https://blog.karumi.com/android-jetpack-compose-review https://courses.csail.mit.edu/6.831/2006/lectures/L9.pdf http://intelligiblebabble.com/compose-from-first-principles/ http://intelligiblebabble.com/content-on-declarative-ui/ https://www.youtube.com/watch? v=Q9MtlmmN4Q0&list=PLWz5rJ2EKKc_xXXubDti2eRnIKU0p7wHd&index=60 https://www.youtube.com/watch? v=SPsdRXcgqjI&list=PLWz5rJ2EKKc_xXXubDti2eRnIKU0p7wHd&index=8&t=0s https://www.youtube.com/watch? v=XPMrnR1_Biw&list=PLWz5rJ2EKKc_xXXubDti2eRnIKU0p7wHd&index=13&t=0s