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

Riding the State Flow

Riding the State Flow

View, State, Action ... Unidirectional Data Flow is not a concept unique to React. Used for many years in Web development, it tends to propagate to mobile platforms. Why? Because it helps develop our UI and consider the changes as states and events.

This topic is not new on Android. But Kotlin and coroutines can now greatly improve our experience.

Let's see how we can structure our Android MVVM development with such an approach. Let's take some concrete samples and see how we can easily write it with just a Kotlin function, and even embrace functional programming.

Arnaud GIULIANI

April 21, 2020
Tweet

More Decks by Arnaud GIULIANI

Other Decks in Technology

Transcript

  1. MVP

  2. View: display data, notified by the presenter & notify for

    actions Presenter: manage UI logic, data & interactions View Presenter 1 .. 1 - Contract MVP
  3. View: observe streams to display data & uses actions ViewModel:

    exposes actions & expose streams of data View ViewModel actions data MVVM
  4. Why is it hard to write? UI binding with incomplete

    data? Bad separation of concerns? is MVVM less formal?
  5. •- the state change is passed to the view •-

    actions can update the state •- actions are triggered by the view •- state is passed to the view * aka Redux
  6. View ViewModel Trigger action Dispatch action Action Reducer State Store

    Run action & get new state Apply & Store new state Notify new state Notify state
  7. View ViewModel Trigger action Dispatch action Action Reducer State Store

    Run action & Got event No state Notify new event Notify event
  8. View ViewModel Trigger action Dispatch action Action Reducer State Store

    Run action on current state (Actor & FlowAction) Apply & Store new state (LiveData) Notify new state Notify state
  9. View AndroidDataFlow Trigger action Dispatch action Action Reducer State Store

    Run action on current state (Actor & FlowAction) Apply & Store new state (LiveData) Notify new state Notify state Provided by Uniflow
  10. object class Empty() data class WeatherDetail(val day: String, val icon:

    String, val description: String, val wind: String, val temperature: String, val humidity: String)
  11. object class Empty() : UIState() data class WeatherDetail(val day: String,

    val icon: String, val description: String, val wind: String, val temperature: String, val humidity: String) : UIState()
  12. sealed class GetWeatherState : UIState(){ object class Empty() : GetWeatherState()

    data class WeatherDetail(val day: String, val icon: String, val description: String, val wind: String, val temperature: String, val humidity: String) : GetWeatherState() }
  13. sealed class GetWeatherState : UIState(){ object class Empty() : GetWeatherState()

    data class WeatherDetail(val day: String, val icon: String, val description: String, val wind: String, val temperature: String, val humidity: String) : GetWeatherState() }
  14. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { } } Emit states & events ♻
  15. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { } } Run coroutines on IO ⚡
  16. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { currentState : UIState -> } } Run on current state ♻
  17. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } }
  18. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } } async call ⚡
  19. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } }
  20. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } } emit new state ♻
  21. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } }
  22. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } } fun DailyForecast.mapToWeatherState() = WeatherDetail(...)
  23. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } }
  24. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow(defaultState = Empty) { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } }
  25. object class Empty() : UIState() data class Failed(val message: String,

    val error: Exception) : UIState() data class WeatherDetail(val day: String, val icon: String, val description: String, val wind: String, val temperature: String, val humidity: String) : UIState()
  26. object class Empty() : UIState() data class Failed(val message: String,

    val error: Exception) : UIState() data class WeatherDetail(val day: String, val icon: String, val description: String, val wind: String, val temperature: String, val humidity: String) : UIState()
  27. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } }
  28. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { try { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } catch (error: Exception) { setState { Failed(“got error”, error) } } } }
  29. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } }
  30. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action( onAction = { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } ) }
  31. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action( onAction = { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } }, onError = { error , state -> setState { Failed("got error", error) } } ) }
  32. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action( onAction = { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } }, onError = { error , state -> setState { Failed("got error", error) } } ) }
  33. class WeatherDetailActivity : AppCompatActivity() { private val detailViewModel: DetailViewModel by

    viewModel() override fun onCreate(savedInstanceState: Bundle?) { //... onStates(detailViewModel) { state -> when (state) { is Empty -> {} // do nothing is WeatherDetail -> showDetail(state) is Failed -> showError(state.error) } } detailViewModel.getDetail() }
  34. class WeatherDetailActivity : AppCompatActivity() { private val detailViewModel: DetailViewModel by

    viewModel() override fun onCreate(savedInstanceState: Bundle?) { //... onStates(detailViewModel) { state -> when (state) { is Empty -> {} // do nothing is WeatherDetail -> showDetail(state) is Failed -> showError(state.error) } } detailViewModel.getDetail() }
  35. class WeatherDetailActivity : AppCompatActivity() { private val detailViewModel: DetailViewModel by

    viewModel() override fun onCreate(savedInstanceState: Bundle?) { //... onStates(detailViewModel) { state -> when (state) { is Empty -> {} // do nothing is WeatherDetail -> showDetail(state) is Failed -> showError(state.error) } } detailViewModel.getDetail() }
  36. class WeatherDetailActivity : AppCompatActivity() { private val detailViewModel: DetailViewModel by

    viewModel() override fun onCreate(savedInstanceState: Bundle?) { //... onStates(detailViewModel) { state -> when (state) { is Empty -> {} // do nothing is WeatherDetail -> showDetail(state) is Failed -> showError(state.error) } } detailViewModel.getDetail() }
  37. class WeatherDetailActivity : AppCompatActivity() { private val detailViewModel: DetailViewModel by

    viewModel() override fun onCreate(savedInstanceState: Bundle?) { //... onStates(detailViewModel) { state -> when (state) { is Empty -> {} // do nothing is WeatherDetail -> showDetail(state) is Failed -> showError(state.error) } } detailViewModel.getDetail() }
  38. class WeatherDetailActivity : AppCompatActivity() { private val detailViewModel: DetailViewModel by

    viewModel() override fun onCreate(savedInstanceState: Bundle?) { //... onStates(detailViewModel) { state -> when (state) { is Empty -> {} // do nothing is WeatherDetail -> showDetail(state) is Failed -> showError(state.error) } } detailViewModel.getDetail() }
  39. class WeatherDetailActivity : AppCompatActivity() { private val detailViewModel: DetailViewModel by

    viewModel() override fun onCreate(savedInstanceState: Bundle?) { //... onEvents(detailViewModel) { event -> when (event) { } } detailViewModel.getDetail() }
  40. @Test fun `get a daily forecast`() { // prepare detailViewModel

    = DetailViewModel(id, repo) view = detailViewModel.mockObservers() // mocks val weather = MockedData.dailyForecasts.first() coEvery { repo.getDailyForecast(id) } returns weather // call detailViewModel.getDetail() // sequence of states & events verifySequence { view.states.onChanged(UIState.Empty) view.states.onChanged(weather.mapToDetailState()) } }
  41. @Test fun `get a daily forecast`() { // prepare detailViewModel

    = DetailViewModel(id, repo) view = detailViewModel.mockObservers() // mocks val weather = MockedData.dailyForecasts.first() coEvery { repo.getDailyForecast(id) } returns weather // call detailViewModel.getDetail() // sequence of states & events verifySequence { view.states.onChanged(UIState.Empty) view.states.onChanged(weather.mapToDetailState()) } }
  42. @Test fun `get a daily forecast`() { // prepare detailViewModel

    = DetailViewModel(id, repo) view = detailViewModel.mockObservers() // mocks val weather = MockedData.dailyForecasts.first() coEvery { repo.getDailyForecast(id) } returns weather // call detailViewModel.getDetail() // sequence of states & events verifySequence { view.states.onChanged(UIState.Empty) view.states.onChanged(weather.mapToDetailState()) } }
  43. @Test fun `get a daily forecast`() { // prepare detailViewModel

    = DetailViewModel(id, repo) view = detailViewModel.mockObservers() // mocks val weather = MockedData.dailyForecasts.first() coEvery { repo.getDailyForecast(id) } returns weather // call detailViewModel.getDetail() // sequence of states & events verifySequence { view.states.onChanged(UIState.Empty) view.states.onChanged(weather.mapToDetailState()) } }
  44. @Test fun `get a daily forecast`() { // prepare detailViewModel

    = DetailViewModel(id, repo) view = detailViewModel.mockObservers() // mocks val weather = MockedData.dailyForecasts.first() coEvery { repo.getDailyForecast(id) } returns weather // call detailViewModel.getDetail() // sequence of states & events verifySequence { view.states.onChanged(UIState.Empty) view.states.onChanged(weather.mapToDetailState()) } }
  45. @Test fun `get a daily forecast`() { // prepare detailViewModel

    = DetailViewModel(id, repo) view = detailViewModel.mockObservers() // mocks val weather = MockedData.dailyForecasts.first() coEvery { repo.getDailyForecast(id) } returns weather // call detailViewModel.getDetail() // sequence of states & events verifySequence { view.states.onChanged(Empty) view.states.onChanged(weather.mapToDetailState()) } }
  46. View AndroidDataFlow Trigger action Dispatch action Action Reducer State Store

    Run action on current state (Actor & FlowAction) Apply & Store new state (LiveData) Notify new state Notify state Provided by Uniflow
  47. NoSuggestion NewSuggestions The user has no company or n/a The

    user can select a company (can’t select anything) (select an existing or provide a new one)
  48. sealed class SelectCompanyState : UIState() { object Empty : SelectCompanyState()

    data class CompanyList(val list: List<CompanyItem>) : SelectCompanyState() data class CompanySelected(val selectedCompany: CompanyItem) : SelectCompanyState() object NoCompanySelected : SelectCompanyState() }
  49. sealed class SelectCompanyState : UIState() { object Empty : SelectCompanyState()

    data class CompanyList(val list: List<CompanyItem>) : SelectCompanyState() data class CompanySelected(val selectedCompany: CompanyItem) : SelectCompanyState() object NoCompanySelected : SelectCompanyState() }
  50. sealed class SelectCompanyState : UIState() { object Empty : SelectCompanyState()

    data class CompanyList(val list: List<CompanyItem>) : SelectCompanyState() data class CompanySelected(val selectedCompany: CompanyItem) : SelectCompanyState() object NoCompanySelected : SelectCompanyState() }
  51. sealed class SelectCompanyState : UIState() { object Empty : SelectCompanyState()

    data class CompanyList(val list: List<CompanyItem>) : SelectCompanyState() data class CompanySelected(val selectedCompany: CompanyItem) : SelectCompanyState() object NoCompanySelected : SelectCompanyState() }
  52. sealed class SelectCompanyState : UIState() { object Empty : SelectCompanyState()

    data class CompanyList(val list: List<CompanyItem>) : SelectCompanyState() data class CompanySelected(val selectedCompany: CompanyItem) : SelectCompanyState() object NoCompanySelected : SelectCompanyState() }
  53. class SelectCompanyViewModel( private val repo: CompanySelectionRepository ) : AndroidDataFlow(defaultState =

    Empty) { // Search for campanies fun searchForName(name: String) = action { // look for new suggestions ... val suggestions = repo.searchForName(name) setState { CompanyList(suggestions.mapToItems()) } } // Select No Company fun selectNoCompany() = singleActionOn<Empty> { repo.setNoCompany() setState { NoCompanySelected } } // Select a Company fun selectCompany(item: CompanyItem) = singleActionOn<CompanyList> {
  54. class SelectCompanyViewModel( private val repo: CompanySelectionRepository ) : AndroidDataFlow(defaultState =

    Empty) { // Search for campanies fun searchForName(name: String) = action { // look for new suggestions ... val suggestions = repo.searchForName(name) setState { CompanyList(suggestions.mapToItems()) } } // Select No Company fun selectNoCompany() = singleActionOn<Empty> { repo.setNoCompany() setState { NoCompanySelected } } // Select a Company fun selectCompany(item: CompanyItem) = singleActionOn<CompanyList> {
  55. class SelectCompanyViewModel( private val repo: CompanySelectionRepository ) : AndroidDataFlow(defaultState =

    Empty) { // Search for campanies fun searchForName(name: String) = action { // look for new suggestions ... val suggestions = repo.searchForName(name) setState { CompanyList(suggestions.mapToItems()) } } // Select No Company fun selectNoCompany() = singleActionOn<Empty> { repo.setNoCompany() setState { NoCompanySelected } } // Select a Company fun selectCompany(item: CompanyItem) = singleActionOn<CompanyList> {
  56. class SelectCompanyViewModel( private val repo: CompanySelectionRepository ) : AndroidDataFlow(defaultState =

    Empty) { // Search for campanies fun searchForName(name: String) = action { // look for new suggestions ... val suggestions = repo.searchForName(name) setState { CompanyList(suggestions.mapToItems()) } } // Select No Company fun selectNoCompany() = singleActionOn<Empty> { repo.setNoCompany() setState { NoCompanySelected } } // Select a Company fun selectCompany(item: CompanyItem) = singleActionOn<CompanyList> {
  57. ) : AndroidDataFlow(default state = Empty) { // Search for

    campanies fun searchForName(name: String) = action { // look for new suggestions ... val suggestions = repo.searchForName(name) setState { CompanyList(suggestions.mapToItems()) } } // Select No Company fun selectNoCompany() = actionOn<Empty> { repo.setNoCompany() setState { NoCompanySelected } } // Select a Company fun selectCompany(item: CompanyItem) = actionOn<CompanyList> { repo.setCompany()(item) setState { CompanySelected(item) } }
  58. ) : AndroidDataFlow(default state = Empty) { // Search for

    campanies fun searchForName(name: String) = action { // look for new suggestions ... val suggestions = repo.searchForName(name) setState { CompanyList(suggestions.mapToItems()) } } // Select No Company fun selectNoCompany() = actionOn<Empty> { repo.setNoCompany() setState { NoCompanySelected } } // Select a Company fun selectCompany(item: CompanyItem) = actionOn<CompanyList> { repo.setCompany()(item) setState { CompanySelected(item) } } Guard on state <Empty>
  59. ) : AndroidDataFlow(default state = Empty) { // Search for

    campanies fun searchForName(name: String) = action { // look for new suggestions ... val suggestions = repo.searchForName(name) setState { CompanyList(suggestions.mapToItems()) } } // Select No Company fun selectNoCompany() = actionOn<Empty> { currentState: Empty -> repo.setNoCompany() setState { NoCompanySelected } } // Select a Company fun selectCompany(item: CompanyItem) = actionOn<CompanyList> { repo.setCompany()(item) setState { CompanySelected(item) } } current state is <Empty>
  60. val suggestions = repo.searchForName(name) setState { CompanyList(suggestions.mapToItems()) } } //

    Select No Company fun selectNoCompany() = actionOn<Empty> { repo.setNoCompany() setState { NoCompanySelected } } // Select a Company fun selectCompany(item: CompanyItem) = actionOn<CompanyList> { repo.setCompany(item) setState { CompanySelected(item) } }
  61. val suggestions = repo.searchForName(name) setState { CompanyList(suggestions.mapToItems()) } } //

    Select No Company fun selectNoCompany() = actionOn<Empty> { repo.setNoCompany() setState { NoCompanySelected } } // Select a Company fun selectCompany(item: CompanyItem) = actionOn<CompanyList> { repo.setCompany(item) setState { CompanySelected(item) } } Guard on state <CompanyList>
  62. View ViewModel Trigger action Dispatch action Action Reducer State Store

    Run action on current state State is Wrong. Notify with BadOrWrongState event Notify BoW Notify BoW
  63. @Test fun `select a company`() { // mock ... viewModel

    = SelectCompanyViewModel(repo) // call viewModel.selectCompany(mockItem) // sequence of states & events verifySequence { view.states.onChanged(Empty) view.events.onChanged(BadOrWrongState(state = Empty)) } }
  64. @Test fun `select a company`() { // mock ... viewModel

    = SelectCompanyViewModel(repo) // call viewModel.selectCompany(mockItem) // sequence of states & events verifySequence { view.states.onChanged(Empty) view.events.onChanged(BadOrWrongState(state = Empty)) } }
  65. onStates(viewModel) { state -> when (state) { is Loading ->

    { // show loading animation } is WeatherState -> { // create Fragments } is Failed -> { // display error } } } viewModel.getWeather() <Activity> onStates(viewModel) { state -> when (state) { is WeatherState -> { // display data } } } <Fragment>
  66. onStates(viewModel) { state -> when (state) { is StartState ->

    openFragment<StartFragment>() is EditCardState -> openFragment<EditCardFragment>() is VerifyCardState -> openFragment<VerifyCardFragment>() is CheckedCardState -> openFragment<CheckedCardFragment>() } } viewModel.startAddCardFlow() <Activity> onStates(viewModel) { state -> when (state) { is <ViewState> -> { // display data } } } <Fragment>
  67. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } }
  68. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } }
  69. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val safeState = Either.catch { repo.getDailyForecast(id) } .map { it.mapToWeatherState() } .getOrHandle { error -> Failed(“got error", error) } setState { safeState } } }
  70. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val safeState = Either.catch { repo.getDailyForecast(id) } .map { it.mapToWeatherState() } .getOrHandle { error -> Failed(“got error", error) } setState { safeState } } }
  71. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val safeState = Either.catch { repo.getDailyForecast(id) } .map { it.mapToWeatherState() } .getOrHandle { error -> Failed("got error", error) } setState { safeState } } }
  72. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val safeState = Either.catch { repo.getDailyForecast(id) } .map { it.mapToWeatherState() } .getOrHandle { error -> Failed(“got error", error) } setState { safeState } } }
  73. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val safeState = Either.catch { repo.getDailyForecast(id) } .map { it.mapToWeatherState() } .getOrHandle { error -> Failed(“got error", error) } setState { safeState } } }