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

MVI with Jetpack Compose

MVI with Jetpack Compose

At @Mobiconf 2019

Luca Nicoletti

October 04, 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 Jetpack Compose • Declarative UI • Concise & Idiomatic

    • Stateless or Stateful components • Reusable components • Compatible
  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 @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 • Declarative UI • Concise & Idiomatic

    • Stateless or Stateful components • Reusable components • Compatible • Unbundled from the OS
  10. 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
  11. 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)
  12. luca_nicolett Jetpack Compose class MyActivity: Activity() { override fun onCreate(savedInstanceState:

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

    Bundle?) { super.onCreate(savedInstanceState) setContent { MyComposableFunction() } } }
  14. 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 ) {
  15. 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 ) {
  16. 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
  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) }
 /* ... */ }
  19. 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. }
 /* ... */ }
  20. luca_nicolett Jetpack Compose @Composable fun TextField(/* ... */) { //

    States val hasFocus = +state { false } val coords = +state<LayoutCoordinates?> { null } /* ... */ }
  21. 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 { /* ... */ }
  22. 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. */
  23. 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
  24. luca_nicolett Jetpack Compose @Composable
 fun RallyBody() {
 Padding(padding = 16.dp)

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

    {
 VerticalScroller {
 Column {
 RallyAlertCard()
 HeightSpacer(height = 10.dp)
 RallyAccountsCard()
 }
 }
 } }
  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 AlertDialog( onCloseRequest: () -> Unit,

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

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

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

    children: @Composable() () -> Unit ) { +onCommit { dialog.setContent(children) } }
  31. 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 = 
 { position -> down.x = position.x.value
  32. luca_nicolett val rippleTransDef = +memo { 
 createTransDef(radius.value) 
 }

    val onPress: (PxPosition) -> Unit = 
 { position -> down.x = position.x.value down.y = position.y.value toState.value = ButtonStatus.Pressed } val onRelease: () -> Unit = { toState.value = ButtonStatus.Released } PressGestureDetector(onPress = onPress, onRele Container(expanded = true) { Transition( definition = rippleTransDef, toState = toState.value Jetpack Compose
  33. luca_nicolett val onRelease: () -> Unit = { toState.value =

    ButtonStatus.Released } PressGestureDetector( onPress = onPress, onRelease = onRelease ) { Container(expanded = true) { Transition( definition = rippleTransDef, toState = toState.value ) { state -> RippleRectFromState(state) } } } } Jetpack Compose
  34. 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 = /* ... */ red = 0, green = 235, Jetpack Compose
  35. 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 = /* ... */ red = 0, green = 235, blue = 224 ) } val radius = state[radius] Draw { canvas, _ -> Jetpack Compose
  36. luca_nicolett red = 0, green = 235, blue = 224

    ) } val radius = state[radius] Draw { canvas, _ -> canvas.drawCircle(
 Offset(x, y), radius, paint
 ) } } Jetpack Compose
  37. luca_nicolett Jetpack Compose @RunWith(JUnit4::class) @get:Rule val composeTestRule = createComposeRule() @Test


    fun topAppBar_expandsToScreen() {
 val dm = composeTestRule.displayMetrics
 composeTestRule
 .setMaterialContentAndCollectSizes {
 TopAppBar<Nothing>()
 }
 .assertHeightEqualsTo(appBarHeight)
 .assertWidthEqualsTo { dm.widthPixels.ipx } }
  38. luca_nicolett Jetpack Compose @RunWith(JUnit4::class) @get:Rule val composeTestRule = createComposeRule() @Test


    fun topAppBar_expandsToScreen() {
 val dm = composeTestRule.displayMetrics
 composeTestRule
 .setMaterialContentAndCollectSizes {
 TopAppBar<Nothing>()
 }
 .assertHeightEqualsTo(appBarHeight)
 .assertWidthEqualsTo { dm.widthPixels.ipx } }
  39. luca_nicolett Jetpack Compose @RunWith(JUnit4::class) @get:Rule val composeTestRule = createComposeRule() @Test


    fun topAppBar_expandsToScreen() {
 val dm = composeTestRule.displayMetrics
 composeTestRule
 .setMaterialContentAndCollectSizes {
 TopAppBar<Nothing>()
 }
 .assertHeightEqualsTo(appBarHeight)
 .assertWidthEqualsTo { dm.widthPixels.ipx } }
  40. luca_nicolett All together object InProgress : BaseViewState(true, null) { @Composable

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

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

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

    = /* view model init */
 setContent {
 CustomTheme {
 render(viewModel.states())
 }
 }
 /* .. */
 }
  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 render( observableState: Observable<BaseViewState> ) {

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

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

    val state = +observe(ViewState.Idle, observableState) state.buildUI() }
  48. 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 }
  49. 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 }
  50. 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 }
  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 override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //

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

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

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

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

    viewModel = /* ... */ not needed anymore setContent { PostListScreen( processIntents(intents()), reloadPostListIntentPublisher ) } }
  59. 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
  60. 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
  61. 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
  62. 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
  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 All together override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) //

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

    when (state) { PostListViewState.InProgress -> { Container(expanded = true) { CircularProgressIndicator() } } /* ... */ } }