这两年在写 Compose 运用的时分,在 Compose 中实践了 MVVM 和 MVI 两个架构,发现 Compose 配合 MVI 写起来十分丝滑,乃至更进一步能够用 Compose 替代 ViewModel 写事务逻辑,乃至带来额外的优势,写个文章分享,我就来个抛砖引玉。
MVI 架构简介
运用架构陈词滥调了,但是仍是先回忆一下现在比较流行,大家也比较了解的 MVVM。在 MVVM 的指导思想之下你会写出这样的代码:
class CounterViewModel: ViewModel() {
private val _count = MutableLiveData<Int>()
val count: LiveData<Int> get() = _count
private val _input = MutableLiveData<String>()
val input: LiveData<String> get() = _input
fun increment() {
_count.value = _count.value?.plus(1)
}
fun input(value: String) {
_input.value = value
}
}
而在 MVI 的指导思想下你会写出这样的代码:
class CounterViewModel : ViewModel() {
private val _count = MutableStateFlow(0)
private val _input = MutableStateFlow("")
val state = combime(
_count,
_input
) { count, input ->
CounterState(
count = it.toString(),
input = input,
)
}
fun increment() {
_count.value++
}
fun input(value: String) {
_input.value = value
}
}
能够看到一个比较明显的不同:
- MVVM 中整个页面的 State 来源较为涣散,往往会露出多个 LiveData/Flow 给 UI 层,UI 层也会订阅多个 LifeData/Flow。
- MVI 中整个页面的 State 由一个或多个 Flow 组合而成,露出给 UI 层的只要一个 LiveData/Flow,UI 层只需求订阅这一个 LiveData/Flow 即可。
MVI 这样做的优点便是:当页面开端复杂之后,你依然能够很清晰的掌握整个页面的状况,特别是当 ViewModel 中多个 LiveData/Flow 之间会有依赖的时分。
至于为什么 MVVM 会给 UI 露出这么多,简略的朔源一下:
MVVM由微软架构师Ken Cooper和Ted Peters开发,通过运用WPF(微软.NET图形系统)和Silverlight(WPF的互联网运用衍生品)的特性来简化用户界面的事件驱动程式设计。微软的WPF和Silverlight架构师之一John Gossman于2005年在他的博客上发表了MVVM。
而在 WPF 中,规范的 UI 数据绑定是这样的:
<StackPanel>
<TextBlock Text="{Binding Counter}"/>
<TextBox Text="{Binding Input, Mode=TwoWay}"/>
<Button Content="Increment" Command="{Binding IncrementCommand}"/>
</StackPanel>
而 ViewModel 是这样界说的:
public class CounterViewModel : INotifyPropertyChanged
{
private int _counter;
private string _input = "";
public int Counter
{
get => _counter;
set
{
_counter = value;
OnPropertyChanged();
}
}
public string Input
{
get => _input;
set
{
_input = value;
OnPropertyChanged();
}
}
public ICommand IncrementCommand { get; } = new RelayCommand(() => Counter++);
public event PropertyChangedEventHandler? PropertyChanged;
protected virtual void OnPropertyChanged([CallerMemberName] string? propertyName = null)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
}
假如用 CommunityToolkit.Mvvm 还能更简略些:
[ObservableObject]
public partial class CounterViewModel
{
[ObservableProperty]
private int _counter;
[ObservableProperty]
private string _input = "";
[RelayCommand]
private void Increment()
{
Counter++;
}
}
由于 UI 和代码是两种言语,并且需求直接在 UI 中绑定不同的数据,乃至存在双向绑定,所以在 MVVM 中会露出十分多的属性给 UI。
Android 的 DataBinding 也是从这儿抄来的(但其实抄的欠好)。
Compose 中实践
在 Compose 中实践 MVI 也十分简略,比方:
@Composable
fun Counter(
viewModel: CounterViewModel = viewModel()
) {
val state by viewModel.state.collectAsState(initial = CounterState(0, ""))
Counter(
state = state,
onIncrement = viewModel::increment,
onInput = viewModel::input
)
}
@Composable
fun Counter(
state: CounterState,
onIncrement: () -> Unit = {},
onInput: (String) -> Unit = {},
) {
Column {
Text(text = state.count)
TextField(
value = state.input,
onValueChange = {
onInput(it)
}
)
Button(
onClick = {
onIncrement()
}
) {
Text(text = "Increment")
}
}
}
假如是 MVVM 的话,上面会写成这样:
@Composable
fun Counter(
viewModel: CounterViewModel = viewModel()
) {
val input by viewModel.input.observeAsState(initial = "")
val count by viewModel.count.observeAsState(initial = 0)
Counter(
count = count,
input = input,
onIncrement = viewModel::increment,
onInput = viewModel::input,
)
}
@Composable
fun Counter(
input: String,
count: Int,
onIncrement: () -> Unit = {},
onInput: (String) -> Unit = {},
) {
Column {
Text(text = count.toString())
TextField(
value = input,
onValueChange = {
onInput(it)
}
)
Button(
onClick = {
onIncrement()
}
) {
Text(text = "Increment")
}
}
}
相比之下,在运用 MVI 时,整个 Compose 页面都是由一个状况驱动的,即便页面复杂度进步也依然是一个状况,而 MVVM 就会有许多状况,这会进步 Compose 代码的复杂度,难以维护,想象一下你有许多行 val xxx by viewModel.xxx.observeAsState
。
emmm 好像实践这块没什么说的。
Compose 写事务逻辑
更进一步,能够用 Compose 替代 ViewModel 写事务逻辑,来规避一些 ViewModel 的局限,仍是上面的比方,当页面开端变得复杂,ViewModel 中状况开端变多的时分,输出 UI State 的代码可能会像这样:
class CounterViewModel : ViewModel() {
//...
val state = combime(
_count,
_input,
_list,
_data,
_xxx,
//...
) { count, input, list, data, xxx /*...*/ ->
}
//...
}
当你组合的 Flow 越来越多的时分,combime
函数就会越来越长,看起来就很费事很累,更不要提你还要在 .collectAsState
的时分给个初始值了,我想这直接挡掉了大部分人实践 MVI 的主意。
那么有没有什么办法不用很费事很累就能够实践 MVI 呢?
有请 Molecule
Molecule 是由 jw 大神(没错便是那个 jw)编写的运用 Compose 写事务逻辑的一个库(或许一个思路)。
必定有人会有疑问:Compose 不是 UI 结构吗?怎么还能写事务逻辑了?莫非设计形式扔了直接在 UI 里面写事务逻辑?
Compose 和 Compose UI
首先需求区分两个概念,Compose 和 Compose UI。
Compose UI 便是咱们十分了解的,用来画 UI 的那些。而抛开 Compose UI,仅保存 Compose Runtime 和 Compose Compiler,这便是不带任何 UI 的 Compose。举个比方:
@Composable
fun CounterPresenter(): CounterState {
var count by remember { mutableStateOf(0) }
//...
return CounterState(count)
}
这便是不带任何 UI 的 Compose,这儿暂时称为 Compose Presenter。
对 Compose 稍有了解的应该都知道,当 count
被改变的时分,就会触发一次 recomposition,CounterPresenter
就会回来一个新的 CounterState
,而这一点特性恰巧和 Flow
十分类似,假如咱们加以运用,上面的 ViewModel 就能够写成这样:
@Composable
fun CounterPresenter(
action: Flow<CounterAction>,
): CounterState {
var count by remember { mutableStateOf(0) }
var input by remember { mutableStateOf("") }
LaunchedEffect(action) {
action.collect { action ->
when (action) {
is CounterAction.Increment -> count++
is CounterAction.Input -> input = action.value
}
}
}
return CounterState(
count = count.toString(),
input = input,
)
}
在 Compose UI 中就能够这样运用:
@Composable
fun Counter() {
val channel = remember { Channel<CounterAction>() }
val flow = remember(channel) { channel.consumeAsFlow() }
val state = CounterPresenter(action = flow)
Counter(
state = state,
onIncrement = {
channel.trySend(CounterAction.Increment)
},
onInput = {
channel.trySend(CounterAction.Input(it))
}
)
}
是不是看着比 combime
要舒畅多了?假如需求组合的状况变多,写起来也完全没有问题,不会像 combime
那样令代码很快胀大。
还有,咱们经常会遇到这样的状况:有一些事务逻辑会在不同当地重复运用,或许当一个页面十分复杂的时分,此刻一般能够笼统出 UseCase,或许笼统出基类 ViewModel。而假如运用 Compose 编写事务逻辑,就会发现,不仅 UI 是可组合的,事务逻辑也是能够组合的:
@Composable
fun CounterPresenter(
action: Flow<CounterAction>,
): CounterState {
//...
val channel = remember { Channel<CounterAction>() }
val flow = remember(channel) { channel.consumeAsFlow() }
val otherState = OtherPresenter(flow)
LaunchedEffect(action) {
action.collect { action ->
when (action) {
//..
is CounterAction.OtherAction -> channel.trySend(action.action)
}
}
}
return CounterState(
//...
otherState = otherState,
)
}
@Composable
fun OtherPresenter(
action: Flow<OtherAction>,
): OtherState {
//..
return OtherState(
//...
)
}
当一个页面十分复杂的时分,拆分 Compose Presenter 成为一个个小的 Compose Presenter,这样可维护性是大大高于一个十分大的 ViewModel的。
简略说的话,在 MVI 架构下,Compose 替代 ViewModel 写事务逻辑有这几个优势:
- 不会发生
combime
那样很简略导致代码胀大的问题 - 事务逻辑也是可组合的,意味着你能够给页面上的一个当地单独写一个 Compose Presenter,最后再在顶层组合成为这个页面的 State,这样不仅有助于理清事务逻辑,方便修改,还能够很简略的编写单元测试,大大降低维护成本,也会进步编写功率
- 由于不依赖 ViewModel,能够跨渠道运转或测试,不再局限于 Android 渠道。
Molecule 的效果
上面写的好像没有用到 Molecule,由于这些 Compose Presenter 和 Compose UI 都履行在一个 Composition 上,而 Molecule 的效果,便是将两者分开,分别履行在不同的 Composition 上。比方:
class CounterActivity : CompomentActivity() {
private val scope = CoroutineScope(Main)
override fun onCreate(savedInstanceState: Bundle?) {
//...
val flow = //...
val models = scope.launchMolecule(clock = RecompositionClock.ContextClock) {
CounterPresenter(flow)
}
setContent {
val state by model.collectAsState()
//...
}
}
override fun onDestroy() {
super.onDestroy()
scope.cancel()
}
}
此刻 Compose Presenter 和 Compose UI 履行在不同的 Composition 上。分开履行的优点除了在 Compose Presenter 的履行不会影响到 UI 之外,还有一个用途便是,Compose Presenter 能够给 XML View 运用:
class CounterActivity : CompomentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
//...
setContentView(R.layout.counter_activity)
val flow = //...
val models = scope.launchMolecule(clock = RecompositionClock.ContextClock) {
CounterPresenter(flow)
}
scope.launch(start = UNDISPATCHED) {
models.collect { model ->
counterText.text = model.count
}
}
}
}
由于scope.launchMolecule
回来的是一个 StateFlow<T>
,这是十分规范的 kotlinx coroutines 里面的组件,所以即便没有 Compose UI 也能够运用。
不过我更喜欢运用纯 Compose UI 来编写运用,现在让我再回去写 XML View 现已回不去了。这样依赖 Activity 还需求手动管理 CoroutineScope 的方式依然仍是有些繁琐,有没有再简略一点的?
接入 PreCompose
PreCompose 给 Compose 提供了跨渠道的 Navigation 和 Lifecycle/ViewModel 支撑,现在支撑 Android/iOS/JVM/Web/macOS 渠道,并且在最近的一次更新中还添加了 Molecule 的支撑,用法十分的简略:
@Composable
fun Counter() {
val (state, channel) = rememberPresenter { CounterPresenter(it) }
Counter(
state = state,
onIncrement = {
channel.trySend(CounterAction.Increment)
},
onInput = {
channel.trySend(CounterAction.Input(it))
}
)
}
完整的比方在这儿。
这下编写事务逻辑只需求关心事务逻辑自身,再也不需求关心其他琐碎的事情,同时还能享受到 Compose Presenter 带来的各种优势,十分的解放心智。
总结
MVI 在前端现已实践了很长时刻了,各种结构层出不穷,最经典的 redux 都很久了。和 React 一样同为声明式 UI 的 Compose,在编写方式上都有十分类似的当地,所以在 Compose 上实践 MVI 是再天然不过的事情。只不过这儿另辟蹊径,运用 Compose 的特性,运用 Compose 自身替代 ViewModel,达到了一种更简略的 MVI 完成方式,在这儿我就抛砖引玉,希望还有愈加解放心智的做法。