Eu estava criando um LazyVerticalGrid simples contendo imagens recuperadas da API pública. Inicialmente, segui um guia que utilizava paginação, mas essa API não possui páginas, então decidi buscar 20 imagens por vez. Cada chamada à API recupera 20 strings, que são inseridas em AsyncImage. Usei o booleano fetchNextImages para buscar as próximas 20 strings e funciona, mas apenas se você não alternar as telas. Tela:
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.PaddingValues
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import com.example.fortests.components.DuckImage
import com.example.fortests.components.LoadingState
sealed interface DuckViewState {
object Loading : DuckViewState
data class GridDisplay(
val ducks: List<String> = emptyList()
) : DuckViewState
}
@Composable
fun SecondScreen(
viewModel: SecondViewModel = hiltViewModel(),
onDuckClicked: (str: String) -> Unit
) {
LaunchedEffect(key1 = viewModel, block = { viewModel.fetchInitialImages() })
val viewState by viewModel.viewState.collectAsState()
val scrollState = viewModel.scrollState.value
val fetchNextImages: Boolean by remember {
derivedStateOf {
val currentCharacterCount =
(viewState as? DuckViewState.GridDisplay)?.ducks?.size
?: return@derivedStateOf false
val lastDisplayedIndex = scrollState.layoutInfo.visibleItemsInfo.lastOrNull()?.index
?: return@derivedStateOf false
return@derivedStateOf lastDisplayedIndex+1 == currentCharacterCount
}
}
LaunchedEffect(key1 = fetchNextImages, block = {
if (fetchNextImages) viewModel.fetchNextImages()
})
when (val state = viewState) {
DuckViewState.Loading -> LoadingState()
is DuckViewState.GridDisplay -> {
Column {
LazyVerticalGrid(
state = scrollState,
modifier = Modifier.padding(bottom = 90.dp),
contentPadding = PaddingValues(all = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp),
horizontalArrangement = Arrangement.spacedBy(8.dp),
columns = GridCells.Fixed(2),
content = {
items(
items = state.ducks
) { duck ->
DuckImage(
onClick = { onDuckClicked(duck) },
imageUrl = "https://random-d.uk/api/${duck}.jpg"
)
}
})
}
}
}
}
Modelo de exibição:
import android.app.Application
import androidx.compose.foundation.lazy.grid.LazyGridState
import androidx.compose.runtime.mutableStateOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.example.fortests.duckrepo.DuckImagesRepo
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class SecondViewModel @Inject constructor(
application: Application, private val duckImagesRepo: DuckImagesRepo
) : ViewModel() {
private val _viewState = MutableStateFlow<DuckViewState>(DuckViewState.Loading)
val viewState: StateFlow<DuckViewState> = _viewState.asStateFlow()
val scrollState = mutableStateOf(LazyGridState())
private val fetchedDucks = mutableListOf<String>()
fun fetchInitialImages() = viewModelScope.launch {
if (fetchedDucks.isNotEmpty()) return@launch
val initialData = duckImagesRepo.fetchImages()
_viewState.update {
return@update DuckViewState.GridDisplay(ducks = initialData)
}
}
fun fetchNextImages() = viewModelScope.launch {
val data = duckImagesRepo.fetchNextData()
_viewState.update { currentState ->
when (currentState) {
is DuckViewState.GridDisplay -> {
val updatedDucks = currentState.ducks + data
DuckViewState.GridDisplay(ducks = updatedDucks)
}
else -> currentState
}
}
}
}
Seria uma boa solução simplesmente salvar todas essas strings em uma lista?
Há vários problemas com seu código, provavelmente o mais proeminente sendo que você redefine a lista de modelos de visualização sempre que SecondScreen entra na composição (executando
fetchInitialImages()
). Isso acontece sempre que você alterna entre telas, mas também é acionado em alterações de configuração, como rotações de tela. A lista inteira é perdida e uma nova é criada. Essa provavelmente é a causa do seu problema principal.Mas acho que não vale a pena consertar isso. Em vez disso, você deve refatorar seu aplicativo para usar a biblioteca de paginação 1 e mover toda a lógica de carregamento de blocos para a camada de dados, onde ela pertence. Isso não é algo com que sua interface de usuário deva se preocupar. Você não precisa depender da API para suportar paginação; você pode implementá-la facilmente como um wrapper para qualquer API. Você só precisa criar um
PagingSource
:É isso.
Como os arquivos
fetchImages()
e do repositóriofetchNextData()
agora fazem parte do PagingSource, você não precisa mais expô-los no repositório. Em vez disso, exponha o próprio PagingSource:(Ou deixe o Hilt injetá-lo no repositório, ele não precisa ser inicializado preguiçosamente)
O modelo de visualização também pode ser limpo. Você não precisa mais
viewState
de andDuckViewState
, você também pode removerfetchInitialImages()
andfetchNextImages()
. Em vez disso, crie umPager
a partir do PagingSource do repositório e exponha-o como um fluxo:Observe que o fluxo não é convertido em um StateFlow; em vez disso
cachedIn
, é usado um método que faz algo semelhante, mas leva em consideração as especificidades do PagingData.E, finalmente, você pode remover a lógica de carregamento do seu composable:
O fluxo é coletado com
collectAsLazyPagingItems
, especialmente projetado para paginação de dados. Ele retorna umLazyPagingItems
objeto que pode ser consultado para saber o estado de carregamento atual e de onde você pode recuperar os dados. Ele carregará automaticamente novos dados quando necessário, e seu elemento de composição será recomposto quando os novos dados chegarem.Como os dados agora são mantidos pelo repositório (respectivamente DuckImagePagingSource), eles sobrevivem a tudo o que você faz na sua interface de usuário (por exemplo, alternar telas, reconfigurar dispositivos e assim por diante).
Este é apenas um exemplo básico de como você pode criar seu próprio PagingSource. Há diversas configurações que você pode usar para ajustá-lo às suas necessidades específicas, como a
getRefreshKey()
implementação em si ou os diversosPagingConfig()
parâmetros opcionais.1 Você precisa dos artefatos
androidx.paging:paging-runtime-ktx
eandroidx.paging:paging-compose
.