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

MVI with Jetpack Compose

MVI with Jetpack Compose

Slides for Mobilization IX

Avatar for Luca Nicoletti

Luca Nicoletti

October 26, 2019
Tweet

More Decks by Luca Nicoletti

Other Decks in Programming

Transcript

  1. luca_nicolett MVI The middleware • Works as a glue •

    Binds actions to transformers • Transformations to reducers • Actions to reducers
  2. luca_nicolett 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)
  3. luca_nicolett 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)
  4. luca_nicolett MVI private fun render(state: HomeScreenRedesignState) { loading_container .show(state.loadingState ==

    LoadingState.Loading) unable_to_connect_error_container .show(state.loadingState == LoadingState.Error) /* ... */ }
  5. 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>
  6. luca_nicolett Android - Jetpack Compose Column ( crossAxisAlignment = Center

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

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

    • Stateless or Stateful components • Reusable components • Compatible • Unbundled from the OS
  12. 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" }
  13. luca_nicolett Jetpack Compose dependencies { // Include the following Compose

    toolkit dependencies. implementation "androidx.ui:ui-framework:0.1.0-dev02" 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" }
  14. luca_nicolett 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
  15. luca_nicolett 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)
  16. luca_nicolett Jetpack Compose class MyActivity: Activity() { override fun onCreate(savedInstanceState:

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

    Bundle?) { super.onCreate(savedInstanceState) setContent { MyComposableFunction() } } }
  18. 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 ) {
  19. 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 ) {
  20. luca_nicolett .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
  21. luca_nicolett Jetpack Compose @Composable fun Greeting(name: String) { Text (text

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


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


    /* ... */ Draw { canvas, _ -> internalSelection.value?.let { textDelegate.paintBackground( it.min, it.max, selectionColor, canvas ) } textDelegate.paint(canvas) }
 /* ... */ }
  24. 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. }
 /* ... */ }
  25. luca_nicolett Jetpack Compose @Composable fun TextField(/* ... */) { //

    States val hasFocus = +state { false } val coords = +state<LayoutCoordinates?> { null } /* ... */ }
  26. 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 { }
  27. 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. */
  28. luca_nicolett Jetpack Compose @Model data class TaskModel( var isDone: Boolean,

    val description: String ): BaseModel() TaskModel.kt: (14, 3): Model objects do not support inheritance
  29. 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) } }
  30. luca_nicolett Jetpack Compose @Composable fun AlertDialog( onCloseRequest: () -> Unit,

    title: (@Composable() () -> Unit)? = null, text: (@Composable() () -> Unit), confirmButton: (@Composable() () -> Unit), dismissButton: (@Composable() () -> Unit)?, buttonLayout: AlertDialogButtonLayout ) { Dialog(onCloseRequest = onCloseRequest) { /* ... */ } }
  31. luca_nicolett Jetpack Compose @Composable fun Dialog( onCloseRequest: () -> Unit,

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

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

    children: @Composable() () -> Unit ) { +onCommit { dialog.setContent(children) } }
  34. 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
  35. luca_nicolett Jetpack Compose } 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) {
  36. luca_nicolett Jetpack Compose } val onRelease: () -> Unit =

    { toState.value = ButtonStatus.Released } PressGestureDetector(onPress, onRelease) { Container(true) { Transition( definition = rippleTransDef, toState = toState.value ) { state -> RippleRectFromState(state = state) } } } }
  37. luca_nicolett @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, Jetpack Compose
  38. 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] Jetpack Compose
  39. luca_nicolett green = 235, blue = 224 ) } val

    radius = state[radius] Draw { canvas, _ -> canvas.drawCircle( Offset(x, y), radius, paint ) } } Jetpack Compose
  40. luca_nicolett canvas.drawCircle( Offset(x, y), radius, paint ) } } fun

    getAlpha() = state[ androidx.ui.animation.demos.alpha ] * 255).toInt() Jetpack Compose
  41. 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 { /* ... */ }
  42. 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] */
  43. luca_nicolett All together object InProgress : BaseViewState(true, null) { @Composable

    override fun buildUI() { Container(expanded = true) { CircularProgressIndicator() } } }
  44. luca_nicolett All together override fun onCreate(savedInstanceState: Bundle?) {
 super.onCreate(savedInstanceState)
 viewModel

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

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

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

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

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

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

    val state = +observe(ViewState.Idle, observableState) state.buildUI() }
  51. luca_nicolett All together 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 }
  52. luca_nicolett All together 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 }
  53. luca_nicolett All together 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 }
  54. luca_nicolett All together 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 }
  55. luca_nicolett All together 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 }
  56. luca_nicolett All together 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 }
  57. 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) }
  58. luca_nicolett All together /** * Disposes any composition previously run

    with [container] * as the root. This will release any resources that have * been built around the composition, including all * [onDispose] callbacks that have been registered with * [CompositionLifecycleObserver] objects. */ @MainThread fun disposeComposition( container: ViewGroup, parent: CompositionReference? = null ) {
  59. luca_nicolett * [onDispose] callbacks that have been registered with *

    [CompositionLifecycleObserver] objects. */ @MainThread fun disposeComposition( container: ViewGroup, parent: CompositionReference? = null ) { // need to remove compositionContext from context map composeInto(container, parent) { } container.setTag(TAG_ROOT_COMPONENT, null) } All together
  60. luca_nicolett All together 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 }
  61. luca_nicolett All together fun modelState() = +effectOf<PostListViewState> { val result

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

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

    { PostListViewState.Idle } var disposable: Disposable? = null +onActive { disposable = intentsObserver .applyBusinessLogic() .subscribe { result.value = it } } +onDispose { disposable?.dispose() } result.value } All together
  64. luca_nicolett val result = +state { PostListViewState.Idle } var disposable:

    Disposable? = null +onActive { disposable = intentsObserver .applyBusinessLogic() .subscribe { result.value = it } } +onDispose { disposable?.dispose() } result.value } All together
  65. luca_nicolett +onActive { disposable = intentsObserver .applyBusinessLogic() .subscribe { result.value

    = it } } +onDispose { disposable?.dispose() } result.value } All together
  66. luca_nicolett All together @Composable fun PostListScreen( state: PostListViewState ) {

    when (state) { PostListViewState.InProgress -> { Container(expanded = true) { CircularProgressIndicator() } } /* ... */ } }
  67. 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