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

MVI with Jetpack Compose

MVI with Jetpack Compose

Slides for Mobilization IX

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