Menjinakkan MVI

Bagaimana cara mengungkap hutan MVI menggunakan Jungle produksi kami sendiri dan mendapatkan solusi arsitektur yang sederhana dan terstruktur.



gambar



Kata pengantar



Ketika saya pertama kali menemukan artikel tentang Model-View-Intent (MVI) untuk Android, saya bahkan tidak membukanya.

- Serius !? Arsitektur di Android Maksud?



. 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.


gambar

, , —



?



, — . , , . :



  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