Jetpack Compose - Lazy loading of data in Accompanist HorizontalPager

Viewed 63

The problem is extremely simple and I don't know why anyone hasn't asked it before.

I have to query loads of data from database and then show that in HorizontalPager one by one. If I load all of the data at once, before composing HorizontalPager, it takes too much time to load and user has to keep seeing CircularProgressIndicator for too long.

I want to load data of only that page of HorizontalPager that is visible to user. So it should go like this:

  • At index = 0, load data of page 0 only, and while loading, show CircularProgressIndicator
  • At index = 1, load data of page 1 only, and while loading, show CircularProgressIndicator

and so on.

How can I achieve that?

Here is what I've written that doesn't work properly:

val map by viewModel.statementWithTransMap.collectAsStateWithLifecycle()

HorizontalPager(
    modifier = Modifier.fillMaxSize(),
    state = horizontalPagerState,
    count = statementIds.size
) { index ->

    LaunchedEffect(Unit) {
        val id = statementIds[index]
        viewModel.loadStatementByStatementId(index, id)
    }

    val item = map[index]

    if (item == null) {
        Box(modifier = Modifier.fillMaxSize()) {
            CircularProgressIndicator(
                modifier = Modifier.align(Alignment.Center)
            )
        }
        return@HorizontalPager
    }

    Page(item)
}

And my relevant code in view model is:

fun loadStatementByStatementId(index: Int, statementId: Int) {

    viewModelScope.launch {
        getStatementsWithTranslationByStatementIdUseCase(statementId).collect {

            loadedStatements[index] = it
            _statementWithTransMap.value = loadedStatements
        }
    }
}
1 Answers

I would suggest using androidx paging, but if you don't want to, something like this should work. For simplicity, I put it all inside the compose code, but it would be better to handle the loading inside ViewModel or something like that.

@Composable fun MyPager(db: Database) {
    val loadedData = remember {
        mutableStateMapOf<Int, Item>()
    }
    HorizontalPager(count = 100) { index ->
        LaunchedEffect(Unit) {
            if (loadedData[index] == null) {
                loadedData[index] = db.getItem(index)
            }
        }

        val item = loadedData[index]
        if (item == null) {
            CircularProgressIndicator()
        } else {
            ItemContent(item)
        }
    }
}

Edit based on the code in question: Ok, assuming that loadedStatements is a MutableMap and _statementWithTransMap is a StateFlow, the problem is here I think:

loadedStatements[index] = it
_statementWithTransMap.value = loadedStatements

You update the mutable map and set it to the StateFlow, but the same map was already the value of that StateFlow and it doesn't emit when new value is the same (it doesn't care about the content of the map, the object is same). You will have to create a new map, something like this:

loadedStatements = loadedStatements.toMutableMap()
loadedStatements[index] = it
_statementWithTransMap.value = loadedStatements
Related