这两年在写 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 完成方式,在这儿我就抛砖引玉,希望还有愈加解放心智的做法。