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?
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?
-
we add a list variable
val leagues: MutableList<League> = mutableListOf()
that holds all the leagues that are returned fromgetLeague(id: Int)
-
we add two live data variables that hold our league list
// for encapsulation
private val _leaguesMutableLiveData = MutableLiveData<MutableList>()
val leaguesLiveData: LiveData<MutableList> = _leaguesMutableLiveData -
here in the body of this function
getLeague(id: Int)
we add the returned league to the list by the following lines of codeval response = remoteRepository.getLeague(id)
response?.body()?.let { leagues.add(it) } -
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)