Jetpack Compose 状态管理的三个误区

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 = ""
): Parcelable

class 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 的使用,可以参考我之前整理的下面文章:

3.不要在单例类中保存全局状态值

这可能是最严重的一个状态管理问题。

例如我们有如下代码:

object SessionStorage {
    var sessionToken: String? = null
}

这个单例类的意图是用户登录之后将登录接口返回的token值保存在object 中,以便在任何地方都可以方便的访问到这个全局状态值。

假设用户登录成功,然后将token值正确的保存到了这个sessionToken当中,然后用户继续浏览,然后用户又切到其他应用,此时当前应用因为某种原因进程被杀死重启(如内存不足),那么这个时候 object 类的成员变量会被重新初始化为初始的零值(对于上面的代码来说就是初始化为null),用户再次返回应用时,所有使用到这个sessionToken的地方都会得到一个空值。对于用户来说,他已经登录过,但是此时他不能做任何事情(比如进行任何操作可能会得到一个“用户未登录”的toast提示)。

解决这个问题的办法是使用状态持久化存储,即便应用重启,我们依然可以从本地读取到正确的状态值。

状态持久化存储的方案可以参考我之前整理的下面文章,这里不再赘述:

  • 15
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

川峰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值