Search code examples
androidmvvmtabsandroid-viewmodel

android keep tabs state on device rotation


I'm building Android app that has fragments and the app uses an MVVM design pattern. Each fragment represents a football league title and icon These tabs are added dynamically depending on API calls.

Here's the API call signature call

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

and here's the view model class that is used to call the API and store the data

    @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)) { //here's the call of league 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()

    ....
} //end of view model class

finally here's how I dynamically add the tabs

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())
    }

Problem definition: Now the tabs are added perfectly but the problem comes when I rotate the device, since the leaguesMutableLiveData only holds the last stored league item, the tab layout only gets that last tab from leaguesMutableLiveData

Other solutions I have tried: I have tried to make leaguesMutableLiveData hold MutableList<League> but that didn't work since the observer always adds extra tabs each time I add a league to the list because the observer loops the leagues list starting from the first element.

so the question in short words is how to add tabs depending on the leagues' list dynamically and keep the tabs state even on device notation? after device rotation

before device rotation


Solution

  • the idea is to post a list of leagues after all the calls of the leagues' API finish.

    in the below code snippet note that

    class FootBallViewModel @Inject constructor(
        private val app: Application,
        private val remoteRepository: RemoteRepository,
        private val defaultLocalRepository: DefaultLocalRepository
      ): ViewModel() {
        val leagues: MutableList<League> = mutableListOf()
        private val _leaguesMutableLiveData = MutableLiveData<MutableList<League>>()
        val leaguesLiveData: LiveData<MutableList<League>> = _leaguesMutableLiveData
        init {
            viewModelScope.launch(Dispatchers.IO) {
                if(Shared.isConnected) {
                    Shared.LEAGUES_IDS.forEach { leagueId ->
                        when(val responseState = getLeague(leagueId)) {
                            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 -> {}
                        }
                    }
                    _leaguesMutableLiveData.postValue(leagues)
                }
            }
        }
    
    suspend fun getLeague(id: Int): ResponseState<Response<League>?> = viewModelScope.async(Dispatchers.IO) {
            if (Shared.isConnected) {
                try {
                    val response = remoteRepository.getLeague(id)
                    response?.body()?.let { leagues.add(it) }
                    return@async ResponseState.Success(response)
                } catch (exception: Exception) {
                    handleLeaguesException(exception)
                    return@async ResponseState.Error(app.getString(R.string.unknown_error))
                }
            }
            else {
                return@async ResponseState.Error(app.getString(R.string.unable_to_connect))
            }
        }.await()
    
       .....
    } //end of view model class
    

    what happened?

    1. we add a list variable val leagues: MutableList<League> = mutableListOf() that holds all the leagues that are returned from getLeague(id: Int)

    2. we add two live data variables that hold our league list

      // for encapsulation private val _leaguesMutableLiveData = MutableLiveData<MutableList>()
      val leaguesLiveData: LiveData<MutableList> = _leaguesMutableLiveData

    3. here in the body of this function getLeague(id: Int) we add the returned league to the list by the following lines of code

      val response = remoteRepository.getLeague(id) response?.body()?.let { leagues.add(it) }

    4. finally, after looping the leagues id's

      Shared.LEAGUES_IDS.forEach { leagueId -> when(val responseState = getLeague(leagueId))

    .... } //end of for loop

    then we add the final list to our mutable live data object like this

    _leaguesMutableLiveData.postValue(leagues)