Making RecyclerViews with Jetpack Compose

May 17, 2021

We will be exploring how to create long lists in Jetpack Compose.

Before We Start

We will be creating an app that has a horizontal and vertical RecyclerView in Jetpack Compose. Since this article is focused around the creation of RecyclerViews, we won’t be discussing the parameters and smaller views created in-depth. More information as well as the code for the Post view can be found in our earlier blog post “Getting Started with Jetpack Compose”. The code from this article can be found at the bottom of this article.

App that displays data vertically and horizontally

We’ll need to set up our project to use Jetpack Compose (currently you can only use the Compose API with the latest Android Studio Preview). In the app level build.gradle we’ll need to enable compose and also declare its dependencies.

android {
    …

    buildFeatures {
        compose true
    }
    composeOptions {
        // This should be the same as your project’s kotlin version
        kotlinCompilerVersion "1.4.31"
        kotlinCompilerExtensionVersion "1.0.0-beta02"
    }

    …

}

dependencies {

    …

    implementation 'androidx.compose.ui:ui:1.0.0-beta02'

    // Tooling support (Previews, etc.)
    implementation 'androidx.compose.ui:ui-tooling:1.0.0-beta02'

    // Foundation (Border, Background, Box, Image, Scroll, shapes, animations, etc.)
    implementation 'androidx.compose.foundation:foundation:1.0.0-beta02'

    // Material Design
    implementation 'androidx.compose.material:material:1.0.0-beta02'

    // Material design icons
    implementation 'androidx.compose.material:material-icons-core:1.0.0-beta02'
    implementation 'androidx.compose.material:material-icons-extended:1.0.0-beta02'

    // Integration with activities
    implementation 'androidx.activity:activity-compose:1.3.0-alpha04'

    // Integration with ViewModels
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:1.0.0-alpha03'

    // Integration with observables
    implementation 'androidx.compose.runtime:runtime-livedata:1.0.0-beta02'
    
    …

}

Try building the project and make sure it succeeds. If there is an error about the compiler version, make sure your com.android.tools.build:gradle is up to date.

I’ve created a Spacing file that I use to define spacing within the examples. You should create a file and paste the enum class to that file so the code snippets below will work.

enum class Spacing(val size: Dp) {
    EXTRA_TIGHT(4.dp),
    TIGHT(8.dp),
    TIGHT_BASE(12.dp),
    BASE(16.dp),
    LOOSE(20.dp),
}

Models

The code for the models used in this app can be found here.

Product

data class Product(val name: String, val price: Double, @DrawableRes val image: Int)

PostData

data class PostData(
    @DrawableRes val avatar: Int,
    val username: String,
    val description: String,
    @DrawableRes val image: Int? = null
)

RecyclerViewSection

This sealed class is used to hold information about each section of the LazyColumn we will be looking at in this article.

sealed class RecyclerViewSection
data class Interstitial(val title: String, val description: String, val backgroundColor: Color) :
    RecyclerViewSection()

data class Carousel(val header: String, val products: List<Product>) : RecyclerViewSection()
data class Posts(val posts: List<PostData>) : RecyclerViewSection()

Dummy Data

You can find this code in the code samples provided.

Let’s Get Started

This list is made up of multiple smaller components. The Post component can be found from our earlier blog post titled “Getting Started with Jetpack Compose”.

Carousel

The carousel component named ProductCarousel is one of the more interesting views we will be making since it’s a horizontal scrolling RecyclerView. This view is made up of ProductCards.

Product Card

We’ll need to populate our ProductCarousel with views to show. Let’s go ahead and make a composable view called ProductCard with the following code.

@Composable
fun ProductCard(product: Product) {
    Card(elevation = Spacing.EXTRA_TIGHT.size) {
        Column(
            modifier = Modifier
                .padding(
                    horizontal = Spacing.TIGHT_BASE.size,
                    vertical = Spacing.TIGHT.size
                )
                .width(75.dp)
                .height(75.dp),
        ) {
            Image(
                painter = painterResource(
                    id = product.image
                ),
                contentDescription = product.name,
                modifier = Modifier
                    .fillMaxWidth()
                    .height(30.dp),
                alignment = Alignment.Center,
            )
            Text(
                text = product.name,
                modifier = Modifier
                    .padding(
                        top = Spacing.TIGHT.size
                    )
            )
            Text(
                text = "$ ${"%.2f".format(product.price)}",
                fontSize = 12.sp,
            )
        }
    }
}

This view is pretty simple! What we’ve done here is create a composable that takes a Product as a parameter. The composable is a Card that has a Column (think of this as a LinearLayout where orientation = vertical). The Column has an Image, a Text composable for the title and a Text composable for the price of the product. It populates those views using the Product model.

Using a preview function we can see what it looks like:

Preview of the ProductCard composable

Product Carousel

Here’s an interesting component. This carousel is a horizontal RecyclerView that displays the ProductCards. Let’s take a look at the code:

@Composable
fun ProductCarousel(products: List<Product>) {
    LazyRow(
        contentPadding = PaddingValues(
            horizontal = Spacing.BASE.size,
            vertical = Spacing.TIGHT.size,
        ),
        horizontalArrangement = Arrangement.spacedBy(Spacing.BASE.size),
    ) {
        items(products) { product ->
            ProductCard(
                product = product
            )
        }
    }
}

Normally, we would use a Row to lay composables out horizontally. However, using that to show a large number of composables can affect performance. A Row will lay out all its children, even if they aren’t visible. This is where a LazyRow comes in.

A LazyRow will only show the composables that are visible in the device’s viewport. Basically, it’s a horizontal RecyclerView. We’ll be taking a look at LazyColumn later, but it’s the same concept except vertical. We can’t pass composables in the content closure like we can with a Row. Instead, it accepts LazyListScope items that’s used to describe the contents. From the code snippet above, we see the use of the items() block to describe the item in the carousel. In this case, we’ve passed in a list of products to the items() extension. We take each product in the list and create a ProductCard from it within the block. We can pass parameters to the LazyRow like contentPadding to set the padding on the edges of the component and also horizontalArrangement to set the space in between each item in the LazyRow.

Similarly, by writing a preview function we can see what it looks like:

Preview of the ProductCarousel composable

Banner

This banner is used in the sample app code. The code can be found below:

@Composable
fun Banner(
    title: String,
    description: String,
    backgroundColor: Color
) {
    Column(
        modifier = Modifier
            .fillMaxWidth()
            .background(backgroundColor)
            .padding(horizontal = Spacing.BASE.size, vertical = Spacing.BASE.size),
        verticalArrangement = Arrangement.spacedBy(Spacing.TIGHT.size),
    ) {
        Header(text = title)
        Text(text = description)
    }
}

Header

The header is defined as:

enum class HeaderStyle(val fontSize: TextUnit) {
    H1(24.sp),
    H2(20.sp)
}

@Composable
fun Header(text: String, modifier: Modifier = Modifier, style: HeaderStyle = HeaderStyle.H1) {
    Text(
        text = text,
        fontSize = style.fontSize,
        modifier = modifier
    )
}

Putting it Together

Let’s explore the LazyColumn.

LazyColumn(verticalArrangement = Arrangement.spacedBy(Spacing.TIGHT.size)) {
    DummyData.getSections().forEach { section ->
        when (section) {
            is Interstitial -> {
                item {
                    Banner(
                        title = "Welcome back to AppLit!",
                        description = "Here's some new posts",
                        backgroundColor = Color.White,
                    )
                }
            }
            is Posts -> {
                items(section.posts) { post ->
                    Post(
                        post.avatar,
                        post.username,
                        post.description,
                        post.image
                    )
                }
            }
            is Carousel -> {
                item {
                    Header(
                        text = section.header,
                        modifier = Modifier.padding(
                            horizontal = Spacing.BASE.size
                        )
                    )
                    ProductCarousel(
                        products = section.products
                    )
                }
            }
        }
    }
}

The code snippet above may look complex at first, but it’s very easy to understand. In the code snippet above we defined a LazyColumn, which behaves the same as a LazyRow except for the fact that it lays its children out vertically. Also like a LazyRow, it takes a LazyListScope content block instead of a Composable one like a Column. We define each item in its block using the item() or items() block. We’ve also given a verticalArrangement to apply some space in between each item of the LazyColumn.

Interstitial

Let’s take a look at the item() when the section is an Interstitial

item {
    Banner(
        title = "Welcome back to AppLit!",
        description = "Here's some new posts",
        backgroundColor = Color.White,
    )
}

Here we define one item that contains a Banner composable. Extremely easy!

Posts

When the section is Posts we do the following:

items(section.posts) { post ->
    Post(
        post.avatar,
        post.username,
        post.description,
        post.image
    )
}

Just like the ProductCarousel we made earlier, we take advantage of the items() extension and pass in a list of posts. From each post we show a Post composable.

Carousel

This item is the most interesting because the item() block doesn’t just contain a singular item. The item() block can contain more than one composable if required.

item {
    Header(
        text = section.header,
        modifier = Modifier.padding(
            horizontal = Spacing.BASE.size
        )
    )
    ProductCarousel(
        products = section.products
    )
}

Here we are saying that this item should have a Header AND ProductCarousel. They go together and will be recycled together once it leaves the viewport. Additionally, the verticalArrangement we set earlier to the LazyColumn will not add padding in between the Header and ProductCarousel. It only adds it to each distinct item.

Closing

The LazyColumn and LazyRow are used to display large amounts of data in Jetpack Compose. They address performance issues a developer would face when using a Column or Row to display this data.

The app from this article can be found here.

Let's work together

Find out how we can help you grow.