Slide 1

Slide 1 text

MVI with Jetpack Compose

Slide 2

Slide 2 text

About me ● Luca ● Italian ● Android Engineer @Babylon Health ● Based in London @luca_nicolett

Slide 3

Slide 3 text

MVI Jetpack Compose Topics

Slide 4

Slide 4 text

MVI

Slide 5

Slide 5 text

MVI MODEL INTENT VIEW USER Updates Sees Manipulates Interacts

Slide 6

Slide 6 text

MVI Core principles: ● Unidirectional ● Immutability ● Reactiveness ● Functional

Slide 7

Slide 7 text

MVI

Slide 8

Slide 8 text

MVI ACTIONS BUSINESS LOGIC EVENTS REDUCER STATE STATE UI LISTENS UI

Slide 9

Slide 9 text

MVI ACTIONS ACTION RELAY TRANSFORMERS REDUCER STATE UPDATES SOMETHING ACTIONS EVENTS STATE STATE RELAY

Slide 10

Slide 10 text

MVI ● Actions ● Transformers ● Reducers ● State

Slide 11

Slide 11 text

MVI We also added middleware ● Works as a glue ● Binds actions to transformers ● Transformations to reducers ● Or actions to reducers directly

Slide 12

Slide 12 text

MVI perform("rating selection") .on() .withReducer(reducers::reduceRatingSelected)

Slide 13

Slide 13 text

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)

Slide 14

Slide 14 text

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)

Slide 15

Slide 15 text

MVI How does it help? ● Decoupling ● Testing

Slide 16

Slide 16 text

MVI

Slide 17

Slide 17 text

MVI

Slide 18

Slide 18 text

MVI ACTIVITY FRAGMENT render() RENDERER

Slide 19

Slide 19 text

MVI private fun render(state: HomeScreenRedesignState) { loading_container .show(state.loadingState == LoadingState.Loading) unable_to_connect_error_container .show(state.loadingState == LoadingState.Error) /* ... */ }

Slide 20

Slide 20 text

MVI fun View.show(isVisible: Boolean = true) { this.visibility = if (isVisible) View.VISIBLE else View.GONE }

Slide 21

Slide 21 text

MVI private fun render(state: EditMoodState) { submitEditsButton?.isEnabled = state.hasAppliedChanges /* ... */ }

Slide 22

Slide 22 text

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

Slide 23

Slide 23 text

MVI

Slide 24

Slide 24 text

Jetpack Compose

Slide 25

Slide 25 text

Jetpack Compose

Slide 26

Slide 26 text

Jetpack Compose

Slide 27

Slide 27 text

Jetpack Compose ● Declarative UI ● Concise and idiomatic ● Stateless or Stateful components ● Reusable components ● Compatible

Slide 28

Slide 28 text

Jetpack Compose @Composable @GenerateView fun Greetings(name: String) { /* ... */ } val greetingsView = findViewById(R.id.greetings) greetingsView.name = "Luca"

Slide 29

Slide 29 text

Jetpack Compose @Composable @GenerateView fun Greetings(name: String) { /* ... */ } val greetingsView = findViewById(R.id.greetings) greetingsView.name = "Luca"

Slide 30

Slide 30 text

Jetpack Compose @Composable @GenerateView fun Greetings(name: String) { /* ... */ } val greetingsView = findViewById(R.id.greetings) greetingsView.name = "Luca"

Slide 31

Slide 31 text

Jetpack Compose ● Declarative UI ● Concise and idiomatic ● Stateless or Stateful components ● Reusable components ● Compatible ● Unbundled from OS

Slide 32

Slide 32 text

Jetpack Compose

Slide 33

Slide 33 text

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

Slide 34

Slide 34 text

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

Slide 35

Slide 35 text

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)

Slide 36

Slide 36 text

cd path/to/checkout/frameworks/support/ ./studiow Jetpack Compose

Slide 37

Slide 37 text

Jetpack Compose

Slide 38

Slide 38 text

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

Slide 39

Slide 39 text

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

Slide 40

Slide 40 text

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

Slide 41

Slide 41 text

val craneView = window.decorView .findViewById(android.R.id.content) .getChildAt(0) as? AndroidCraneView ?: AndroidCraneView(this).also { setContentView(it) } Jetpack Compose

Slide 42

Slide 42 text

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

Slide 43

Slide 43 text

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

Slide 44

Slide 44 text

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

Slide 45

Slide 45 text

Jetpack Compose

Slide 46

Slide 46 text

Jetpack Compose

Slide 47

Slide 47 text

@Composable fun TextField(/* ... */) { // States val hasFocus = +state { false } val coords = +state { null } /* ... */ } Jetpack Compose

Slide 48

Slide 48 text

@CheckResult("+") /* inline */ fun state(init: () -> T) = memo { State(init()) } Jetpack Compose

Slide 49

Slide 49 text

/** * 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 @PublishedApi internal constructor(value:T) : Framed { /* ... */ } Jetpack Compose

Slide 50

Slide 50 text

/** * [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

Slide 51

Slide 51 text

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

Slide 52

Slide 52 text

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

Slide 53

Slide 53 text

@Model data class TaskModel( var isDone: Boolean, val description: String ): BaseModel() TaskModel.kt: (14, 3): Model objects do not support inheritance Jetpack Compose

Slide 54

Slide 54 text

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

Slide 55

Slide 55 text

@Composable fun RallyBody() { Padding(padding = 16.dp) { VerticalScroller { Column { RallyAlertCard() HeightSpacer(height = 10.dp) RallyAccountsCard() } } } } Jetpack Compose

Slide 56

Slide 56 text

Jetpack Compose

Slide 57

Slide 57 text

Jetpack Compose

Slide 58

Slide 58 text

@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

Slide 59

Slide 59 text

Jetpack Compose

Slide 60

Slide 60 text

Text(text = "Row") ContainerWithBackground( width = ExampleSize, color = lightGrey ) { Row { PurpleSquare() CyanSquare() } } Jetpack Compose

Slide 61

Slide 61 text

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

Slide 62

Slide 62 text

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

Slide 63

Slide 63 text

Jetpack Compose ContainerWithBackground( height = ExampleSize, color = lightGrey ) { Column( crossAxisAlignment = CrossAxisAlignment.End ) { PurpleSquare() CyanSquare() } }

Slide 64

Slide 64 text

Jetpack Compose

Slide 65

Slide 65 text

@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

Slide 66

Slide 66 text

@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

Slide 67

Slide 67 text

@Composable fun RippleRect() { /* ... */ val onRelease: () -> Unit = { toState.value = ButtonStatus.Released } /* ... */ } Jetpack Compose

Slide 68

Slide 68 text

PressGestureDetector(onPress, onRelease) { Container(expanded = true) { Transition(toState = toState.value) { s-> RippleRectFromState(s) } } } Jetpack Compose

Slide 69

Slide 69 text

@Composable fun RippleRectFromState(state: TransitionState) { Draw { canvas, _ -> canvas.drawCircle( Offset(x, y), radius, paint ) } } Jetpack Compose

Slide 70

Slide 70 text

Jetpack Compose

Slide 71

Slide 71 text

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

Slide 72

Slide 72 text

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

Slide 73

Slide 73 text

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

Slide 74

Slide 74 text

Jetpack Compose @Composable fun Dialog( onCloseRequest: () -> Unit, children: @Composable() () -> Unit ) { +onCommit { dialog.setContent(children) } }

Slide 75

Slide 75 text

@RunWith(JUnit4::class) @get:Rule val composeTestRule = createComposeRule() Jetpack Compose

Slide 76

Slide 76 text

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

Slide 77

Slide 77 text

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

Slide 78

Slide 78 text

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

Slide 79

Slide 79 text

All together

Slide 80

Slide 80 text

All together Functional approach OOP approach

Slide 81

Slide 81 text

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

Slide 82

Slide 82 text

All together object InProgress : BaseViewState(true, null) { @Composable override fun buildUI() { Container(expanded = true) { CircularProgressIndicator() } } }

Slide 83

Slide 83 text

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

Slide 84

Slide 84 text

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

Slide 85

Slide 85 text

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

Slide 86

Slide 86 text

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

Slide 87

Slide 87 text

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

Slide 88

Slide 88 text

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

Slide 89

Slide 89 text

fun observe(initialState: T, data: Observable) = effectOf { val result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } return result.value } All together

Slide 90

Slide 90 text

fun observe(initialState: T, data: Observable) = effectOf { val result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } return result.value } All together

Slide 91

Slide 91 text

fun observe(initialState: T, data: Observable) = effectOf { val result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } return result.value } All together

Slide 92

Slide 92 text

fun observe(initialState: T, data: Observable) = effectOf { val result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } return result.value } All together

Slide 93

Slide 93 text

fun observe(initialState: T, data: Observable) = effectOf { val result = +state { initialState } +onActive { val disposable = data.subscribe { newValue -> result.value = newValue } onDispose { disposable.dispose() } } return result.value } All together

Slide 94

Slide 94 text

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

Slide 95

Slide 95 text

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

Slide 96

Slide 96 text

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

Slide 97

Slide 97 text

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

Slide 98

Slide 98 text

All together fun processIntents(intents: Observable): PostListViewState { intents.subscribe(intentsObserver) return modelState() } }

Slide 99

Slide 99 text

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

Slide 100

Slide 100 text

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

Slide 101

Slide 101 text

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

Slide 102

Slide 102 text

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

Slide 103

Slide 103 text

All together +onActive { disposable = intentsObserver .applyBusinessLogic() .subscribe { result.value = it } }

Slide 104

Slide 104 text

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

Slide 105

Slide 105 text

All together @Composable fun PostListScreen( state: PostListViewState ) { when (state) { PostListViewState.InProgress -> { /* ... */ } } }

Slide 106

Slide 106 text

All together PostListScreen(processIntents(intents()))

Slide 107

Slide 107 text

All together

Slide 108

Slide 108 text

All together PostListScreen( processIntents( intents() ) )

Slide 109

Slide 109 text

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/

Slide 110

Slide 110 text

Thank you @luca_nicolett

Slide 111

Slide 111 text

Questions?