You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

旋转后恢复运行动画的MVI架构解决方案咨询

用MVI解决屏幕旋转后进度动画不触发的问题

这个问题的核心在于混淆了MVI中「状态(State)」和「一次性副作用(Side Effect)」的职责,以及没有处理状态恢复后的一次性操作触发逻辑。下面是具体的解决方案和思路:

1. 先明确MVI中状态与副作用的边界

在MVI里,状态是用来描述UI当前的持久化状态(比如游戏处于哪个阶段、动画当前进度),它是可恢复、可重复渲染的;而像启动动画这种一次性、不影响UI持久状态的操作,属于副作用——但如果动画需要在旋转后继续(比如进度需要保留),那进度就要放进状态里。

你的问题中,旋转后动画不展示,是因为状态只记录了「游戏已启动/正在启动」,但没有标记「是否需要触发动画」,导致UI认为已经处理过这个操作。

2. 解决方案一:用「消费式状态标记」处理一次性动画触发

这是最贴合MVI单一数据源原则的方案,通过在状态中添加一个可重置的标记来控制动画触发:

步骤1:定义包含触发标记的状态类

data class GameState(
    val gamePhase: GamePhase,
    // 标记是否需要触发进度动画,触发后立即重置为false
    val shouldTriggerProgressAnim: Boolean = false,
    // 如果是带进度的动画,添加进度字段用于旋转后恢复
    val progress: Int = 0
)

enum class GamePhase {
    Idle,
    Starting,
    Started
}

步骤2:ViewModel中的状态更新逻辑

fun handleIntent(intent: Intent) {
    when(intent) {
        is Intent.StartGame -> {
            // 进入启动阶段,同时标记需要触发动画
            updateState {
                it.copy(
                    gamePhase = GamePhase.Starting,
                    shouldTriggerProgressAnim = true,
                    progress = 0
                )
            }
            // 模拟游戏初始化逻辑(比如加载资源)
            viewModelScope.launch {
                simulateGameInit()
                // 初始化完成后,进入已启动状态
                updateState {
                    it.copy(
                        gamePhase = GamePhase.Started,
                        shouldTriggerProgressAnim = false
                    )
                }
            }
        }
        // UI触发动画后,通知ViewModel重置标记
        is Intent.ProgressAnimTriggered -> {
            updateState {
                it.copy(shouldTriggerProgressAnim = false)
            }
        }
        // 后台更新进度时同步到状态(针对带进度的动画)
        is Intent.UpdateProgress -> {
            updateState {
                it.copy(progress = intent.newProgress)
            }
        }
    }
}

步骤3:UI层的处理逻辑

viewModel.state.collect { state ->
    when(state.gamePhase) {
        GamePhase.Starting -> {
            if(state.shouldTriggerProgressAnim) {
                // 启动进度动画
                progressBar.startAnimation()
                // 通知ViewModel标记已消费,避免重复触发
                viewModel.sendIntent(Intent.ProgressAnimTriggered)
            }
            // 如果是带进度的动画,同步状态中的进度
            progressBar.setProgress(state.progress)
        }
        GamePhase.Started -> {
            progressBar.stopAnimation()
            // 展示游戏界面
        }
        // ...其他状态处理
    }
}

这样处理后:

  • 如果游戏已经进入Started阶段,屏幕旋转后状态中的shouldTriggerProgressAnimfalse,UI不会重复启动动画;
  • 如果旋转时还在Starting阶段,状态会恢复当前的progress,UI可以继续展示动画,且shouldTriggerProgressAnim已经是false,不会重复触发启动。

3. 解决方案二:分离副作用流处理一次性操作

如果不想在状态中添加触发标记,可以让ViewModel额外暴露一个副作用流(比如用SharedFlow),专门发送一次性操作指令:

步骤1:ViewModel定义副作用流

class GameViewModel : ViewModel() {
    private val _sideEffects = MutableSharedFlow<SideEffect>()
    val sideEffects = _sideEffects.asSharedFlow()

    sealed class SideEffect {
        object ShowProgressAnimation : SideEffect()
    }

    // ...状态流和意图处理逻辑
}

步骤2:触发动画时发送副作用

fun handleIntent(intent: Intent) {
    when(intent) {
        is Intent.StartGame -> {
            // 发送副作用触发动画
            viewModelScope.launch {
                _sideEffects.emit(SideEffect.ShowProgressAnimation)
            }
            // 更新状态为启动中
            updateState { it.copy(gamePhase = GamePhase.Starting) }
            // ...后续初始化逻辑
        }
    }
}

步骤3:UI订阅副作用流

lifecycleScope.launch {
    viewModel.sideEffects.collect { sideEffect ->
        when(sideEffect) {
            SideEffect.ShowProgressAnimation -> {
                progressBar.startAnimation()
            }
        }
    }
}

这个方案需要注意:屏幕旋转后,新的UI订阅副作用流时,默认不会收到之前发送的副作用(如果用replay = 0SharedFlow),这符合需求——因为旋转后如果游戏已经启动,不需要再触发动画;但如果旋转时还在启动阶段,UI需要根据GamePhase.Starting状态来判断是否要继续展示动画,这时候还是需要把进度放进状态里。

回答你的核心疑问

是否需要通过MVI状态流发送所有事件,在状态中逐次更新进度并同步视图?

是的,所有需要在屏幕旋转后恢复的UI状态(比如动画进度、游戏阶段)都应该放进状态流中,这样才能保证旋转前后UI的一致性。而一次性的触发操作(比如启动动画),可以通过状态中的消费式标记或者副作用流处理,但结合状态的方式更符合MVI的单一数据源原则,避免状态和副作用的不一致。

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

火山引擎 最新活动