36

I've read through similar topics but I couldn't find satisfactory result:

My use-case is: to create a comments' list (hundreds of items) with possibility to show replies to each comment (hundreds of items for each item).

Currently it's not possible to do a nested LazyColumn inside another LazyColumn because Compose will throw an exception:

java.lang.IllegalStateException: Vertically scrollable component was measured with an infinity maximum height constraints, which is disallowed. One of the common reasons is nesting layouts like LazyColumn and Column(Modifier.verticalScroll()). If you want to add a header before the list of items please add a header as a separate item() before the main items() inside the LazyColumn scope. There are could be other reasons for this to happen: your ComposeView was added into a LinearLayout with some weight, you applied Modifier.wrapContentSize(unbounded = true) or wrote a custom layout. Please try to remove the source of infinite constraints in the hierarchy above the scrolling container.

The solutions provided by links above (and others that came to my mind) are:

  • Using fixed height for internal LazyColumn - I cannot use it as each item can have different heights (for example: single vs multiline comment).
  • Using normal Columns (not lazy) inside LazyColumn - performance-wise it's inferior to lazy ones, when using Android Studio's Profiler and list of 500 elements, normal Column would use 350MB of RAM in my app comparing to 220-240MB using lazy Composables. So it will not recycle properly.
  • Using FlowColumn from Accompanist - I don't see any performance difference between this one and normal Column so see above.
  • Flatten the list's data source (show both comments and replies as "main" comments and only make UI changes to distinguish between them) - this is what I was currently using but when I was adding more complexity to this feature it prevents some of new feature requests to be implemented.
  • Disable internal LazyColumn's scrolling using newly added in Compose 1.2.0 userScrollEnabled parameter - unfortunately it throws the same error and it's an intended behaviour (see here).
  • Using other ways to block scrolling (also to block it programatically) - same error.
  • Using other LazyColumn's .height() parameters like wrapContentHeight() or using IntrinsicSize.Min - same error.

Any other ideas how to solve this? Especially considering that's doable to nest lazy components in Apple's SwiftUI without constraining heights.

adek111
  • 1,026
  • 2
  • 14
  • 29
  • 4
    Check out [this](https://www.youtube.com/watch?v=1ANt65eoNhQ&t=899s) youtube video about the topic as well as [this answer](https://stackoverflow.com/a/68995732/3585796) for a basic example - shortly you can place your subcomments in a separate `item`/`items`. Also [this answer](https://stackoverflow.com/a/71709816/3585796) may be useful for building a dynamic items tree. – Phil Dukhov May 17 '22 at 10:53
  • Thanks @Pylyp Dukhov for your input, it was helpful, the main difference between my implementation and your suggestions was using `forEachIndexed` with manual control of `item` and `items` instead of `itemsIndexed` that I was using. However, it breaks pagination implemented like [here](https://www.youtube.com/watch?v=Z4FnYeYR_fo) as `itemsIndexed` has different (user-aware) index value comparing to `forEachIndexed` one). At the end, it seems I managed to mix both "mindsets" to use `itemsIndexed` for comments and `forEach` for replies. – adek111 May 17 '22 at 13:07
  • 1
    you should use `key` parameter of `item`/`items` to specify a unique identifier for each item, also `itemContent` can improve performance if you specify different types depending on cell type (comment/reply) – Phil Dukhov May 18 '22 at 02:15
  • Sad that this still doesn't have an answer. I have comment trees in my application and am still forced to use `Column` for the inner branches. – dessalines Oct 02 '22 at 16:11
  • Does this answer your question? [How to build a tree using LazyColumn in Jetpack Compose?](https://stackoverflow.com/questions/71667299/how-to-build-a-tree-using-lazycolumn-in-jetpack-compose) – dessalines Oct 05 '22 at 18:11
  • This was answered by Phil here: https://stackoverflow.com/questions/71667299/how-to-build-a-tree-using-lazycolumn-in-jetpack-compose – dessalines Oct 05 '22 at 18:12

5 Answers5

4

I had a similar use case and I have a solution with a single LazyColumn that works quite well and performant for me, the idea is to treat your data as a large LazyColumn with different types of elements. Because comment replies are now separate list items you have to first flatten your data so that it's a large list or multiple lists. Now for sub-comments you just add some padding in front but otherwise they appear as separate lazy items. I also used a LazyVerticalGrid instead of LazyColumn because I've had to show a grid of gallery pictures at the end, you may not need that, but if you do, you have to use span option everywhere else as shown below.

You'll have something like this:

LazyVerticalGrid(
        modifier = Modifier
            .padding(6.dp),
        columns = GridCells.Fixed(3)
    ) {
        item(span = { GridItemSpan(3) }) {
            ComposableTitle()
        }
        items(
            items = flattenedCommentList,
            key = { it.commentId },
            span = { GridItemSpan(3) }) { comment ->
                ShowCommentComposable(comment) 
                //here subcomments will have extra padding in front
        }
        item(span = { GridItemSpan(3) }) {
            ComposableGalleryTitle()
        }
        items(items = imageGalleryList,
            key = { it.imageId }) { image ->
                ShowImageInsideGrid(image) //images in 3 column grid
    }
}
David Aleksanyan
  • 2,953
  • 4
  • 29
  • 39
0

I sloved this problem in this function

@Composable
fun NestedLazyList(
    modifier: Modifier = Modifier,
    outerState: LazyListState = rememberLazyListState(),
    innerState: LazyListState = rememberLazyListState(),
    outerContent: LazyListScope.() -> Unit,
    innerContent: LazyListScope.() -> Unit,
) {
    val scope = rememberCoroutineScope()
    val innerFirstVisibleItemIndex by remember {
        derivedStateOf {
            innerState.firstVisibleItemIndex
        }
    }
    SideEffect {
        if (outerState.layoutInfo.visibleItemsInfo.size == 2 && innerState.layoutInfo.totalItemsCount == 0)
            scope.launch { outerState.scrollToItem(outerState.layoutInfo.totalItemsCount) }
        println("outer ${outerState.layoutInfo.visibleItemsInfo.map { it.index }}")
        println("inner ${innerState.layoutInfo.visibleItemsInfo.map { it.index }}")
    }

    BoxWithConstraints(
        modifier = modifier
            .scrollable(
                state = rememberScrollableState {
                    scope.launch {
                        val toDown = it <= 0
                        if (toDown) {
                            if (outerState.run { firstVisibleItemIndex == layoutInfo.totalItemsCount - 1 }) {
                                Log.i(TAG, "NestedLazyList: down inner")
                                innerState.scrollBy(-it)
                            } else {
                                Log.i(TAG, "NestedLazyList: down outer")
                                outerState.scrollBy(-it)
                            }
                        } else {
                            if (innerFirstVisibleItemIndex == 0 && innerState.firstVisibleItemScrollOffset == 0) {
                                Log.i(TAG, "NestedLazyList: up outer")
                                outerState.scrollBy(-it)
                            } else {
                                Log.i(TAG, "NestedLazyList: up inner")
                                innerState.scrollBy(-it)
                            }
                        }
                    }
                    it
                },
                Orientation.Vertical,
            )
    ) {
        LazyColumn(
            userScrollEnabled = false,
            state = outerState,
            modifier = Modifier
                .heightIn(maxHeight)
        ) {
            outerContent()
            item {
                LazyColumn(
                    state = innerState,
                    userScrollEnabled = false,
                    modifier = Modifier
                        .height(maxHeight)

                ) {
                    innerContent()
                }
            }
        }

    }
}

All what I did is that:
At first I set the height of the inner lazyList to the height of the parent view using BoxWithConstraints, this lets the inner list fill the screen without distroying the lazy concept.
Then I controlled the scrolling by disable lazy scroll and make the parent scrollable to determine when the scroll affect the parent list and when the child should scroll.
bud this still has some bugs when the parent size changed , in my case I escaped by this SideEffect

ABADA S
  • 35
  • 4
0

For cases where

  • a flattened LazyColumn is undesirable, (for eg. the inner LazyColumn has a background that spans its items or something),
  • and where that inner list is best represented by a LazyColumn (for eg. because you want item animations out of the box)

a workaround is to specify a fixed height for the inner LazyColumn, if it's possible to know it. Either determine the desired height using a subcompose layout or similar, or if your items have fixes sizes, compute the total height manually.

@Composable
fun MyScreen() {
    LazyColumn {
        item {
            // ...
        }
        item {
            // We want to animate item placement which is only possible out of the box
            // with a LazyColumn. Nested scrollables are not allowed in Jetpack Compose
            // due to single layout pass requirements, however if we have a specified
            // height for the LazyCol it is allowed. So we must compute the expected
            // height ahead of time.
            val cardsHeightSum = CardHeight * cards.size
            val cardsPaddingSum = CardSpacing * (cards.size - 1)
            LazyColumn(
                verticalArrangement = Arrangement.spacedBy(8.dp),
                userScrollEnabled = false,
                modifier = Modifier
                    .background(Color.Blue, RoundedCornerShape(8.dp))
                    .height(cardsHeightSum + cardsPaddingSum)
            ) {
                items(
                    count = cards.size,
                    key = { index -> cards[index].id },
                ) { index ->
                    MyCard(
                        model = cards[index],
                        modifier = Modifier.animateItemPlacement()
                    )
                }
            }
        }
    }
}
Tom
  • 6,946
  • 2
  • 47
  • 63
-1

you can use a customView thats implements a "RecyclerView" class and then put it on your compose fragment. I'll show you my solution, works pretty well for me.

compose view

customViewRecyclerView

enter image description here

Of course you haves to create the adapter and the view for each item of the list, but I think you can do it without my help, good luck! :D

  • Please add code and data as text ([using code formatting](//stackoverflow.com/editing-help#code)), not images. Images: A) don't allow us to copy-&-paste the code/errors/data for testing; B) don't permit searching based on the code/error/data contents; and [many more reasons](//meta.stackoverflow.com/a/285557). Images should only be used, in addition to text in code format, if having the image adds something significant that is not conveyed by just the text code/error/data. – Adrian Mole Apr 05 '23 at 12:53
  • It is really a bad practice to use the androidview inside the compose in such case that there is better solution like using for/foreach – Sepideh Vatankhah Aug 08 '23 at 22:24
-8

You can use rememberScrollState() for root column. Like this;

Column(
    modifier = Modifier
        .verticalScroll(rememberScrollState())) {
   
    LazyColumn {
        // your code
    }

    LazyRow {
        // your code
    }

}

Edited;

the above works incorrectly, this is correct

LazyColumn {
  
  item {
    LazyRow {
      // your code
    }
  }

  items(yourList) {
    // your code  
  }

}
talhafaki
  • 40
  • 6