3

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
        }
    }
}
Bugs Happen
  • 2,169
  • 4
  • 33
  • 59
  • I think your proposed solution isn't the best either, your user will see progress indicator on each page as well, even if only for a split second. The best solution would be to load some chunks of the data (something like 20 items at a time) using for example androidx paging library. With the paging library you will get that functionality out of the box. – Jan Bína Sep 06 '22 at 10:09
  • Seeing progress indicator is not the problem if it's for a split second. Do you have any solution for this problem other than Paging? – Bugs Happen Sep 06 '22 at 10:23
  • Well you can have a LaunchedEffect in each page and do the load or call some viewmodel method that will do the load there. – Jan Bína Sep 06 '22 at 10:50
  • Already tried that, it didn't work at my end. Can you please share an example code, as answer? – Bugs Happen Sep 06 '22 at 10:53

1 Answers1

1

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
Jan Bína
  • 3,447
  • 14
  • 16