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

MVI made for Android

MVI made for Android

State management is one of the challenging task while developing softwares and recently MVI is one of the architecture pattern which promises to help us with it.
There have been multiple talks about "why" and "what" MVI is and there are several libraries and framework providing their own MVI implementation for Android.

In this talk, we will combine the power of MVI with MVVM [ViewModel and LiveData] to have peace with the platform rather than fight it.

Agenda
- What is MVI?
- Leverage the power of ViewModel and LiveData
- How Roxie to can help you do it?
- Testing with Roxie [MVI]
- Efficient workflow with MVI

You’ll walk away with solid idea of how to do “build MVI for Android” combined with power of Kotlin and MVVM [ViewModel and LiveData]

Akshay Chordiya

July 02, 2019
Tweet

More Decks by Akshay Chordiya

Other Decks in Technology

Transcript

  1. The aim Help with state management Productive workflow Ability to

    parallelise within the team Focus on business logic Promotes Testing
  2. There are lot of things to solve for Android with

    MVI architecture Lots of Rx Configuration change Make it play well with Activities and Fragments
  3. View [UI Controller] Repository Local Data Source Remote Data Source

    ViewModel LiveData LiveData LiveData Local DB Remote
  4. User interacts with UI Action / Initial action Action to

    changes State Changes + Previous State Local DB Remote State Reducer ViewModel View
  5. User interacts with UI Action / Initial action Action to

    changes State Changes + Previous State Local DB Remote State Action Reducer ViewModel View
  6. User interacts with UI Action / Initial action Action to

    changes State Changes + Previous State Local DB Remote State Action Reducer ViewModel View
  7. User interacts with UI Action / Initial action Action to

    changes State Changes + Previous State Local DB Remote State Action Reducer ViewModel View
  8. User interacts with UI Action / Initial action Action to

    changes State Changes + Previous State Local DB Remote State Action Reducer ViewModel View
  9. User interacts with UI Action / Initial action Action to

    changes State Changes + Previous State Local DB Remote State Action Reducer ViewModel View
  10. View [UI Controller] Repository Remote Data Source ViewModel LiveData LiveData

    LiveData Reducer Action State LiveData Local Data Source Local DB Remote
  11. View [UI Controller] Repository Remote Data Source ViewModel LiveData LiveData

    LiveData Reducer Action State Rx Local Data Source Local DB Remote
  12. View [UI Controller] Repository Remote Data Source ViewModel LiveData LiveData

    LiveData Reducer Action State Coroutines Local Data Source Local DB Remote
  13. View [UI Controller] Repository Remote Data Source ViewModel LiveData LiveData

    LiveData Reducer Action State Flow Local Data Source Local DB Remote
  14. View [UI Controller] Repository Local Data Source Remote Data Source

    ViewModel LiveData LiveData LiveData Reducer Action State LiveData Async Local DB Remote
  15. private val newsViewModel by lazy { getViewModel<NewsViewModel>() } override fun

    onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) … // Observe for state changes newsViewModel.observableState.observe(this) { state -> renderState(state) } // Dispatch initial action to load news newsViewModel.dispatch(Action.LoadNews) }
  16. private val newsViewModel by lazy { getViewModel<NewsViewModel>() } override fun

    onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) … // Observe for state changes newsViewModel.observableState.observe(this) { state -> renderState(state) } // Dispatch initial action to load news newsViewModel.dispatch(Action.LoadNews) }
  17. class NewsViewModel @Inject constructor( private val newsRepository: NewsRepository ) :

    BaseViewModel<Action, State>() { /* Initial state */ override val initialState = State.Loading init { bindActions() } private fun bindActions() { // Action -> 1..n Change val loadNews = actions.ofType<Action.LoadNews>() .switchMap { newsRepository.getTopHeadlines("technology") .subscribeOn(Schedulers.io()) .map<Change> { Change.News(it.articles) } .onErrorReturn { Change.Error }
  18. private fun bindActions() { // Action -> 1..n Change val

    loadNews = actions.ofType<Action.LoadNews>() .switchMap { newsRepository.getTopHeadlines("technology") .subscribeOn(Schedulers.io()) .map<Change> { Change.News(it.articles) } .onErrorReturn { Change.Error } .startWith(Change.Loading) } // Use Observable.merge to merge changes into a single stream: // val allChanges = Observable.merge(loadNews, ...) // Change -> Reducer disposables += loadNews .scan(initialState, reducer) .distinctUntilChanged() .subscribe(state::postValue, Timber::e) }
  19. private fun bindActions() { // Action -> 1..n Change val

    loadNews = actions.ofType<Action.LoadNews>() .switchMap { newsRepository.getTopHeadlines("technology") .subscribeOn(Schedulers.io()) .map<Change> { Change.News(it.articles) } .onErrorReturn { Change.Error } .startWith(Change.Loading) } // Use Observable.merge to merge changes into a single stream: // val allChanges = Observable.merge(loadNews, ...) // Change -> Reducer disposables += loadNews .scan(initialState, reducer) .distinctUntilChanged() .subscribe(state::postValue, Timber::e) }
  20. private fun bindActions() { // Action -> 1..n Change val

    loadNews = actions.ofType<Action.LoadNews>() .switchMap { newsRepository.getTopHeadlines("technology") .subscribeOn(Schedulers.io()) .map<Change> { Change.News(it.articles) } .onErrorReturn { Change.Error } .startWith(Change.Loading) } // Use Observable.merge to merge changes into a single stream: // val allChanges = Observable.merge(loadNews, ...) // Change -> Reducer disposables += loadNews .scan(initialState, reducer) .distinctUntilChanged() .subscribe(state::postValue, Timber::e) }
  21. private fun bindActions() { // Action -> 1..n Change val

    loadNews = actions.ofType<Action.LoadNews>() .switchMap { newsRepository.getTopHeadlines("technology") .subscribeOn(Schedulers.io()) .map<Change> { Change.News(it.articles) } .onErrorReturn { Change.Error } .startWith(Change.Loading) } // Use Observable.merge to merge changes into a single stream: // val allChanges = Observable.merge(loadNews, ...) // Change -> Reducer disposables += loadNews .scan(initialState, reducer) .distinctUntilChanged() .subscribe(state::postValue, Timber::e) }
  22. private fun bindActions() { // Action -> 1..n Change val

    loadNews = actions.ofType<Action.LoadNews>() .switchMap { newsRepository.getTopHeadlines("technology") .subscribeOn(Schedulers.io()) .map<Change> { Change.News(it.articles) } .onErrorReturn { Change.Error } .startWith(Change.Loading) } // Use Observable.merge to merge changes into a single stream: // val allChanges = Observable.merge(loadNews, ...) // Change -> Reducer disposables += loadNews .scan(initialState, reducer) .distinctUntilChanged() .subscribe(state::postValue, Timber::e) }
  23. private fun bindActions() { // Action -> 1..n Change val

    loadNews = actions.ofType<Action.LoadNews>() .switchMap { newsRepository.getTopHeadlines("technology") .subscribeOn(Schedulers.io()) .map<Change> { Change.News(it.articles) } .onErrorReturn { Change.Error } .startWith(Change.Loading) } // Use Observable.merge to merge changes into a single stream: // val allChanges = Observable.merge(loadNews, ...) // Change -> Reducer disposables += loadNews .scan(initialState, reducer) .distinctUntilChanged() .subscribe(state::postValue, Timber::e) }
  24. // Use Observable.merge to merge changes into a single stream:

    // val allChanges = Observable.merge(loadNews, ...) // Change -> Reducer disposables += loadNews .scan(initialState, reducer) .distinctUntilChanged() .subscribe(state::postValue, Timber::e) } /* Reducer => <Previous State, Change> -> New State */ private val reducer: Reducer<State, Change> = { , change -> when (change) { is Change.News -> State.News(change.news) is Change.Loading -> State.Loading is Change.Error -> State.Error } } }
  25. private val newsViewModel by lazy { getViewModel<NewsViewModel>() } override fun

    onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) … // Observe for state changes newsViewModel.observableState.observe(this) { state -> renderState(state) } // Dispatch initial action to load news newsViewModel.dispatch(Action.LoadNews) }
  26. private val newsViewModel by lazy { getViewModel<NewsViewModel>() } override fun

    onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) … // Observe for state changes newsViewModel.observableState.observe(this) { state -> renderState(state) } // Dispatch initial action to load news newsViewModel.dispatch(Action.LoadNews) }
  27. @Test fun `Given news successfully loaded, when action load news,

    then state contains news articles`() { // GIVEN val newsList = listOf(..) val newsResponse = NewsResponse("ok", 1, newsList) val successState = State.News(newsList) // MOCK whenever(newsRepository.getTopHeadlines("technology")) .thenReturn(Observable.just(newsResponse)) // WHEN viewModel.dispatch(Action.LoadNews) testSchedulerRule.triggerActions()
  28. val successState = State.News(newsList) // MOCK whenever(newsRepository.getTopHeadlines("technology")) .thenReturn(Observable.just(newsResponse)) // WHEN

    viewModel.dispatch(Action.LoadNews) testSchedulerRule.triggerActions() // THEN inOrder(observer) { verify(observer).onChanged(loadingState) verify(observer).onChanged(successState) } verifyNoMoreInteractions(observer) }
  29. - suites your use-case - applies to your team -

    the cons are not cons for you - makes you happy Pick the one which