前言
使用 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()
}
这么做比之前优雅一些了,但还是不够完美:
onChange
还是得写成viewModel.setDarkThemePreference(it)
- 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 代码非常简洁。