Loading... ## 前言 使用 Jetpack Compose 的时候,经常会遇到需要在 Composable 中将界面元素与 ViewModel 关联。当遇到大量的简单状态时,我们希望代码尽可能的简单。有时候在修改状态时我们还希望同时产生一些副作用(比如写入配置文件)。 ## 探索 ### 第一次设计 在刚开始从传统的 View 迁移到 Compose 时,自然而然就想到了经典的 ViewModel + StateFlow (LiveData) 结构: ```kotlin 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 中去: ```kotlin 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 不支持多重代理! 因此只能采用一种比较折中的办法: ```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](https://gist.github.com/Dr-TSNG/186e407525cef1ea60789eb2fc144e41) `MultiDelegateState.kt` ```kotlin 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` ```kotlin ... 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 日 © 允许规范转载 赞 0 如果觉得我的文章对你有用,请随意赞赏