Blog Infos
Author
Published
Topics
Author
Published

Jetpack Compose increases developer productivity in views, especially when it comes to RecyclerView and Adapter. We only need to

define a composable (LazyRow/LazyColumn),
set the items and components for each item,
… yeah, that’s all!
There are other composable such as Row and Column too. In this article, we’ll see some use cases of Row/Column, LazyRow/LazyColumn, and how to handle the scroll action inside them.

Disclaimer: All source code exposed here has been developed with artifact December 2022 release of Compose Bom. Please note that some source code may change with new versions.

Row/Column

Row /Column can be used to arrange some views with a specific orientation (horizontal or vertical). In other words, we can treat Row/Column like LinearLayout in XML. The difference is scroll can be set directly to Row/Column modifier using verticalScroll or horizontalScroll.

@Composable
fun PreviewScreen(
screenName: String,
modifier: Modifier = Modifier
) {
Surface(modifier = modifier.fillMaxSize()) {
Column(modifier = modifier.verticalScroll()) {
PreviewAnimation()
Text(
text = stringResource(id = R.string.preview_of_page, screenName),
style = MaterialTheme.typography.titleLarge
)
Text(
text = stringResource(id = R.string.preview_page_description, screenName),
style = MaterialTheme.typography.bodyLarge
)
}
}
}
LazyRow/LazyColumn

Compose provides LazyRow/LazyColumn to replace RecyclerView in XML. Use these components when we need to:

  1. Display items with unknown/large sizes, with items() API inside LazyRow/LazyColumn (LazyListScope).
  2. Only compose and lay out items that are visible in the component’s viewport (same principle as RecyclerView).
LazyColumn {
items(posts) { post ->
PostCard(post)
}
}
view raw LazyColumn.kt hosted with ❤ by GitHub
Nested Scroll with Nested Items

Let’s take an example: Instagram home screen. The screen is scrollable with infinite posts. There’s also a horizontal scroll in the story section.

Typically, first, we will add a Column to wrap the story and post section as one vertical scroll. Then, use LazyRow and LazyColumn for each section as below.

@Composable
fun HomeScreenWrong(modifier: Modifier = Modifier) {
val scrollState = rememberScrollState()
val context = LocalContext.current
Surface(modifier = modifier.fillMaxWidth()) {
Column(modifier = Modifier.verticalScroll(scrollState)) {
LazyRow(modifier = modifier.fillMaxWidth()) {
items(stories) { story ->
StoryThumbnail(story = story)
}
}
LazyColumn(modifier = modifier.fillMaxWidth()) {
items(posts) { post ->
PostCard(
post = safePost,
postCardListener = getPostCardListener(context),
modifier = Modifier.padding(vertical = 8.dp))
}
}
}
}
}
view raw HomeScreen.kt hosted with ❤ by GitHub

If we try to preview the results, an error from Android Studio will be shown, which prevents us from creating nested scrolls with the same orientation without fixed height/width. By default view system (XML), it’s similar to adding RecyclerView inside a NestedScrollView. It may hurt app performance since the recycler’s ability is omitted when it’s inside a nested scroll.

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.   at androidx.compose.foundation.CheckScrollableContainerConstraintsKt.checkScrollableContainerConstraints-K40F9xA(CheckScrollableContainerConstraints.kt:35)   at androidx.compose.foundation.lazy.LazyListKt$rememberLazyListMeasurePolicy$1$1.invoke-0kLqBqw(LazyList.kt:192)   at androidx.compose.foundation.lazy.LazyListKt$rememberLazyListMeasurePolicy$1$1.invoke(LazyList.kt:191)   at androidx.compose.foundation.lazy.layout.LazyLayoutKt$LazyLayout$1$2$1.invoke-0kLqBqw(LazyLayout.kt:71) .....

To achieve nested scroll, instead of creating a new LazyColumn inside a Column, we should directly wrap all the composable inside parent LazyColumn as below. The concept is similar to ConcatAdapter.

@Composable
fun HomeScreen(modifier: Modifier = Modifier) {
val scrollState = rememberScrollState()
val context = LocalContext.current
Surface(modifier = modifier.fillMaxWidth()) {
LazyColumn(modifier = Modifier.verticalScroll(scrollState)) {
// LazyRow still able to be use, since it has different orientation with parent LazyColumn
item {
LazyRow(modifier = modifier.fillMaxWidth()) {
items(getStories()) { story ->
StoryThumbnail(story = story)
}
}
}
items(getPosts()) { post ->
PostCard(
post = post,
postCardListener = getPostCardListener(context),
modifier = Modifier.padding(vertical = 8.dp))
}
}
}
}
view raw HomeScreen.kt hosted with ❤ by GitHub

And the result is… Tadaaa! The nested scrolls work both horizontally and vertically.

Last, since the use case is common, let’s create a general composable component to make it reusable. The complete source code of the results can be shown below.

import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
data class ChildLayout(
val contentType: String = "",
val content: @Composable (item: Any?) -> Unit = {},
val items: List<Any> = emptyList()
)
@Composable
fun VerticalScrollLayout(
modifier: Modifier = Modifier,
vararg childLayouts: ChildLayout
) {
LazyColumn(modifier = modifier.fillMaxSize()) {
childLayouts.forEach { child ->
if (child.items.isEmpty()) {
loadItem(child)
} else {
loadItems(child)
}
}
}
}
/**
* Use single item compose if no scroll or only horizontal scroll needed
*/
private fun LazyListScope.loadItem(childLayout: ChildLayout) {
item(contentType = childLayout.contentType) {
childLayout.content(null)
}
}
/**
* Use load multiple items to the lazy column when nested vertical scroll is needed
*/
private fun LazyListScope.loadItems(childLayout: ChildLayout) {
items(items = childLayout.items) { item ->
childLayout.content(item)
}
}
/**
* Compose items only if general item is successfully casted to defined class
*/
@Suppress("UNCHECKED_CAST")
@Composable
fun <T: Any> LoadItemAfterSafeCast(
generalItem: Any?,
composeWithSafeItem: @Composable (item: T) -> Unit
) {
(generalItem as? T)?.let { safeItem ->
composeWithSafeItem(safeItem)
}
}
import android.content.Context
import android.widget.Toast
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Divider
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.surfaceColorAtElevation
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.syntia.instagramcompose.domain.model.Post
import com.syntia.instagramcompose.domain.model.Story
import com.syntia.instagramcompose.domain.model.StoryType
import com.syntia.instagramcompose.ui.component.layout.ChildLayout
import com.syntia.instagramcompose.ui.component.layout.VerticalScrollLayout
import com.syntia.instagramcompose.ui.navigation.navigateToSingleProfile
import com.syntia.instagramcompose.ui.theme.InstagramComposeTheme
import com.syntia.instagramcompose.ui.view.component.post.card.PostCard
import com.syntia.instagramcompose.ui.view.component.post.card.listener.PostCardListener
import com.syntia.instagramcompose.ui.view.component.story.StoriesSection
import com.syntia.instagramcompose.util.LoadItemAfterSafeCast
/**
* Usages
*/
@Composable
fun HomeScreen(
modifier: Modifier = Modifier
) {
val context = LocalContext.current
Surface(modifier = modifier.fillMaxWidth()) {
VerticalScrollLayout(
modifier = Modifier,
ChildLayout(
contentType = HomeScreenContents.STORIES_SECTION,
content = {
StoriesSection(
stories = getStories(),
modifier = Modifier.padding(
start = 16.dp,
top = 16.dp,
bottom = 16.dp,
end = 0.dp
)
)
}
),
ChildLayout(
contentType = HomeScreenContents.DIVIDER,
content = {
Divider(
color = MaterialTheme.colorScheme.surfaceColorAtElevation(2.dp),
thickness = 1.dp)
}
),
ChildLayout(
contentType = HomeScreenContents.POST_CARDS,
items = getPosts(),
content = { item ->
LoadItemAfterSafeCast<Post>(item) { safePost ->
PostCard(
post = safePost,
postCardListener = getPostCardListener(context, navController),
modifier = Modifier.padding(vertical = 8.dp))
}
}
)
)
}
}
view raw HomeScreen.kt hosted with ❤ by GitHub

Happy coding.

This article was originally published on proandroiddev.com

Job Offers

Job Offers

There are currently no vacancies.

OUR VIDEO RECOMMENDATION

,

Jetpack Compose: Drawing without pain and recomposition

This is a talk on recomposition in Jetpack Compose and the myths of too many calls it is followed by. I’ll briefly explain the reasons behind recompositions and why they are not as problematic as…
Watch Video

Jetpack Compose: Drawing without pain and recomposition

Vitalii Markus
Android Engineer
Flo Health Inc.

Jetpack Compose: Drawing without pain and recomposition

Vitalii Markus
Android Engineer
Flo Health Inc.

Jetpack Compose: Drawing without pain and recomposition

Vitalii Markus
Android Engineer
Flo Health Inc.

Jobs

YOU MAY BE INTERESTED IN

YOU MAY BE INTERESTED IN

blog
It’s one of the common UX across apps to provide swipe to dismiss so…
READ MORE
blog
In this part of our series on introducing Jetpack Compose into an existing project,…
READ MORE
blog
In the world of Jetpack Compose, where designing reusable and customizable UI components is…
READ MORE
blog

How to animate BottomSheet content using Jetpack Compose

Early this year I started a new pet project for listening to random radio…
READ MORE
Menu