rishi kumar
rishi kumar

Reputation: 111

Android jetpack compose pagination : Pagination not working with staggered layout jetpack compose

Pagination given by android (https://developer.android.com/topic/libraries/architecture/paging/v3-overview) is working fine with Column,Row,lazy column, lazy rows. Problem occurs when I am trying to achieve pagination in staggered layout (Answer How to achieve a staggered grid layout using Jetpack compose? was very helpful).

Problem statement is there is no further network call when I scroll towards bottom of the list. As per docs there is no method for making paginated calls for next items it just automatically does as soon as we make input list as itemList.collectAsLazyPagingItems() and pass it to lazycolumn/lazyrow. But its not automatically happening for above mentioned staggered layout.

One solution I am testing is there is manual observation on the index of visible items and if they are near the end of the list and manually calling the network request. (see start code for this code lab ( https://developer.android.com/codelabs/android-paging#0 )

Staggered layout somehow in an essence of implementation of creating and using multiple COLUMNS inside and distributing items to them columns. Challenge here is how do we know we are approaching towards the end of the list.

Code for staggered layout is something like this (tbh i don't completly understand how this works)

@Composable
private fun CustomStaggeredVerticalGrid(
  // on below line we are specifying
  // parameters as modifier, num of columns
    modifier: Modifier = Modifier,
    numColumns: Int = 2,
    content: @Composable () -> Unit
) {
// inside this grid we are creating
// a layout on below line.
Layout(
    // on below line we are specifying
    // content for our layout.
    content = content,
    // on below line we are adding modifier.
    modifier = modifier
) { measurable, constraints ->
    // on below line we are creating a variable for our column width.
    val columnWidth = (constraints.maxWidth / numColumns)

    // on the below line we are creating and initializing our items 
    constraint widget.
    val itemConstraints = constraints.copy(maxWidth = columnWidth)

    // on below line we are creating and initializing our column height
    val columnHeights = IntArray(numColumns) { 0 }

    // on below line we are creating and initializing placebles
    val placeables = measurable.map { measurable ->
        // inside placeble we are creating
        // variables as column and placebles.
        val column = testColumn(columnHeights)
        val placeable = measurable.measure(itemConstraints)

        // on below line we are increasing our column height/
        columnHeights[column] += placeable.height
        placeable
    }

    // on below line we are creating a variable for
    // our height and specifying height for it.
    val height =
        columnHeights.maxOrNull()?.coerceIn(constraints.minHeight, 
    constraints.maxHeight)
            ?: constraints.minHeight

    // on below line we are specifying height and width for our layout.
    layout(
        width = constraints.maxWidth,
        height = height
    ) {
        // on below line we are creating a variable for column y pointer.
        val columnYPointers = IntArray(numColumns) { 0 }

        // on below line we are setting x and y for each placeable item
        placeables.forEach { placeable ->
            // on below line we are calling test
            // column method to get our column index
            val column = testColumn(columnYPointers)

            placeable.place(
                x = columnWidth * column,
                y = columnYPointers[column]
            )

            // on below line we are setting
            // column y pointer and incrementing it.
            columnYPointers[column] += placeable.height
        }
    }
}

}

Calling above code as below

Column(
    // for this column we are adding a
    // modifier to it to fill max size.
    modifier = Modifier
        .fillMaxSize()
        .verticalScroll(rememberScrollState())
        .then(layoutModifier)
) {
    // on below line we are creating a column
    // for each item of our staggered grid.
    CustomStaggeredVerticalGrid(
        // on below line we are specifying
        // number of columns for our grid view.
        numColumns = numColumns,
    ) {
        // inside staggered grid view we are
        // adding images for each item of grid.
        itemList.forEachIndexed { index,  singleItem ->
            // on below line inside our grid
            // item we are adding card.
            SomesingleItemCompose(singleItem , singleItemModifier ,index) // this one single grid item Ui as per requirement
        }
    }
}

Upvotes: 1

Views: 1383

Answers (1)

rishi kumar
rishi kumar

Reputation: 111

As I said above , I was testing paginated data loading in compose staggered layout. It working. Trick was to use and tweak a little 'advance-pagiantion-start' codelab code (manual pagination data handling) and move it to compose. (there is still no way to use pagination library yet)

Solution : https://github.com/rishikumr/stackoverflow_code_sharing/tree/main/staggered-layout-compose-with_manual_pagination

Working video : https://drive.google.com/file/d/1IsKy0wzbyqI3dme3x7rzrZ6uHZZE9jrL/view?usp=sharing

How does it work :

  1. Make a network request , fed it to UI custom staggered layout ( How to achieve a staggered grid layout using Jetpack compose? )

  2. Listen to manual scroll and make a network request (for next page) when it scrolled to the end. (https://github.com/rishikumr/stackoverflow_code_sharing/blob/main/staggered-layout-compose-with_manual_pagination/app/src/main/java/com/example/android/codelabs/paging/ui/SearchRepositoriesActivity.kt)

    val itemList = remember { mutableStateListOf<Repo>() }
         var lastListSize by remember { mutableStateOf(0) }
    
         LaunchedEffect(Unit) {
             viewModel.fetchContent()
                 .collect { result ->
                     when (result) {
                         is RepoSearchResult.Success -> {
                             Log.d("GithubRepository", "result.data ${result.data.size}")
                             itemList.clear()
                             itemList.addAll(result.data)
                         }
                         is RepoSearchResult.Error -> {
                             Toast.makeText(
                                 this@SearchRepositoriesActivity,
                                 "\uD83D\uDE28 Wooops $result.message}",
                                 Toast.LENGTH_LONG
                             ).show()
                         }
                     }
                 }
         }
    
         val scrollState = rememberScrollState()
         val endReached = remember {
             derivedStateOf {
                 (scrollState.value == scrollState.maxValue) && (lastListSize != itemList.size) && (scrollState.isScrollInProgress)
             }
         }
    
         Column(Modifier.verticalScroll(scrollState), horizontalAlignment = Alignment.CenterHorizontally) {
             Box(modifier = Modifier.size(100.dp)) {
                 Text("Other Top composable")
             }
             StaggeredVerticalScreen(
                 itemList = itemList,
                 numColumns = 2,
                 layoutModifier = Modifier.padding(
                     start = 12.dp,
                     bottom = 12.dp
                 ),
                 singleItemModifier = Modifier.padding(
                     end = 12.dp,
                     top = 12.dp
                 )
             ) { singleGridItem, singleItemModifier, index ->
                 SingleArticleItem(singleGridItem  , index)
             }
    
             if (endReached.value) {
                 lastListSize = itemList.size
                 Log.d("SearchRepositoriesActivity", "End of scroll lazyItems.itemCount=${itemList.size}")
                 viewModel.accept(UiAction.FetchMore)
             }
         }
    
  3. you have to maintain the current page number and few other things. (https://github.com/rishikumr/stackoverflow_code_sharing/blob/main/staggered-layout-compose-with_manual_pagination/app/src/main/java/com/example/android/codelabs/paging/data/GithubRepository.kt)

A. Go through the code lab advance-pagination-start-code and you will understand how it all works. (I removed the part of code for on text change api calls because I didn't needed them) B. this currently inMemory caching only, I am working on Room database storing. i believe this should not be difficult.

If you have to have to use paging library then we need to embed view inside our compose

OPTION 2: Another option is to inflate view-staggered layout xml in parent compose using 'androidx.compose.ui:ui-viewbinding' library. I also tried with this and this works fantasticaly. Beaware all thigs need to be wrt view, adapter and all

 setContent {
        // get the view model
        val viewModel = ViewModelProvider(
            this, Injection.provideViewModelFactory(
                context = this,
                owner = this
            )
        )[SearchRepositoriesViewModel::class.java]

            AndroidViewBinding(ActivitySearchRepositoriesBinding::inflate) {
                val repoAdapter = ReposAdapter()
                val header = ReposLoadStateAdapter { repoAdapter.retry() }
                list.adapter = repoAdapter.withLoadStateHeaderAndFooter(
                    header = header,
                    footer = ReposLoadStateAdapter { repoAdapter.retry() }
                )

                val staggeredGridLayoutManager =
                    StaggeredGridLayoutManager(2, LinearLayoutManager.VERTICAL)
                this.list.apply {
                    layoutManager = staggeredGridLayoutManager
                    setHasFixedSize(true)
                    adapter = repoAdapter
                }

                lifecycleScope.launch {
                    viewModel.pagingDataFlow.collectLatest { movies ->
                        repoAdapter.submitData(movies)
                    }
                }
                retryButton.setOnClickListener { repoAdapter.retry() }

                lifecycleScope.launch {
                    viewModel.pagingDataFlow.collectLatest(repoAdapter::submitData)
                }

                lifecycleScope.launch {
                    repoAdapter.loadStateFlow.collect { loadState ->
                        // Show a retry header if there was an error refreshing, and items were previously
                        // cached OR default to the default prepend state
                        header.loadState = loadState.mediator
                            ?.refresh
                            ?.takeIf { it is LoadState.Error && repoAdapter.itemCount > 0 }
                            ?: loadState.prepend

                        val isListEmpty =
                            loadState.refresh is LoadState.NotLoading && repoAdapter.itemCount == 0
                        // show empty list
                        emptyList.isVisible = isListEmpty
                        // Only show the list if refresh succeeds, either from the the local db or the remote.
                        list.isVisible =
                            loadState.source.refresh is LoadState.NotLoading || loadState.mediator?.refresh is LoadState.NotLoading
                        // Show loading spinner during initial load or refresh.
                        progressBar.isVisible = loadState.mediator?.refresh is LoadState.Loading
                        // Show the retry state if initial load or refresh fails.
                        retryButton.isVisible =
                            loadState.mediator?.refresh is LoadState.Error && repoAdapter.itemCount == 0
                        // Toast on any error, regardless of whether it came from RemoteMediator or PagingSource
                        val errorState = loadState.source.append as? LoadState.Error
                            ?: loadState.source.prepend as? LoadState.Error
                            ?: loadState.append as? LoadState.Error
                            ?: loadState.prepend as? LoadState.Error
                        errorState?.let {
                            Toast.makeText(
                                this@SearchRepositoriesActivity,
                                "\uD83D\uDE28 Wooops ${it.error}",
                                Toast.LENGTH_LONG
                            ).show()
                        }
                    }
                }
        }
    }

I have both the sample (option 1 and option 2) working and tested. There is no flickering no abrupt behaviour so far. Its working good, with both options. let me know if there any thing I could help. (Also this is my first answer yeyy...!!)

Upvotes: 2

Related Questions