Taming MVI

How to unravel the MVI jungle using our own production Jungle and get a simple and structured architectural solution.



image



Foreword



When I first came across an article about Model-View-Intent (MVI) for Android, I didn't even open it.

- Seriously!? Architecture on Android Intents?



. MVI , .



MVI, , - , - . , MVP MVVM, , , : " ?".



, , - ; , .



. ( ):



  1. ;
  2. UI , ;
  3. .


?



β€” (Jungle). β€” RxJava .





  • State β€” "" UI, View;
  • Action β€” "" UI, View (, Snackbar Toast);
  • Event β€” Intent Model-View-Intent;
  • MviView β€” , Actions State;
  • Middleware β€” UI;
  • Store β€” Model View, , Events, State Actions.


image

, , β€”



?



, β€” . , , . :



  1. PrgoressBar ;
  2. Button Toast ;
  3. , ;
  4. , - .


UI :



sealed class DemoEvent {
   object Load : DemoEvent()
}


sealed class DemoAction {
   data class ShowError(val error: String) : DemoAction()
}


data class DemoState(
   val loading: Boolean = false,
   val countries: List<Country> = emptyList()
)


class DemoFragment : Fragment, MviView<DemoState, DemoAction> {

   private lateinit var demoStore: DemoStore
   private var adapter: DemoAdapter? = null

   /*Initializations are skipped*/

   override fun onViewCreated(view: View, bundle: Bundle?) {
      super.onViewCreated(view, bundle)
      demoStore.run {
         attach(this@DemoFragment)
         dispatchEventSource(
            RxView.clicks(demo_load)
               .map { DemoEvent.Load }
         )
      }
   }

   override fun onDestroyView() {
      super.onDestroyView()
      demoStore.detach()
   }

   override fun render(state: DemoState) {
      val showReload = state.run {
         !loading && countries.isEmpty()
      }
      demo_load.visibility = if (showReload)
         View.GONE else
         View.VISIBLE
      demo_progress.visibility = if (state.loading)
         View.VISIBLE else
         View.GONE
      demo_recycler.visibility = if (state.countries.isEmpty())
         View.GONE else
         View.VISIBLE
      adapter?.apply {
         setItems(state.countries)
         notifyDataSetChanged()
      }
   }

   override fun processAction(action: DemoAction) {
      when (action) {
         is DemoAction.ShowError ->
            Toast.makeText(
               requireContext(),
               action.error,
               Toast.LENGTH_SHORT
            ).show()
      }
   }
}


() ? DemoEvent.Load DemoStore ( Reload ); DemoAction.ShowError ( ) Toast; DemoState ( ) UI . .



DemoStore. , Store, DemoEvent, DemoAction DemoState:



class DemoStore (
   foregroundScheduler: Scheduler,
   backgroundScheduler: Scheduler
) : Store<DemoEvent, DemoState, DemoAction>(
   foregroundScheduler = foregroundScheduler,
   backgroundScheduler = backgroundScheduler
)


, CountryMiddleware, :



class CountryMiddleware(
   private val getCountriesInteractor: GetCountriesInteractor
) : Middleware<CountryMiddleware.Input>() {
   override val inputType = Input::class.java

   override fun transform(upstream: Observable<Input>) =
      upstream.switchMap<CommandResult> {
         getCountriesInteractor.execute()
            .map<Output> { Output.Loaded(it) }
            .onErrorReturn {
               Output.Failed(it.message ?: "Can't load countries")
            }
            .startWith(Output.Loading)
      }

   object Input : Command

   sealed class Output : CommandResult {
      object Loading : Output()
      data class Loaded(val countries: List<Country>) : Output()
      data class Failed(val error: String) : Output()
   }
}


Command? , "-" . CommandResult? "-".



CountryMiddleware.Input , CountryMiddleware . Middleware CommandResult; sealed (CountryMiddleware.Output).



Observable, Output.Loading , Output.Loaded , Output.Failed .



DemoStore CountryMiddleware Reload :



class DemoStore (..., countryMiddleware: CountryMiddleware) ... {
   override val middlewares = listOf(countryMiddleware)

   override fun convertEvent(event: DemoEvent) = when (event) {
      is DemoEvent.Load -> CountryMiddleware.Input
   }
}


middlewares , Middlewares DemoStore . Store Commands. DemoEvent.Load CountryMiddleware.Input ( , ).



, CountryMiddleware. DemoState:



class DemoStore ... {

   ...

   override val initialState = DemoState()

   override fun reduceCommandResult(
      state: DemoState,
      result: CommandResult
   ) = when (result) {
      is CountryMiddleware.Output.Loading ->
         state.copy(loading = true)
      is CountryMiddleware.Output.Loaded ->
         state.copy(loading = false, countries = result.countries)
      is CountryMiddleware.Output.Failed ->
         state.copy(loading = false)
      else -> state
   }
}


State, initialState. reduceCommandResult , CommandResult State.



DemoAction.ShowError. , Command ( CommandResult) Action:



class DemoStore ... {

   ...

   override fun produceCommand(commandResult: CommandResult) =
      when (commandResult) {
         is CountryMiddleware.Output.Failed ->
            ProduceActionCommand.Error(commandResult.error)
         else -> null
      }

   override fun produceAction(command: Command) =
      when (command) {
         is ProduceActionCommand.Error ->
            DemoAction.ShowError(command.error)
         else -> null
      }

   sealed class ProduceActionCommand : Command {
      data class Error(val error: String) : ProduceActionCommand()
   }
}


, β€” CountryMiddleware. , , Command bootstrapCommands:



class DemoStore ... {

   ...

   override val bootstrapCommands = listOf(CountryMiddleware.Input)
}


!



?



, , - . . Store, Middlewares, MviView.



View - ? Events, Store, Middleware render MviView.



, - ? , Event Store .



, , , .



?



, , :



  • Commands sealed Store, : Actions State?
  • Commands, Middlewares, .


, Middleware β€” , UseCase (Interactor). , (, , - domain layer) . , , Middleware .





, . , SingleLiveEvent Actions.



wiki. . , !




All Articles