Cómo desentrañar la jungla MVI usando nuestra propia producción Jungle y obtener una solución arquitectónica simple y estructurada.
Prefacio
Cuando me encontré por primera vez con un artículo sobre Model-View-Intent (MVI) para Android, ni siquiera lo abrí.
- ¿¡Seriamente!? Arquitectura en Intentos de Android?
. MVI , .
MVI, , - , - . , MVP MVVM, , , : " ?".
, , - ; , .
. ( ):
- ;
- UI , ;
- .
?
- State — "" UI, View;
- Action — "" UI, View (, Snackbar Toast);
- Event — Intent Model-View-Intent;
- MviView — , Actions State;
- Middleware — UI;
- Store — Model View, , Events, State Actions.
, , —
?
, — . , , . :
- PrgoressBar ;
- Button Toast ;
- , ;
- , - .
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. . , !