Olá pessoal! Neste artigo, quero falar sobre uma nova biblioteca que traz o padrão de design do MVI para o Android. Essa biblioteca é chamada MVIDroid, escrita 100% em Kotlin, leve e usa RxJava 2.x. Pessoalmente, sou o autor da biblioteca, seu código fonte está disponível no GitHub e você pode conectá-lo através do JitPack (link para o repositório no final do artigo). Este artigo consiste em duas partes: uma descrição geral da biblioteca e um exemplo de seu uso.
MVI
E assim, como prefácio, deixe-me lembrá-lo do que é MVI. Model - View - Intent ou, se em russo, Model - View - Intention. Esse é um padrão de design no qual o Modelo é um componente ativo que aceita Intents e gera State. A apresentação (vista), por sua vez, aceita modelos de representação (vista modelo) e produz as mesmas intenções. O estado é convertido em um Modelo de Visualização usando uma função de transformação (Mapeador de Modelos de Visualização). Esquematicamente, o padrão MVI pode ser representado da seguinte maneira:

No MVIDroid, a Representação não produz Intenções diretamente. Em vez disso, produz eventos da interface do usuário, que são convertidos em intenção usando uma função de transformação.

Principais componentes do MVIDroid
Modelo
Vamos começar com o modelo. Na biblioteca, o conceito de Modelo é um pouco expandido, aqui produz não apenas Estados, mas também Rótulos. As etiquetas são usadas para comunicar modelos entre si. Os rótulos de alguns modelos podem ser convertidos nas intenções de outros modelos usando funções de transformação. Esquematicamente, o Modelo pode ser representado da seguinte maneira:

No MVIDroid, o Modelo é representado pela interface MviStore (o nome da Loja é emprestado do Redux):
interface MviStore<State : Any, in Intent : Any, Label : Any> : (Intent) -> Unit, Disposable { @get:MainThread val state: State val states: Observable<State> val labels: Observable<Label> @MainThread override fun invoke(intent: Intent) @MainThread override fun dispose() @MainThread override fun isDisposed(): Boolean }
E para que tenhamos:
- A interface possui três parâmetros genéricos: Estado - tipo de estado, Intenção - tipo de intenção e Etiqueta - tipo de etiquetas
- Ele contém três campos: estado - o estado atual do modelo, estados - Estados observáveis e rótulos - Rótulos observáveis. Os dois últimos campos possibilitam a assinatura de alterações no Status e nos Tags, respectivamente.
- Intenção do Consumidor
- É descartável, o que torna possível destruir o modelo e interromper todos os processos que ocorrem nele
Observe que todos os métodos de modelo devem ser executados no encadeamento principal. O mesmo vale para qualquer outro componente. Obviamente, você pode executar tarefas em segundo plano usando as ferramentas padrão do RxJava.
Componente
Um componente no MVIDroid é um grupo de modelos unidos por um objetivo comum. Por exemplo, você pode selecionar todos os modelos para uma tela no componente. Em outras palavras, o Componente é a fachada dos Modelos incluídos nele e permite ocultar os detalhes da implementação (Modelos, funções de transformação e seus relacionamentos). Vejamos o diagrama de componentes:

Como você pode ver no diagrama, o componente tem uma função importante de transformar e redirecionar eventos.
Uma lista completa da função Component é a seguinte:
- Associa eventos e tags de representação recebidos a cada modelo usando as funções de transformação fornecidas
- Trazer etiquetas de modelo de saída para fora
- Destrói todos os modelos e quebra todos os vínculos quando um componente é destruído
O componente também possui sua própria interface:
interface MviComponent<in UiEvent : Any, out States : Any> : (UiEvent) -> Unit, Disposable { @get:MainThread val states: States @MainThread override fun invoke(event: UiEvent) @MainThread override fun dispose() @MainThread override fun isDisposed(): Boolean }
Considere a interface do componente em mais detalhes:
- Contém dois parâmetros genéricos: UiEvent - tipo de exibição de eventos e estados - tipo de status de modelos
- Contém o campo de estados que dá acesso ao grupo Estados do Modelo (por exemplo, como uma interface ou classe de dados)
- Eventos de exibição do consumidor
- É descartável, o que torna possível destruir o componente e todos os seus modelos
Ver
Como você pode imaginar, é necessária uma visão para exibir dados. Os dados de cada visualização são agrupados em um modelo de visualização e geralmente são representados como uma classe de dados (Kotlin). Considere a interface View:
interface MviView<ViewModel : Any, UiEvent : Any> { val uiEvents: Observable<UiEvent> @MainThread fun subscribe(models: Observable<ViewModel>): Disposable }
Tudo aqui é um pouco mais fácil. Dois parâmetros genéricos: ViewModel - tipo de modelo de exibição e UiEvent - tipo de eventos de exibição. Um campo uiEvents é o Observable View Event, que permite que os clientes se inscrevam nesses mesmos eventos. E um método subscribe () que permite que você assine o View Models.
Exemplo de uso
Agora é a hora de tentar algo na prática. Proponho fazer algo muito simples. Algo que não requer muito esforço para entender e, ao mesmo tempo, dá uma idéia de como usar tudo isso e em que direção seguir em frente. Que seja um gerador de UUID: com o toque de um botão, geraremos um UUID e o exibiremos na tela.
Submissão
Primeiro, descrevemos o modelo de exibição:
data class ViewModel(val text: String)
E exibir eventos:
sealed class UiEvent { object OnGenerateClick: UiEvent() }
Agora, implementamos o próprio View, para isso precisamos de herança da classe abstrata MviAbstractView:
class View(activity: Activity) : MviAbstractView<ViewModel, UiEvent>() { private val textView = activity.findViewById<TextView>(R.id.text) init { activity.findViewById<Button>(R.id.button).setOnClickListener { dispatch(UiEvent.OnGenerateClick) } } override fun subscribe(models: Observable<ViewModel>): Disposable = models.map(ViewModel::text).distinctUntilChanged().subscribe { textView.text = it } }
Tudo é extremamente simples: assinamos as alterações de UUID e atualizamos o TextView quando recebemos um novo UUID e, quando o botão é clicado, enviamos o evento OnGenerateClick.
Modelo
O modelo será composto de duas partes: interface e implementação.
Interface:
interface UuidStore : MviStore<State, Intent, Nothing> { data class State(val uuid: String? = null) sealed class Intent { object Generate : Intent() } }
Tudo é simples aqui: nossa interface estende a interface MviStore, indicando os tipos de estado (estado) e intenção (intenção). Tipo de Tags - Nada, porque nosso Modelo não as produz. A interface também contém as classes State e Intent.
Para implementar o Modelo, você precisa entender como ele funciona. Na entrada do Model, são recebidas as Intents, que são convertidas em Actions usando a função IntentToAction especial. As ações são inseridas no Executor, que as executa e produz o Resultado e o Rótulo. Os resultados vão para o redutor, que converte o estado atual em um novo.
Todos os quatro modelos de composição:
- IntentToAction - uma função que converte Intent em ação
- MviExecutor - Executa Ações e produz Resultados e Tags
- MviReducer - converte pares (Estado, Resultado) em novos Estados
- O MviBootstrapper é um componente especial que permite inicializar o modelo. Dá todas as mesmas ações que também vão para o Executor. Você pode executar uma ação única ou assinar uma fonte de dados e executar ações em determinados eventos. O Bootstrapper inicia automaticamente quando você cria um modelo.
Para criar o próprio modelo, você deve usar uma fábrica especial de modelos. É representado pela interface MviStoreFactory e sua implementação do MviDefaultStoreFactory. A fábrica aceita os componentes do modelo e emite um modelo pronto para uso.
A fábrica do nosso modelo terá a seguinte aparência:
class UuidStoreFactory(private val factory: MviStoreFactory) { fun create(factory: MviStoreFactory): UuidStore = object : UuidStore, MviStore<State, Intent, Nothing> by factory.create( initialState = State(), bootstrapper = Bootstrapper, intentToAction = { when (it) { Intent.Generate -> Action.Generate } }, executor = Executor(), reducer = Reducer ) { } private sealed class Action { object Generate : Action() } private sealed class Result { class Uuid(val uuid: String) : Result() } private object Bootstrapper : MviBootstrapper<Action> { override fun bootstrap(dispatch: (Action) -> Unit): Disposable? { dispatch(Action.Generate) return null } } private class Executor : MviExecutor<State, Action, Result, Nothing>() { override fun invoke(action: Action): Disposable? { dispatch(Result.Uuid(UUID.randomUUID().toString())) return null } } private object Reducer : MviReducer<State, Result> { override fun State.reduce(result: Result): State = when (result) { is Result.Uuid -> copy(uuid = result.uuid) } } }
Este exemplo apresenta todos os quatro componentes do modelo. Primeiro, a fábrica cria um método, depois Ações e Resultados, seguido pelo Empreiteiro e, no final, o Redutor.
Componente
Os estados do componente (grupo de estados) são descritos pela classe de dados:
data class States(val uuidStates: Observable<UuidStore.State>)
Ao adicionar novos modelos a um componente, seu status também deve ser adicionado ao grupo.
E, de fato, a própria implementação:
class Component(uuidStore: UuidStore) : MviAbstractComponent<UiEvent, States>( stores = listOf( MviStoreBundle( store = uuidStore, uiEventTransformer = UuidStoreUiEventTransformer ) ) ) { override val states: States = States(uuidStore.states) private object UuidStoreUiEventTransformer : (UiEvent) -> UuidStore.Intent? { override fun invoke(event: UiEvent): UuidStore.Intent? = when (event) { UiEvent.OnGenerateClick -> UuidStore.Intent.Generate } } }
Herdamos a classe abstrata MviAbstractComponent, especificamos os tipos de Estados e Visualizar Eventos, passamos nosso Modelo para a superclasse e implementamos o campo de estados. Além disso, criamos uma função de transformação que transformará Exibir eventos em intenções do nosso modelo.
Modelos de exibição de mapeamento
Temos condições e modelos de apresentação, é hora de converter um em outro. Para fazer isso, implementamos a interface MviViewModelMapper:
object ViewModelMapper : MviViewModelMapper<States, ViewModel> { override fun map(states: States): Observable<ViewModel> = states.uuidStates.map { ViewModel(text = it.uuid ?: "None") } }
Encadernação
A presença do componente e da apresentação por si só não é suficiente. Para que tudo comece a funcionar, eles devem estar conectados. É hora de criar uma atividade:
class UuidActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_uuid) bind( Component(UuidStoreFactory(MviDefaultStoreFactory).create()), View(this) using ViewModelMapper ) } }
Usamos o método bind (), que utiliza um Component e uma matriz de Views com os mapeadores de seus modelos. Esse método é um método de extensão no LifecycleOwner (que é Activity e Fragment) e usa o DefaultLifecycleObserver do pacote Arch, que requer compatibilidade com a origem do Java 8. Se, por algum motivo, você não puder usar o Java 8, o segundo método bind () é adequado para você, que não é um método de extensão e retorna MviLifecyleObserver. Nesse caso, você precisará chamar os métodos de ciclo de vida.
Referências
O código fonte da biblioteca, bem como instruções detalhadas para conectar e usar, podem ser encontradas no GitHub .