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

Android设备旋转时如何维持动态添加的Tab状态?

问题描述

应用背景

我正在开发一款采用MVVM设计模式的Android应用,应用包含多个Fragment,每个Fragment对应一个足球联赛的标题和图标,这些Tab会根据API调用结果动态添加。

API调用签名

@GET("/leagues")
suspend fun getLeague(
    @Query("id")
    leagueId: Int = Shared.LEAGUES_IDS[0],
    @Query("current")
    current: String = "true"
): Response<Leagues>

ViewModel核心代码

@HiltViewModel
class FootBallViewModel @Inject constructor(
    private val app: Application,
    private val remoteRepository: RemoteRepository,
    private val defaultLocalRepository: DefaultLocalRepository
): ViewModel() {
    var apiCounter = 0
    val leagues: MutableList<Leagues> = mutableListOf()
    private val _leaguesMutableLiveData = MutableLiveData<ResponseState<Leagues>>()
    val leaguesMutableLiveData: LiveData<ResponseState<Leagues>> = _leaguesMutableLiveData

    init {
        viewModelScope.launch(Dispatchers.IO) {
            if(Shared.isConnected) {
                Shared.LEAGUES_IDS.forEach { leagueId -> 
                    when(val responseState = getLeague(leagueId)) { // 调用联赛API
                        is ResponseState.Success -> {
                            if(Shared.isLiveMatches)
                                responseState.data?.body()?.response?.get(0)?.seasons?.get(0)?.year?.let { season -> 
                                    getLeagueMatches(
                                        leagueId,
                                        season,
                                        app.getString(R.string.live_matches)
                                    )
                                }
                            else
                                responseState.data?.body()?.response?.get(0)?.seasons?.get(0)?.year?.let { season -> 
                                    getLeagueMatches(
                                        leagueId,
                                        season,
                                        null
                                    )
                                }
                        }
                        is ResponseState.Loading -> {}
                        is ResponseState.Error -> {}
                    }
                }
            }
        }
    }

    suspend fun getLeague(id: Int): ResponseState<Response<Leagues>> = viewModelScope.async(Dispatchers.IO) {
        if (Shared.isConnected) {
            try {
                val response = remoteRepository.getLeague(id)
                response?.body()?.let { leagues.add(it) }
                _leaguesMutableLiveData.postValue(ResponseState.Success(response?.body()!!))
                apiCounter++
                Log.i("apiCounter", apiCounter.toString())
                return@async ResponseState.Success(response)
            } catch (exception: Exception) {
                _leaguesMutableLiveData.postValue(ResponseState.Error(app.getString(R.string.unknown_error)))
                handleLeaguesException(exception)
                return@async ResponseState.Error(app.getString(R.string.unknown_error))
            }
        }
        else {
            _leaguesMutableLiveData.postValue(ResponseState.Error(app.getString(R.string.unknown_error)))
            return@async ResponseState.Error(app.getString(R.string.unable_to_connect))
        }
    }.await()

    // 其他省略代码
}

动态添加Tab的代码

footBallViewModel.leaguesMutableLiveData.observe(viewLifecycleOwner) {
    TabItemBinding.inflate(layoutInflater).apply {
        this.league = it.data?.response?.get(0)?.league
        val tab = tabLayout.newTab().setCustomView(this.root)
        tabLayout.addTab(tab)
        val animation = AnimationUtils.loadAnimation(context, R.anim.slide_in)
        tab.customView?.startAnimation(animation)
    }
    Log.i("tabsCount", tabLayout.tabCount.toString())
}

遇到的问题

Tab初始添加正常,但设备旋转后,由于leaguesMutableLiveData仅保存最后一个联赛的数据,TabLayout只显示最后一个Tab。

尝试过让leaguesMutableLiveData存储MutableList<League>,但每次向列表添加新联赛时,观察者会遍历整个列表重复添加Tab,导致出现大量重复项。

需求:实现根据联赛列表动态添加Tab,且设备旋转后能维持Tab状态。


解决方案

1. 改造ViewModel的LiveData,存储完整联赛列表

把ViewModel中的LiveData改为存储整个列表的状态,用私有可变列表维护数据,对外发布不可变列表实例,确保每次数据更新都能触发观察者回调:

@HiltViewModel
class FootBallViewModel @Inject constructor(
    private val app: Application,
    private val remoteRepository: RemoteRepository,
    private val defaultLocalRepository: DefaultLocalRepository
): ViewModel() {
    // 私有可变列表维护所有联赛数据
    private val _leaguesList = mutableListOf<Leagues>()
    // 改为存储列表的ResponseState
    private val _leaguesLiveData = MutableLiveData<ResponseState<List<Leagues>>>()
    val leaguesLiveData: LiveData<ResponseState<List<Leagues>>> = _leaguesLiveData

    init {
        viewModelScope.launch(Dispatchers.IO) {
            if(Shared.isConnected) {
                // 发布加载状态(可选)
                _leaguesLiveData.postValue(ResponseState.Loading)
                Shared.LEAGUES_IDS.forEach { leagueId -> 
                    when(val responseState = getLeague(leagueId)) {
                        is ResponseState.Success -> {
                            responseState.data?.body()?.let { league ->
                                _leaguesList.add(league)
                                // 发布不可变列表,触发观察者更新
                                _leaguesLiveData.postValue(ResponseState.Success(_leaguesList.toList()))
                                // 处理赛事逻辑...
                            }
                        }
                        is ResponseState.Error -> {
                            _leaguesLiveData.postValue(ResponseState.Error(app.getString(R.string.unknown_error)))
                        }
                        else -> {}
                    }
                }
            } else {
                _leaguesLiveData.postValue(ResponseState.Error(app.getString(R.string.unable_to_connect)))
            }
        }
    }

    suspend fun getLeague(id: Int): ResponseState<Response<Leagues>> = viewModelScope.async(Dispatchers.IO) {
        return@async try {
            val response = remoteRepository.getLeague(id)
            ResponseState.Success(response)
        } catch (exception: Exception) {
            handleLeaguesException(exception)
            ResponseState.Error(app.getString(R.string.unknown_error))
        }
    }.await()

    // 其他省略代码
}

2. 修改观察者逻辑,避免重复添加Tab

在Fragment/Activity中,收到列表更新时先清空已有Tab,再重新添加所有项,确保每次都是完整且唯一的Tab集合:

footBallViewModel.leaguesLiveData.observe(viewLifecycleOwner) { state ->
    when(state) {
        is ResponseState.Success -> {
            // 清空已有Tab,避免重复
            tabLayout.removeAllTabs()
            state.data.forEach { leagueData ->
                TabItemBinding.inflate(layoutInflater).apply {
                    this.league = leagueData.response?.get(0)?.league
                    val tab = tabLayout.newTab().setCustomView(this.root)
                    tabLayout.addTab(tab)
                    val animation = AnimationUtils.loadAnimation(context, R.anim.slide_in)
                    tab.customView?.startAnimation(animation)
                }
            }
        }
        is ResponseState.Error -> {
            // 处理错误提示
            Toast.makeText(context, state.message, Toast.LENGTH_SHORT).show()
        }
        is ResponseState.Loading -> {
            // 可选:显示加载进度
        }
    }
}

3. 额外优化:避免旋转时重复请求API

ViewModel的生命周期独立于Activity/Fragment,设备旋转时不会重新创建,因此init块中的API请求只会执行一次。如果需要根据网络状态重新加载,可以在onViewCreated中判断列表是否为空,再触发加载逻辑(通常无需额外操作)。


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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.02 01:17:22