大家好! 在本文中,我想讨论一个将MVI设计模式引入Android的新库。 该库称为MVIDroid,使用Kotlin 100%编写,轻巧,并使用RxJava2.x。 我个人是该库的作者,其源代码可在GitHub上找到,您可以通过JitPack将其连接(在本文结尾处链接至存储库)。 本文由两部分组成:对该库的一般说明及其使用的示例。
MVI
因此,作为序言,让我提醒您什么是MVI。 模型-视图-意图,或者,如果使用俄语,则模型-视图-意图。 这种设计模式是模型是一个接受Intents并生成State的活动组件。 表示(视图)又接受表示模型(视图模型)并产生相同的意图。 使用转换功能(视图模型映射器)将状态转换为视图模型。 示意图中,MVI模式可以表示如下:

在MVIDroid中,制图表达不会直接产生Intent。 相反,它将生成UI事件,然后使用转换函数将其转换为Intent。

MVIDroid的主要组件
型号
让我们从模型开始。 在图书馆中,模型的概念略有扩展,在这里不仅产生状态,而且产生标签。 标签用于相互传递模型。 可以使用转换功能将某些模型的标签转换为其他模型的意图。 示意地,模型可以表示如下:

在MVIDroid中,模型由MviStore接口表示(商店名称是从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 }
这样我们就可以:
- 该接口具有三个通用参数:状态-状态类型,意图-意图类型和标签-标签类型
- 它包含三个字段:状态-模型的当前状态,状态-可观察的状态和标签-可观察的标签。 最后两个字段可以分别订阅对状态和标签的更改。
- 消费者意向
- 它是一次性的,因此可以销毁模型并停止其中发生的所有过程
请注意,所有Model方法都必须在主线程上执行。 其他任何组件也是如此。 当然,您可以使用标准RxJava工具执行后台任务。
组成部分
MVIDroid中的一个组件是一组由共同目标联合而成的模型。 例如,您可以在组件中为屏幕选择所有模型。 换句话说,组件是其中包含的模型的外观,并且允许您隐藏实现细节(模型,转换函数及其关系)。 让我们看一下组件图:

从图中可以看到,该组件具有转换和重定向事件的重要功能。
Component函数的完整列表如下:
- 使用提供的转换功能将传入的表示事件和标签与每个模型相关联
- 将传出的模型标签带到外面
- 销毁组件时销毁所有模型并断开所有联系
该组件还具有自己的接口:
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 }
更详细地考虑Component接口:
- 包含两个通用参数:UiEvent-视图事件类型和状态-模型状态类型
- 包含用于访问“模型状态”组的状态字段(例如,作为接口或数据类)
- 消费者观点活动
- 它是一次性的,因此可以销毁组件及其所有模型
检视
您可能会猜到,需要一个视图来显示数据。 每个视图的数据被分组为一个视图模型,通常表示为数据类(科特林)。 考虑查看界面:
interface MviView<ViewModel : Any, UiEvent : Any> { val uiEvents: Observable<UiEvent> @MainThread fun subscribe(models: Observable<ViewModel>): Disposable }
这里的一切都比较简单。 两个通用参数:ViewModel-视图模型的类型和UiEvent-视图事件的类型。 uiEvents字段之一是Observable View Event,它使客户可以订阅这些相同的事件。 还有一个subscription()方法,可让您订阅视图模型。
使用范例
现在是时候尝试一些实践了。 我建议做一些非常简单的事情。 不需要太多努力就可以理解的东西,同时又给出了如何使用所有这些内容以及向哪个方向前进的想法。 让它成为一个UUID生成器:只要触摸一个按钮,我们就会生成一个UUID并将其显示在屏幕上。
投稿
首先,我们描述视图模型:
data class ViewModel(val text: String)
并查看事件:
sealed class UiEvent { object OnGenerateClick: UiEvent() }
现在我们实现View本身,为此,我们需要从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 } }
一切都非常简单:当我们收到新的UUID时,我们订阅UUID更改并更新TextView,并且当单击按钮时,我们发送OnGenerateClick事件。
型号
该模型将由两部分组成:接口和实现。
介面
interface UuidStore : MviStore<State, Intent, Nothing> { data class State(val uuid: String? = null) sealed class Intent { object Generate : Intent() } }
这里的一切都很简单:我们的接口扩展了MviStore接口,指示状态(State)和意图(Intent)的类型。 标签类型-没什么,因为我们的模型不生产它们。 该接口还包含State和Intent类。
为了实现模型,您需要了解它是如何工作的。 在模型的输入处,接收到Intent,然后使用特殊的IntentToAction函数将其转换为Actions。 动作被输入执行器,执行器执行它们并产生结果和标签。 然后将结果发送到Reducer,Reducer将当前状态转换为新状态。
所有四个组成模型:
- IntentToAction-将Intent转换为Action的函数
- MviExecutor-执行动作并产生结果和标签
- MviReducer-将对(状态,结果)转换为新状态
- MviBootstrapper是一个特殊的组件,允许您初始化模型。 赋予执行器所有相同的动作。 您可以执行一次性动作,也可以订阅数据源并对某些事件执行动作。 创建模型时,Bootstrapper会自动启动。
要创建模型本身,必须使用特殊的模型工厂。 它由MviStoreFactory接口及其MviDefaultStoreFactory的实现表示。 工厂接受模型的组成部分并签发即用型模型。
我们模型的工厂外观如下:
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) } } }
本示例介绍了模型的所有四个组成部分。 首先是工厂创建方法,然后是“操作和结果”,然后是承包商,最后是减速器。
组成部分
组件状态(状态组)由数据类描述:
data class States(val uuidStates: Observable<UuidStore.State>)
将新模型添加到组件时,它们的状态也应添加到组中。
而且,实际上,实现本身:
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 } } }
我们继承了MviAbstractComponent抽象类,指定了状态和视图事件的类型,将我们的模型传递给超类,并实现了状态字段。 此外,我们创建了一个转换函数,该函数会将“视图事件”转换为模型的意图。
映射视图模型
我们有条件和表示模型,是时候将其转换为另一种了。 为此,我们实现MviViewModelMapper接口:
object ViewModelMapper : MviViewModelMapper<States, ViewModel> { override fun map(states: States): Observable<ViewModel> = states.uuidStates.map { ViewModel(text = it.uuid ?: "None") } }
装订
仅组件和演示文稿的存在是不够的。 为了使一切开始工作,必须将它们连接起来。 现在该创建一个活动了:
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 ) } }
我们使用bind()方法,该方法使用一个Component和一个Views数组及其Models的映射器。 此方法是LifecycleOwner(分别是Activity和Fragment)上的扩展方法,并使用Arch包中的DefaultLifecycleObserver,它需要Java 8源兼容性。 如果由于某种原因您不能使用Java 8,则第二个bind()方法适合您,它不是扩展方法,并返回MviLifecyleObserver。 在这种情况下,您将必须自己调用生命周期方法。
参考文献
该库的源代码以及有关连接和使用的详细说明可以在GitHub上找到 。