前言

使用 Jetpack Compose 的时候,经常会遇到需要在 Composable 中将界面元素与 ViewModel 关联。当遇到大量的简单状态时,我们希望代码尽可能的简单。有时候在修改状态时我们还希望同时产生一些副作用(比如写入配置文件)。

探索

第一次设计

在刚开始从传统的 View 迁移到 Compose 时,自然而然就想到了经典的 ViewModel + StateFlow (LiveData) 结构:

class MyViewModel : ViewModel() {
    var _darkThemePreference =
        MutableStateFlow(MyApplication.pref.getInt("dark_theme", FOLLOW_SYSTEM))
    val darkThemePreference: StateFlow<Int> = _darkThemePreference

    fun setDarkThemePreference(value: Int) {
        _darkThemePreference.value = value
        MyApplication.pref.edit().putInt("dark_theme", value).apply()
    }
}

@Composable
fun Example() {
    val viewModel = viewModel<MyViewModel>()
    val darkThemePreference by viewModel.darkThemePreference.collectAsState()
    ThemeButton(
        isDarkTheme = darkThemePreference,
        onChange = { viewModel.setDarkThemePreference(it) }
    )
}

初看上去还不错,和 View 的观察者模式差不多,但仔细一看,我们将简单的问题复杂化了:毕竟我们需要的仅仅是改变一个状态,写入配置文件,并同时触发 UI 重组而已。那么能够更简单吗?

第二次设计

我们想直接将 State 放到 ViewModel 里,然后让 Composable 根据状态改变自动重组,因此想到将代理放到 ViewModel 中去:

var darkThemePreference by
MutableState(MyApplication.pref.getInt("dark_theme", FOLLOW_SYSTEM))
    set(value) {
        field = value
        MyApplication.pref.edit().putInt("dark_theme", value).apply()
    }

然后,IDE 喜闻乐见地报错了:因为 Kotlin 不支持多重代理!

因此只能采用一种比较折中的办法:

var darkThemePreference by
MutableStateFlow(MyApplication.pref.getInt("dark_theme", FOLLOW_SYSTEM))
    private set

@JvmName("setDarkThemePreferenceImpl")
fun setDarkThemePreference(value: Boolean) {
    darkThemePreference = value
    MyApplication.pref.edit().putInt("dark_theme", value).apply()
}

这么做比之前优雅一些了,但还是不够完美:

  1. onChange还是得写成 viewModel.setDarkThemePreference(it)
  2. ViewModel 中写法还是不够简洁,还得加注解

第三次设计

因此,一个好的办法是自己造一个多重代理的轮子!

实现:GitHub Gist

MultiDelegateState.kt

package icu.nullptr.util.compose

import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import kotlin.reflect.KProperty

class DelegateState<T>(initial: T, private val sideEffectSetter: (T) -> Unit) {
    private var snapshot by mutableStateOf(initial)

    var value: T
        get() = snapshot
        set(value) {
            snapshot = value
            sideEffectSetter(snapshot)
        }

    operator fun component1(): T = value

    operator fun component2(): (T) -> Unit = { value = it }
}

fun <T> delegateStateOf(initial: T, sideEffectSetter: (T) -> Unit) = DelegateState(initial, sideEffectSetter)

@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> DelegateState<T>.getValue(thisObj: Any?, property: KProperty<*>): T = value

@Suppress("NOTHING_TO_INLINE")
inline operator fun <T> DelegateState<T>.setValue(thisObj: Any?, property: KProperty<*>, value: T) {
    this.value = value
}

MultiDelegateStateExample.kt

...
import icu.nullptr.util.compose.delegateStateOf
import icu.nullptr.util.compose.getValue
import icu.nullptr.util.compose.setValue

class ExampleViewModel : ViewModel() {
    var darkThemePreference by
    delegateStateOf(MyApplication.pref.getInt("dark_theme", FOLLOW_SYSTEM)) {
        MyApplication.pref.edit().putInt("dark_theme", it).apply()
    }
}

// When darkThemePreference change, related composables will automatically recompose
@Composable
fun Example() {
    val viewModel = viewModel<ExampleViewModel>()
    ThemeButton(
        isDarkTheme = viewModel.darkThemePreference,
        onChange = { viewModel.darkThemePreference = it }
    )
}

这样,我们就做到了把 ViewModel 中的 State 当成一个普通的变量来使用,并在修改时自动触发 SideEffort 和 UI 重组,并且 ViewModel 代码非常简洁。

最后修改:2022 年 03 月 31 日
如果觉得我的文章对你有用,请随意赞赏