实战:多Tab页面的状态同步管理

多Tab页面,说白了就是底部导航栏那几个标签页。每个Tab都有自己的状态,但有时候它们之间又需要互相通信。比如你在「首页」收藏了一篇文章,切换到「我的」页面时,收藏列表要立刻更新。

这种场景,我遇到过太多次了。早期我用LiveData+EventBus硬扛,结果代码越写越乱。后来用密封类+内联类重新设计,整个状态管理变得清晰多了。

问题本质:Tab间状态同步的痛点

先想想看,多Tab页面到底难在哪?

  • 状态隔离:每个Tab应该有自己的ViewModel,互不干扰
  • 跨Tab通信:一个Tab的操作,要通知其他Tab刷新
  • 生命周期:Tab被销毁后重新创建,状态不能丢
  • 内存泄漏:不小心持有Activity引用,就等着OOM吧

我早期踩过一个坑:用静态变量存Tab状态。结果用户切到后台再回来,所有Tab都乱了。嗯,从那以后我再也不敢用静态变量管理UI状态了。

密封类:定义Tab状态的核心

密封类在这里扮演什么角色?它定义了「Tab可能处于的所有状态」。每个Tab的状态,都是密封类的一个子类。

// 定义Tab状态
sealed class TabState {
    object Loading : TabState()
    data class Content(val data: List<Item>) : TabState()
    data class Error(val message: String) : TabState()
    object Empty : TabState()
}

// 每个Tab的ViewModel
class HomeTabViewModel : ViewModel() {
    private val _state = MutableStateFlow<TabState>(TabState.Loading)
    val state: StateFlow<TabState> = _state.asStateFlow()
    
    fun loadData() {
        viewModelScope.launch {
            _state.value = TabState.Loading
            try {
                val result = repository.fetchData()
                _state.value = if (result.isEmpty()) {
                    TabState.Empty
                } else {
                    TabState.Content(result)
                }
            } catch (e: Exception) {
                _state.value = TabState.Error(e.message ?: "未知错误")
            }
        }
    }
}

你看,每个Tab的状态都被密封类约束住了。不会出现「这个Tab的状态是null」或者「状态类型搞混」的情况。编译器会帮你检查所有分支,少写一个when分支都编译不过。

内联类:轻量级的状态包装

内联类在这里干嘛用?它用来包装那些「需要传递但不想创建新对象」的数据。比如Tab切换时的参数传递。

// 内联类包装Tab切换参数
inline class TabSwitchPayload(val raw: String) {
    val tabId: Int get() = raw.substringBefore(":").toInt()
    val action: String get() = raw.substringAfter(":")
    val data: String? get() = raw.substringAfter(":", "").takeIf { it.contains(":") }
}

// 使用示例
fun onTabSwitch(payload: TabSwitchPayload) {
    when (payload.action) {
        "refresh" -> refreshTab(payload.tabId)
        "navigate" -> navigateToDetail(payload.data ?: return)
        "sync" -> syncTabState(payload.tabId)
    }
}

我个人习惯用内联类包装这种「轻量级消息」。它不会产生额外的堆分配,性能上比普通类好很多。而且语义清晰,一看就知道这是个「Tab切换载荷」。

状态同步的核心机制

多Tab状态同步,我推荐用「共享ViewModel + 事件总线」的组合方案。注意,这里的事件总线不是EventBus库,而是用SharedFlow实现的轻量级事件通道。

// 共享的事件通道
object TabEventBus {
    private val _events = MutableSharedFlow<TabEvent>(
        replay = 0,
        extraBufferCapacity = 64
    )
    val events = _events.asSharedFlow()
    
    suspend fun send(event: TabEvent) {
        _events.emit(event)
    }
}

// Tab事件定义
sealed class TabEvent {
    data class DataChanged(val tabId: Int, val data: Any) : TabEvent()
    object UserLoggedIn : TabEvent()
    data class FavoriteUpdated(val itemId: String) : TabEvent()
}

// 在Tab中监听事件
class BaseTabFragment : Fragment() {
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        viewLifecycleOwner.lifecycleScope.launch {
            TabEventBus.events
                .flowWithLifecycle(viewLifecycleOwner.lifecycle, Lifecycle.State.STARTED)
                .collect { event ->
                    when (event) {
                        is TabEvent.DataChanged -> handleDataChanged(event)
                        is TabEvent.UserLoggedIn -> refreshUserInfo()
                        is TabEvent.FavoriteUpdated -> updateFavoriteStatus(event.itemId)
                    }
                }
        }
    }
}

为什么要用SharedFlow而不是LiveData?因为LiveData只能在主线程用,而且粘性事件处理起来很麻烦。SharedFlow天然支持背压,还能指定缓冲区大小,更适合这种「一对多」的事件分发场景。

实战:完整的Tab状态管理方案

下面是一个完整的例子。假设我们有三个Tab:首页、发现、我的。每个Tab都有自己的状态,但「我的」页面需要知道首页的收藏状态。

// 主Activity的ViewModel
class MainViewModel : ViewModel() {
    // 管理所有Tab的状态
    private val _tabStates = MutableStateFlow<Map<Int, TabState>>(emptyMap())
    val tabStates: StateFlow<Map<Int, TabState>> = _tabStates.asStateFlow()
    
    // 更新某个Tab的状态
    fun updateTabState(tabId: Int, state: TabState) {
        _tabStates.value = _tabStates.value + (tabId to state)
        
        // 如果是收藏操作,通知其他Tab
        if (state is TabState.Content && tabId == 0) {
            viewModelScope.launch {
                TabEventBus.send(TabEvent.FavoriteUpdated("some_id"))
            }
        }
    }
}

// 每个Tab Fragment
class HomeTabFragment : Fragment() {
    private val viewModel: HomeTabViewModel by viewModels()
    private val mainViewModel: MainViewModel by activityViewModels()
    
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)
        
        // 监听自己的状态
        viewModel.state.observe(viewLifecycleOwner) { state ->
            // 更新到主ViewModel
            mainViewModel.updateTabState(0, state)
            renderState(state)
        }
        
        // 监听跨Tab事件
        viewLifecycleOwner.lifecycleScope.launch {
            TabEventBus.events
                .flowWithLifecycle(viewLifecycleOwner.lifecycle)
                .collect { event ->
                    if (event is TabEvent.FavoriteUpdated) {
                        // 刷新当前Tab的收藏状态
                        viewModel.refreshFavoriteStatus(event.itemId)
                    }
                }
        }
    }
}

核心要点:每个Tab的ViewModel只管理自己的状态。跨Tab通信通过事件总线完成。主ViewModel只做「状态聚合」,不直接操作子Tab的内部逻辑。

避坑指南

我曾经犯过一个错误:在Tab的ViewModel里直接持有Activity的引用。结果用户频繁切换Tab,导致内存泄漏。后来我改用activityViewModels()获取共享ViewModel,才彻底解决这个问题。

注意:不要在Tab的ViewModel里保存Activity或Fragment的引用。用SavedStateHandle保存需要持久化的数据,用SharedFlow做跨Tab通信。

另一个坑是:Tab切换时状态丢失。比如用户从Tab A切到Tab B,再切回Tab A,发现之前加载的数据没了。解决方案是用StateFlow配合stateIn操作符,让状态在ViewModel层面保持存活。

// 使用stateIn保持状态
val state: StateFlow<TabState> = repository.fetchData()
    .map { result ->
        if (result.isSuccess) {
            TabState.Content(result.getOrDefault(emptyList()))
        } else {
            TabState.Error(result.exceptionOrNull()?.message ?: "错误")
        }
    }
    .stateIn(
        scope = viewModelScope,
        started = SharingStarted.WhileSubscribed(5000),
        initialValue = TabState.Loading
    )

这里WhileSubscribed(5000)的意思是:当最后一个订阅者取消订阅后,再等5秒才停止数据流。这样用户快速切换Tab时,状态不会丢失。

SVG:多Tab状态同步架构图

多Tab状态同步架构 Tab 1 (首页) HomeTabViewModel StateFlow<TabState> Tab 2 (发现) DiscoverTabViewModel StateFlow<TabState> Tab 3 (我的) ProfileTabViewModel StateFlow<TabState> MainViewModel (状态聚合) StateFlow<Map<Int, TabState>> TabEventBus (SharedFlow) 状态上报 事件通知

个人建议:如果你的App有3个以上的Tab,一定要用这种「状态聚合+事件总线」的方案。别想着用单例或者静态变量,那是在给自己挖坑。我见过太多项目因为状态管理混乱,最后不得不重构整个页面架构。

最后说一句:多Tab状态管理没有银弹。密封类帮你约束状态类型,内联类帮你减少对象开销,SharedFlow帮你做事件分发。把这些工具用好,你的代码会干净很多。

公众号:蓝海资料掘金营,微信deep3321