You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Jetpack Compose中两种rememberSaveable调用的差异与最佳实践

关于Jetpack Compose中rememberSaveable两种写法的最佳实践疑问

我做了一个参数录入界面,参数存储在Room数据库中,用Room的Flow维护状态,确保导航离开界面后再返回时数值仍能保留。代码里有两种rememberSaveable的写法,功能完全一致,但之前看到资料推荐较长的那种写法,说和传入的key有关。想知道哪种是最佳实践,以及原因是什么?

@Composable
fun BuyInSection(
    sessionViewModel: SessionViewModel,
    sessionId: Long,
    buyInError: Boolean,
) {
    val session by sessionViewModel.currentValues.collectAsState()
    val (buyIn, setBuyIn) = rememberSaveable { mutableStateOf(session.buyIn) }
   // val (buyIn, setBuyIn) = rememberSaveable(session.buyIn) { mutableStateOf(session.buyIn) }

    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 10.dp),
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        Text(
            text = "Buy In",
            fontWeight = FontWeight.Bold,
            modifier = Modifier
                .weight(1f)
                .background(if (buyInError) Color.Red else Color.White)
                .padding(10.dp)
                .align(Alignment.CenterVertically)
        )
        OutlinedTextField(
            value = buyIn.toString(),
            onValueChange = { setBuyIn(it.toIntOrNull() ?: 0) },
            modifier = Modifier
                .weight(1f)
                .padding(10.dp)
                .align(Alignment.CenterVertically),
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
        )
        Button(
            enabled = sessionViewModel.isSessionRunning.value,
            modifier = Modifier
                .weight(1f),
            onClick = {
                sessionViewModel.updateAttribute(
                    sessionId.toInt(),
                    SessionAttribute.BUY_IN,
                    SessionRepository.IntValue(buyIn))
                if (buyInError) sessionViewModel.completeSession()
            }
        ) {
            Text(text = "Enter Buy In")
        }
    }
}

补充代码:ViewModel及更新后的UI模块

class SessionViewModel(application: Application): ViewModel() {
    private var sessionRepository: SessionRepository

    init {
        val sessionDb = SessionRoomDatabase.getInstance(application)
        val sessionDao = sessionDb.SessionDao()
        sessionRepository = SessionRepository(sessionDao)
    }

    private val _sessionId = MutableStateFlow<Long>(0)
    val sessionId: MutableStateFlow<Long> = _sessionId

    private val _incompleteValues = MutableStateFlow(emptyList<String>())
    val incompleteValues: StateFlow<List<String>> = _incompleteValues

    private var _timeUnit = MutableStateFlow(0)
    val timeUnit: MutableStateFlow<Int> = _timeUnit

    fun setTimeAmount(amount: Int) {
        _timeUnit.value = amount
    }

    private val _elapsedTimeText =   MutableStateFlow("")
    val elapsedTimeText: MutableStateFlow<String> = _elapsedTimeText

    private val _startTimeText = MutableStateFlow("Press to start session time")
    val startTimeText: MutableStateFlow<String> = _startTimeText

    @OptIn(ExperimentalTime::class)
    fun startSessionTimer() {
        val current = LocalDateTime.now()
        val formatter = DateTimeFormatter.ofPattern("MM/dd/yyyy HH:mm:ss")
        val formatted = current.format(formatter)
        updateAttribute(sessionId.value.toInt(),
            SessionAttribute.START_TIME,
            SessionRepository.StringValue(formatted))

        _startTimeText.value = formatted
        val startTime = TimeSource.Monotonic.markNow()
        viewModelScope.launch {
            while (isSessionRunning.value) {
                delay(1000)
                _elapsedTimeText.value = startTime.elapsedNow().inWholeSeconds.seconds.toString()
            }
        }
    }

    fun getAll(): List<Session> {
        return sessionRepository.getAll()
    }

    //When add cash session is called, a new session is created and inserted into the database
    //The session id is then returned and used to update the session attributes
    //Also calls getCurrentValues to update the UI with the current values
    private fun insertAll(session: Session) {
        viewModelScope.launch(Dispatchers.IO) {
            _sessionId.value = sessionRepository.insertAll(session)
            getCurrentValues()
        }
    }

    fun addCashSession() {
        val session = Session()
        session.location = ""
        session.startTime = ""
        session.endTime = ""
        session.buyIn = 0
        session.addOn = 0
        session.rakePaid = 0
        session.timePaid = 0
        session.tipsPaid = 0
        session.jackpotPaid = 0
        session.cashOut = 0
        session.gameId = 0
        session.notes = ""

        insertAll(session)
    }

    fun updateAttribute(id: Int, attributeName: SessionAttribute, newValue: SessionRepository.AttributeValue) {
        viewModelScope.launch {
            sessionRepository.updateAttribute(id, attributeName, newValue)
        }
    }

    // isSessionRunning is a MutableStateFlow that holds the current session running state.
    private val _isSessionRunning = MutableStateFlow(false)
    val isSessionRunning: MutableStateFlow<Boolean> = _isSessionRunning

    fun setIsSessionRunning(isRunning: Boolean) {
        _isSessionRunning.value = isRunning
    }

    // getById is a Flow that holds the current session database values.
    private fun getById(id: Int): Flow<Session> {
        return sessionRepository.getById(id)
    }

    //completeSession is called when the session is complete. Checks to see if there are any
    //incomplete values and updates the UI accordingly with red background.
    fun completeSession() {
        viewModelScope.launch {
            val session = getById(sessionId.value.toInt()).first()
            _incompleteValues.value = session.incompleteAttributes()
            isSessionRunning.value = incompleteValues.value.isNotEmpty()
        }
    }

    // CurrentValues is a MutableStateFlow that holds the current session database values.
    // It is initialized when a new session is started.
    private var _currentValues = MutableStateFlow(Session())
    val currentValues: MutableStateFlow<Session> = _currentValues

    private fun getCurrentValues() {
        viewModelScope.launch {
            getById(sessionId.value.toInt()).collect { session ->
                _currentValues.value = session
            }
        }
    }
}
@Composable
fun BuyInSection(
    sessionViewModel: SessionViewModel,
    isSessionRunning: Boolean,
    sessionId: Long,
    session: Session,
    incompleteValues: List<String>
) {
    Row(
        modifier = Modifier
            .fillMaxWidth()
            .padding(horizontal = 10.dp),
        horizontalArrangement = Arrangement.SpaceBetween
    ) {
        Text(
            text = "Buy In",
            fontWeight = FontWeight.Bold,
            modifier = Modifier
                .weight(1f)
                .background(if (incompleteValues.contains("buyIn")) Color.Red else Color.White)
                .padding(10.dp)
                .align(Alignment.CenterVertically)
        )
        OutlinedTextField(
            enabled = isSessionRunning,
            value = session.buyIn.toString(),
            onValueChange = { sessionViewModel.updateAttribute(sessionId.toInt(),
                SessionAttribute.BUY_IN,
                SessionRepository.IntValue(it.toIntOrNull() ?: 0))
                if (incompleteValues.contains("buyIn")) sessionViewModel.completeSession() },
            modifier = Modifier
                .weight(1f)
                .padding(10.dp)
                .align(Alignment.CenterVertically),
            keyboardOptions = KeyboardOptions(keyboardType = KeyboardType.Number)
        )
    }
}

最佳实践分析

首先明确rememberSaveable的核心作用:在Compose组件重组、配置变更(比如旋转屏幕)甚至进程被杀后恢复状态。两种写法的差异在于是否给rememberSaveable传入了key参数(这里的session.buyIn就是作为key使用)。

1. 两种写法的本质区别

  • 短写法:rememberSaveable { mutableStateOf(session.buyIn) }
    没有显式传key,Compose会自动基于组件在Compose树中的位置生成默认key。只要组件位置不变,状态就能正确保存和恢复。
  • 长写法:rememberSaveable(session.buyIn) { mutableStateOf(session.buyIn) }
    把session.buyIn作为key传入,当key的值变化时,rememberSaveable会重新执行初始化lambda,生成新的MutableState。

2. 哪种更适合你的场景?

看第一个BuyInSection版本的逻辑,你用本地buyIn状态临时保存用户输入,直到点击按钮才同步到数据库。这种情况下推荐用短写法,原因如下:

  • 本地状态是用来缓存输入的,不需要跟着数据库里的buyIn值变化自动更新。如果用长写法,一旦数据库中buyIn值被其他地方修改,本地状态会被重置成数据库值,直接覆盖用户正在输入的内容,体验极差。
  • 短写法的默认key足够应对你的场景:只要BuyInSection在Compose树中的位置不变,导航离开再返回、配置变更时状态都能正确恢复。

只有在以下场景中,长写法才是必要的:

  • 组件状态需要依赖外部值,当外部值变化时必须重置状态(比如列表项的状态要跟着项数据变化)。
  • 组件在Compose树中的位置可能变化(比如LazyColumn中的项移动),默认key会失效,需要显式传入唯一key绑定状态和组件。

3. 额外建议:优化状态管理

你补充的第二个BuyInSection版本更合理,直接用session.buyIn作为输入框值,输入时同步调用updateAttribute更新数据库:

  • 去掉了本地状态缓存,输入框值和数据库完全同步,避免了状态不一致的问题。
  • 不需要rememberSaveable,状态完全由ViewModel的Flow维护,导航返回时自动从数据库获取最新值。

这种模式符合Jetpack Compose单向数据流原则,减少了状态不一致的风险,推荐使用这个版本的实现。

内容的提问来源于stack exchange,提问作者JD74

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.26 03:07:36