1. 注意在协程中更新状态值时的线程安全问题
例如我们有如下代码:
data class MyUiState(val counter: Int = 0,val text: String = ""
)class MainViewModel: ViewModel() {private val _state = MutableStateFlow(MyUiState())val state = _state.asStateFlow()fun incrementCount() {_state.value = state.value.copy(counter = state.value.counter + 1)}
}
这样写可能看不出什么问题,但是进一步的你可能会这样写:
fun incrementCount() {viewModelScope.launch {_state.value = state.value.copy(counter = state.value.counter + 1)}
}
虽然viewModelScope
默认的协程上下文是 SupervisorJob + Dispatchers.Main.immediate
,但是一旦你这样写了,就可能会继续这样调用:
fun incrementCount() {viewModelScope.launch {withContext(Dispatchers.IO) {_state.value = state.value.copy(counter = state.value.counter + 1)}}viewModelScope.launch {withContext(Dispatchers.IO) {_state.value = state.value.copy(counter = state.value.counter + 1)}}
}
现在就存在潜在的线程并发安全问题了。假设两个线程同时进入更新代码块时,count
都是0
,它们各自会尝试将其+1
,但是最终的结果不是2
,而是1
(无论哪个线程先结束,另一个也只会将其改成1
,因为每个线程看到的本地副本的初始值都是0
)。
这其实是一个习惯问题。
解决方法是使用 state.update { }
来更新:
fun incrementCount() {_state.update {it.copy(counter = state.value.counter + 1)}
}
fun incrementCount1() {viewModelScope.launch {_state.update {it.copy(counter = state.value.counter + 1)}}viewModelScope.launch {_state.update {it.copy(counter = state.value.counter + 1)}}
}
这样,不管你的代码是否会在协程中更新都会得到安全保证,我们假设以后官方将viewModelScope
的调度器更改了,即便不是主线程也不会产生问题。
2. 注意应用因为低内存被系统杀死后状态恢复问题
如果应用回到后台,随时有可能因为内存不足而被系统杀死,这时用户从最近应用程序列表中返回时,会发现应用界面从头开始,上次看到的状态丢失。
例如前面的代码,假设用户调用了incrementCount()
将 count
值增加之后,回到后台去做其他事情,此时应用因为内存不足被系统杀死,用户再次返回应用,应用会重启,用户看到的将会是初始值 0。
解决这个问题可以使用 Saved State APIs ,包括 rememberSaveable
(Jetpack Compose) 和 onSaveInstanceState
(View system),以及ViewModel
中的SavedStateHandle
等等。
下面是一个使用 SavedStateHandle
的简单示例:
@Parcelize
data class MyUiState(val counter: Int = 0,val text: String = ""
): Parcelableclass MainViewModel(private val savedStateHandle: SavedStateHandle
): ViewModel() {val state = savedStateHandle.getStateFlow("state", MyUiState())fun incrementCount() {viewModelScope.launch {savedStateHandle["state"] = savedStateHandle.get<MyUiState>("state")?.copy(counter = state.value.counter + 1)}}
}class MyActivity: ComponentActivity() {val viewModel: MainViewModel by viewModels()...
}
注意:如果
ViewModel
构造函数只依赖于SavedStateHandle
,您无需为其提供工厂函数,viewModels()
函数将会正确为其初始化。
当然这取决于业务场景,如果你的业务没有这样的需求,那么应用重启之后回到初始状态的情况可能就是一种正常的不错的选择。但是作为开发者我们应该有这样的意识,一旦用户有这样的需求,我们就可以立即修改代码来达到需求。
如果你想了解更多关于状态恢复相关的内容以及 Saved State APIs 的使用,可以参考我之前整理的下面文章:
-
Jetpack架构组件库:Lifecycle、LiveData、ViewModel 中的 ViewModel 部分
-
Jetpack Compose 中的架构思想 中的状态持久化与恢复部分
3.不要在单例类中保存全局状态值
这可能是最严重的一个状态管理问题。
例如我们有如下代码:
object SessionStorage {var sessionToken: String? = null
}
这个单例类的意图是用户登录之后将登录接口返回的token
值保存在object
中,以便在任何地方都可以方便的访问到这个全局状态值。
假设用户登录成功,然后将token
值正确的保存到了这个sessionToken
当中,然后用户继续浏览,然后用户又切到其他应用,此时当前应用因为某种原因进程被杀死重启(如内存不足),那么这个时候 object
类的成员变量会被重新初始化为初始的零值(对于上面的代码来说就是初始化为null
),用户再次返回应用时,所有使用到这个sessionToken
的地方都会得到一个空值。对于用户来说,他已经登录过,但是此时他不能做任何事情(比如进行任何操作可能会得到一个“用户未登录”的toast提示)。
解决这个问题的办法是使用状态持久化存储,即便应用重启,我们依然可以从本地读取到正确的状态值。
状态持久化存储的方案可以参考我之前整理的下面文章,这里不再赘述:
- Jetpack架构组件库:DataStore
- Jetpack架构组件库:Room