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

Handling state: Unidirectional data flow

Handling state: Unidirectional data flow

Presentation for a talk about implementing unidirectional data flow and MVI on Android

Avatar for Andriy Matkovsky

Andriy Matkovsky

November 11, 2017
Tweet

Other Decks in Programming

Transcript

  1. interface FavoriteListView { fun showFavorites(favs: List<Favorite>) fun showError(message: String) fun

    setIsLoading(isLoading: Boolean) } class FavoriteListPresenter(private val favoriteListUseCase: UseCase) : Presenter<FavoriteListView> { override fun onAttach(view: FavoriteListView) { favoriteListUseCase.getFavorites() .doOnSubscribe { view.setIsLoading(true) } .doOnTerminate { view.setIsLoading(false) } .subscribe( { view.setIsLoading(false) view.showFavorites(it) }, { view.showError(it.message) } ) } } Typical MVP
  2. What is a model? A model is a structure that

    describes the data to be displayed or interacted with in the UI
  3. class FavoriteListModel( val favorites: List<Favorite>, val throwable: Throwable?, val isLoading:

    Boolean ) interface FavoriteListView { fun show(model: FavoriteListModel) } Better model
  4. Benefits • Immutable • Can be persisted across configuration changes

    • Easier debugging • View state can be reasoned entirely from one class
  5. Single source of model useCase.getFavorites() .map { FavoriteListModel( favorites =

    it, throwable = null, isLoading = false ) } .onErrorReturn { t -> FavoriteListModel(emptyList(), t, false) } .defaultIfEmpty( FavoriteListModel(emptyList(), null, false) ) .startWith( FavoriteListModel(emptyList(), null, true) ) .subscribe { view.show(it) }
  6. Model-View-Intent view(model(intent())) • Intent – returns actions to change the

    model • Model – takes an input from intent and returns a new state • View – takes an input from model and renders it
  7. sealed class FavoriteListState { object Loading : FavoriteListState() object Empty

    : FavoriteListState() data class Data(val data: List<Favorite>) : FavoriteListState() data class Error(@StringRes val message: Int) : FavoriteListState() } View State
  8. Model class FavoriteListModel( private val actions: Observable<Action> ) { fun

    getHomeState(): Observable<FavoriteListState> } Return the proper state depending on the actions
  9. Intentions sealed class Action { data class RemoveFromFavorites(val id: String)

    : Action() data class UndoRemoveFromFavorites(val id: String) : Action() } Action that describes our intention to change the model
  10. View class FavoriteListActivity : MviActivity<FavoriteListState, FavoriteListView>(), FavoriteListView { override fun

    render(state: FavoriteListState) { when (state) { FavoriteListState.Loading -> showLoading() FavoriteListState.Empty -> showEmpty() is FavoriteListState.Data -> showContent(state.data) is FavoriteListState.Error -> showError(state.message) } } } One responsibility – to render the given state
  11. Reducers Reducer is a function that takes the current state

    and some new arguments and computes a new state
  12. Reducers typealias FavoriteListStateReducer = (FavoriteListState) -> FavoriteListState fun favoriteListReducer(favorites: Result<List<Favorite>>):

    HomeStateReducer = { when (favorites) { is Result.Data -> FavoriteListState.Data(favorites.data) is Result.Error -> FavoriteListState.Error() is Result.Loading -> FavoriteListState.Loading is Result.Nothing -> FavoriteListState.Empty } }
  13. Reducers fun removeFromFavoritesReducer( id: Long, favorites: Result<List<Favorite>>): FavoriteListStateReducer = {

    when (it) { is HomeState.Data -> RemoveFromFavorite(getState(favorites), id) else -> throw IllegalStateException( "invalid state transition $it -> RemoveFromFavorite" ) } }
  14. Reducers RemoveFromFavorite state will be rendered as a SnackBar. What

    do we do with its duration? What will happen if we rotate the screen?
  15. fun timerReducer(id: Long) = Single.timer(2750, TimeUnit.MILLISECONDS) .flatMap { favoriteRepository.addToFavorites(id) }

    .map { timerReducer() } .toObservable() .takeUntil( actions.filter { it is Action.RemoveFromFavorites || it is Action.DismissUndo } ) val removeFromFavoritesReducer = actions.ofType<Action.RemoveFromFavorites>() .flatMap { favoriteRepository.removeFromFavorites(it.id) .map { removeFromFavoritesReducer( it.id, it.favorites.flatMap(converter) ) } .toObservable() .concatWith(timerReducer(it.id)) } Reducers
  16. override fun getState(): Observable<FavoriteListState> { … val initialState: FavoriteListState =

    FavoriteListState.Loading return Observable.merge(listOf( favoriteListStateReducer, removeFromFavoritesReducer, dismissReducer) ) .scan(initialState, { state, reducer -> reducer(state) } ) } Connecting reducers together
  17. class BaseViewModel<Action, State>(private val model: Model<State>) : ViewModel() { val

    actions: PublishRelay<Action> = PublishRelay.create() val liveData: LiveData<State> by lazy { LiveDataReactiveStreams.fromPublisher( model.getState().toFlowable(BackpressureStrategy.BUFFER) ) } } interface Model<State> { fun getState(): Observable<State> }
  18. abstract class MviActivity<S, V : BaseView<S>, A> : AppCompatActivity() {

    @Inject protected lateinit var view: V @Inject protected lateinit var viewModelFactory: ViewModelFactory<A, S> private val viewModel: BaseViewModel<A, S> by lazy { ViewModelProviders.of(this, viewModelFactory) .get(BaseViewModel::class.java) as BaseViewModel<A, S> } override fun onCreate(savedInstanceState: Bundle?) { AndroidInjection.inject(this) super.onCreate(savedInstanceState) viewModel.liveData.observe( this, Observer { it?.let { view.render(it) } } ) } }
  19. Links • hannesdorfmann.com/android/mosby3-mvi-1 • https://github.com/fnberta/PopularMovies • Christina Lee: Borrowing the

    best from the Web https://youtube.com/watch?v=GOVMkQp3LZ4 • https://staltz.com/unidirectional-user-interface-architectures.html