android keep tabs state on device rotation

Issue

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)

Answered By – Ahmed Gamal

Answer Checked By – David Marino (FlutterFixes Volunteer)

Leave a Reply

Your email address will not be published. Required fields are marked *