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.

E02cd1d0fc5c51ac491b484a258a63a6?s=128

Arnaud GIULIANI

April 21, 2020
Tweet

Transcript

  1. None
  2. Riding the State Flow AndroidMakers

  3. ! Software Eng @ BlaBlaCar Mobile First Kotlin Open Source

    Arnaud GIULIANI
  4. None
  5. Building mobile applications…

  6. View Model Presenter ViewModel …

  7. business logic & data layers (database, networking…) Model Presenter ViewModel

    … Request data to Model and prepare it for UI
  8. Why are we still having issues with MVP, MVVM?

  9. View, State, Action … … to help us solve them

  10. MVP

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

    actions Presenter: manage UI logic, data & interactions View Presenter 1 .. 1 - Contract MVP
  12. Problems with MVP ⚠

  13. Tight coupling logic View/Presenter Data inconsistency Not reactive approach?

  14. MVVM

  15. View: observe streams to display data & uses actions ViewModel:

    exposes actions & expose streams of data View ViewModel actions data MVVM
  16. From MVP to MVVM

  17. None
  18. Architecture Components Lifecycle ViewModel LiveData

  19. Just use streams, what else?

  20. Still got some problems ⚠

  21. View & business logic coupling Data streams inconsistency

  22. Why is it hard to write? UI binding with incomplete

    data? Bad separation of concerns? is MVVM less formal?
  23. None
  24. Let’s think states over interactions

  25. Unidirectional data flow ♻

  26. •- 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
  27. Single source of truth Immutable data Testability, Logging & Replayability

  28. Redux-ing UI bugs - Christina Lee @RunChristinaRun

  29. MVI - Mosby http:/ /hannesdorfmann.com/mosby/

  30. @dggonzalez

  31. @dggonzalez

  32. <Store> <Reducer> <VM Dispatcher>

  33. …well

  34. View ViewModel Trigger action Dispatch action Action Reducer State Store

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

    Run action & Got event No state Notify new event Notify event
  36. Can we avoid to write Reducer & Store?

  37. Just focus on writing Actions?

  38. ViewModel Event State View action State - UI State (Main

    Data) Event - Side events
  39. ViewModel offers Actions

  40. Action emits States

  41. Testing—action’s sequence of states & events

  42. Coroutines inside ✅

  43. Simplify Async Code

  44. 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
  45. 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
  46. Let’s go

  47. #1 - A simple “Action”

  48. #1 - A simple “Action”

  49. Day Weather Icon Weather Description Temperature Humidity Wind

  50. Day Weather Icon Weather Description Temperature Humidity Wind Empty WeatherDetail

  51. Immutable Data

  52. object class Empty() data class WeatherDetail(val day: String, val icon:

    String, val description: String, val wind: String, val temperature: String, val humidity: String)
  53. 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()
  54. 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() }
  55. 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() }
  56. ViewModel

  57. class WeatherDetailViewModel( ) : ViewModel() { }

  58. class WeatherDetailViewModel( ) : ViewModel() { }

  59. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : ViewModel() { }
  60. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : ViewModel() { fun getDetail() { } }
  61. Action as function

  62. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : ViewModel() { fun getDetail() { } }
  63. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() { } } UDF ViewModel
  64. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { } }
  65. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

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

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

    ) : AndroidDataFlow() { fun getDetail() = action { currentState : UIState -> } } Run on current state ♻
  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 dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } } async call ⚡
  70. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } }
  71. 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 ♻
  72. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } }
  73. 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(...)
  74. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

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

    ) : AndroidDataFlow(defaultState = Empty) { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } }
  76. Error Handling

  77. 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()
  78. 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()
  79. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

    ) : AndroidDataFlow() { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } }
  80. 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) } } } }
  81. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

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

    ) : AndroidDataFlow() { fun getDetail() = action( onAction = { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } ) }
  83. 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) } } ) }
  84. 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) } } ) }
  85. UI Binding

  86. 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() }
  87. 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() }
  88. 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() }
  89. 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() }
  90. 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() }
  91. 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() }
  92. class WeatherDetailActivity : AppCompatActivity() { private val detailViewModel: DetailViewModel by

    viewModel() override fun onCreate(savedInstanceState: Bundle?) { //... onEvents(detailViewModel) { event -> when (event) { } } detailViewModel.getDetail() }
  93. Testing

  94. @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()) } }
  95. @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()) } }
  96. @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()) } }
  97. @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()) } }
  98. @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()) } }
  99. @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()) } }
  100. One Coding Convention

  101. fun myAction() = action { }

  102. 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
  103. #2 - State Guard

  104. #2 - State Guard

  105. None
  106. 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)
  107. One View - Multiple States

  108. None
  109. 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() }
  110. 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() }
  111. 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() }
  112. 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() }
  113. 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() }
  114. 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> {
  115. 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> {
  116. 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> {
  117. 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> {
  118. ) : 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) } }
  119. ) : 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>
  120. ) : 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>
  121. 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) } }
  122. 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>
  123. 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
  124. @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)) } }
  125. @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)) } }
  126. NoSuggestion NewSuggestions

  127. #3 - Rooting States & Views

  128. Activity -> View rooting Shared ViewModel - ViewModelStoreOwner

  129. One stream -> Multiple Views

  130. None
  131. WeatherActivity WeatherHeader Fragment WeatherList Fragment ViewModel Weather State Create Create

    Weather State Weather State ⚠ no arg passed
  132. 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>
  133. One stream -> Multiple Views*

  134. None
  135. NewCardActivity NewCardViewModel StartFragment Start State Start State

  136. NewCardActivity NewCardViewModel EditCardFragment EditCard State EditCard State

  137. NewCardActivity NewCardViewModel VerifyCardFragment VerifyCard State VerifyCard State

  138. NewCardActivity NewCardViewModel CheckedCardFragment CheckedCard State CheckedCard State

  139. 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>
  140. #4 - Safe & Functional

  141. None
  142. Better Unsafe Values Handling With Arrow.io

  143. class WeatherDetailViewModel( private val id: DailyForecastId, private val repo: WeatherRepository

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

    ) : AndroidDataFlow() { fun getDetail() = action { val dailyForecast = repo.getDailyForecast(id) setState { dailyForecast.mapToWeatherState() } } }
  145. 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 } } }
  146. 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 } } }
  147. 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 } } }
  148. 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 } } }
  149. 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 } } }
  150. Ready to ride the State Flow?

  151. Make your own Experience

  152. Think UI as States

  153. Unidirectional Data Flow

  154. Architecture — Decisions which are both important and hard to

    change Martin Fowler
  155. One way of thinking But many ways to write it

  156. ~5 classes

  157. Uniflow https:/ /github.com/uniflow-kt/

  158. Arnaud Giuliani @arnogiu Thank you!

  159. Any question? 3 sli.do