Slide 1

Slide 1 text

Most Valuable (Architecture) Idea Aida Issayeva

Slide 2

Slide 2 text

About me 󰠁 Software Engineer 🤖 I build Android apps 󰠅 Udacity Instructor 🐦 @aida_isay 🌐 cupsofcode.com

Slide 3

Slide 3 text

Current State of Affairs in Android App

Slide 4

Slide 4 text

Current State of Affairs in Android App 1. Many Asynchronous Executions;

Slide 5

Slide 5 text

Current State of Affairs in Android App 1. Many Asynchronous Executions; 2. Updates to the states from random places;

Slide 6

Slide 6 text

Current State of Affairs in Android App 1. Many Asynchronous Executions; 2. Updates to the states from random places; 3. Different States in Widgets, Fragments/Activities, ViewModels, Domain and Data Layers, etc;

Slide 7

Slide 7 text

MVI Model - View - Intent

Slide 8

Slide 8 text

VIEW VIEWMODEL USECASES / REPOSITORY RENDER INTENTS USER Intent State REDUCER PROCESSOR

Slide 9

Slide 9 text

VIEW VIEWMODEL USECASES / REPOSITORY RENDER INTENTS USER Intent State REDUCER PROCESSOR

Slide 10

Slide 10 text

Model as a State data class ViewState( val isLoading: Boolean = true, val error: Throwable? = null, val itemName: String = "", val totalPrice: Int = 0, val isButtonEnabled: Boolean = false )

Slide 11

Slide 11 text

VIEW VIEWMODEL USECASES / REPOSITORY RENDER INTENTS USER Intent State REDUCER PROCESSOR

Slide 12

Slide 12 text

Render Model to View private fun render(context: Context) { mviViewModel.bindIntents(intents.hide()) .subscribe({ viewstate -> binding.loadingBar.isVisible = viewstate.isLoading switchError(viewstate.error) binding.button.isEnabled = viewstate.isButtonEnabled binding.itemName.text = viewstate.itemName …. }, { …. }) }

Slide 13

Slide 13 text

VIEW VIEWMODEL USECASES / REPOSITORY RENDER INTENTS USER Intent State REDUCER PROCESSOR

Slide 14

Slide 14 text

Intents are passed to VM private fun render(context: Context) { mviViewModel.bindIntents(intents.hide()) .subscribe({ viewstate -> …. }, { …. }) } binding.button.setOnClickListener { intents.onNext(MVIView.Intent.ButtonClicked) }

Slide 15

Slide 15 text

VIEW VIEWMODEL USECASES / REPOSITORY RENDER INTENTS USER Intent State REDUCER PROCESSOR

Slide 16

Slide 16 text

Intent is an Action / Event / Input / etc sealed class Intent { // data data class Data(val maxNumberInDropdown: Int, val itemName: String, val itemPrice: Int) : Intent() data class Error(val throwable: Throwable) : Intent() object Loading : Intent() // user actions data class DropdownNumberSelected(val number: Int) : Intent() object ButtonClicked : Intent() object OnBackClicked : Intent() object DialogDismissed: Intent() }

Slide 17

Slide 17 text

VIEW VIEWMODEL USECASES / REPOSITORY RENDER INTENTS USER Intent State REDUCER PROCESSOR

Slide 18

Slide 18 text

Processing Intents: Transformation private fun bindIntent(viewIntents: Observable): Observable { val transformedIntents = viewIntents.publish { val buttonClicked = it.ofType(ButtonClicked::class.java) .debounce(300, TimeUnit.MILLISECONDS) .switchMap { ThrowErrorUseCase.execute() .map { NoOp } .onErrorReturn { Error(throwable = it) } .startWith(Loading) } …. Observable.merge( buttonClicked, … ) } return Observable.merge(transformedIntents, … , …) }

Slide 19

Slide 19 text

Processing Intents: Data private fun bindIntent(viewIntents: Observable): Observable { …. val dataIntent = GetItemUseCase.execute() .map { Data( itemPrice = it.price.toInt(), itemName = it.name, maxNumberInDropdown = it.maxCount ) } .onErrorReturn { Error(throwable = it) } .startWith(Loading) return Observable.merge(..., dataIntent, …) }

Slide 20

Slide 20 text

Processing Intents: Pass Through private fun bindIntent(viewIntents: Observable): Observable { …. return Observable.merge(..., … , viewIntents) }

Slide 21

Slide 21 text

VIEW VIEWMODEL USECASES / REPOSITORY RENDER INTENTS USER Intent State REDUCER PROCESSOR

Slide 22

Slide 22 text

Reducing Intent to State fun bindIntents(viewIntents: Observable): Observable { return bindIntent(viewIntents) .subscribeOn(Schedulers.io()) .scanWith(initialState, reducer ) .onErrorReturn { ViewState(error = it) } .distinctUntilChanged() }

Slide 23

Slide 23 text

Reducing Intent to State private val reducer = BiFunction { previousState, intent -> when (intent) { is Data -> previousState.copy( isLoading = false, itemName = intent.itemName, itemPrice = intent.itemPrice, dropdownValues = listOfNumbers(intent.maxNumberInDropdown) ) … else -> previousState } }

Slide 24

Slide 24 text

VIEW VIEWMODEL USECASES / REPOSITORY RENDER INTENTS USER Intent State REDUCER PROCESSOR

Slide 25

Slide 25 text

Where is the catch 🤔 ?

Slide 26

Slide 26 text

1. High Learning Curve; Caveats

Slide 27

Slide 27 text

Caveats 1. High Learning Curve; 2. Boilerplate code to setup;

Slide 28

Slide 28 text

Caveats 1. High Learning Curve; 2. Boilerplate code to setup; 3. Special handling of alert states, animations, etc;

Slide 29

Slide 29 text

Caveats 1. High Learning Curve; 2. Boilerplate code to setup; 3. Special handling of alert states, animations, etc; 4. Redundant UI rerendering.

Slide 30

Slide 30 text

Where to trip over 😮?

Slide 31

Slide 31 text

Common Mistakes 1. Over-engineering;

Slide 32

Slide 32 text

Common Mistakes 1. Over-engineering; 2. Partial State of the View;

Slide 33

Slide 33 text

Common Mistakes 1. Over-engineering; 2. Partial State of the View; 3. Concurrency issue

Slide 34

Slide 34 text

Debugging on the fly ✨

Slide 35

Slide 35 text

1. Single Source of Truth; Debugging

Slide 36

Slide 36 text

1. Single Source of Truth; 2. Unidirectional Data Flow; Debugging

Slide 37

Slide 37 text

1. Single Source of Truth; 2. Unidirectional Data Flow; 3. Immutable objects; Debugging

Slide 38

Slide 38 text

1. Single Source of Truth; 2. Unidirectional Data Flow; 3. Immutable objects; 4. Clear sequence of changes. Debugging

Slide 39

Slide 39 text

Testing on the fly ⚡

Slide 40

Slide 40 text

Testing @Test fun `should return a viewState with error when button is clicked`() { //given val viewState = MVIView.ViewState() val expectedError = Throwable("Something Bad Happened") val expectedStates = arrayOf( viewState, viewState.copy( error = expectedError, isLoading = false ) ) //when ….

Slide 41

Slide 41 text

Testing @Test fun `should return a viewState with error when button is clicked`() { //given …. //when val resultsObserver = viewmodel.bindIntents(intentSubject).test() intentSubject.onNext(MVIView.Intent.ButtonClicked) //then resultsObserver.assertValues(*expectedStates) }

Slide 42

Slide 42 text

Show us Code ⭐

Slide 43

Slide 43 text

No content

Slide 44

Slide 44 text

1. Consistency and predictability; MVI gives you:

Slide 45

Slide 45 text

1. Consistency and predictability; 2. Easy debugging, testing, reproducing of crashes; MVI gives you:

Slide 46

Slide 46 text

1. Consistency and predictability; 2. Easy debugging, testing, reproducing of crashes; 3. Focus on business needs. MVI gives you:

Slide 47

Slide 47 text

Resources 1. https:/ /hannesdorfmann.com/androi d/mosby3-mvi-1/ 2. https:/ /abhiappmobiledeveloper.med ium.com/android-mvi-reactive-archit ecture-pattern-74e5f1300a87 3. https:/ /www.raywenderlich.com/817 602-mvi-architecture-for-android-tuto rial-getting-started 4. https:/ /www.youtube.com/watch?v= UsuzhTlccRk&ab_channel=SquareEng ineering 5. https:/ /downloads.ctfassets.net/2gru fn031spf/6lC85sBYmQmSCoiqYiQOM o/a7ccea6ded95c6a1c5d3f46d74773 da1/Sergey_Ryabov_Kak_prigotovit_k horosho_prozharennyy_MVI_pod_And roid.pdf 6. https:/ /www.godaddy.com/engineeri ng/2021/11/05/android-state-manag ement-mvi/

Slide 48

Slide 48 text

@aida_isay cupsofcode.com Thank You