Jetpack Compose — Building a RecyclerView with StickyHeader

Reasons behind Jetpack Compose

  • XML Layout files are static, which means
  • you had to define numerous variations of the same XML layout depending on your device size and its orientation.
  • Accessing the UI Elements is a pain, with the default way to access UI elements (findViewById) to have an impact on performance. As a result, several alternatives have been introduced to access directly the UI components, from Kotlin Synthetics to ViewBinding and DataBinding.
  • Allowing for decision making in UI Elements is also a pain. To add extra logic and functionalities to UI Elements you have to create BindingAdapters in separate Kotlin files. And for harder scenarios, like building a RecyclerView? Well in that case you had to build a complex Adapter with ViewHolders because you cannot fit logic inside your layout.

Classic RecyclerView

Stages of the project we are about to Build

PART A: Basic Project

  • [Demo 1] We will start with a ListView equivalent in Jetpack Compose
  • [Demo 2] We will then continue to its RecyclerView Equivalent
  • [Demo 3] Then we will put click listeners to our RecyclerView items

PART B: Improved Project

  • [Demo 4] We will create an improved layout where we will display data and images in a LazyColumn (RecyclerView) from a Data Set.
  • [Demo 5] We will then add Sticky Headers to our LazyColumn (RecyclerView)

PART A: Basic Project

Demo 1: “Column” — a ListView Equivalent

  1. At your onCreate method create a call to our composable function
setContent {
ComposeAppTheme {
Surface(color = MaterialTheme.colors.background) {
ScrollableColumnDemo()
}
}
}
@Composable
fun ScrollableColumnDemo(){
val scrollState = rememberScrollState()
Column(
modifier = Modifier
.verticalScroll(scrollState)
.fillMaxSize()
) {
for (i in 1..200){
Text(
text = "Person $i",
fontSize = 36.sp,
modifier = Modifier.padding(8.dp)
)
Divider(color = Color.Gray, thickness = 1.dp)
}
}
}
  • We create a Text followed by a Divider composable object, two hundred times inside a for-loop.
  • These composable components are nested inside a Column composable component, which is the equivalent of a vertical LinerLayout.
  • For this Column to be scrollable, we need to declare a verticalScroll(scrollState), which is the equivalent of attaching a LinearLayout to a ScrollView.

Demo 2: “LazyColumn” — a RecyclerView Equivalent

  1. At your onCreate method change your setContent call from ScrollableColumnDemo() to LazyColumnDemo().
setContent {
ComposeAppTheme {
Surface(color = MaterialTheme.colors.background) {
LazyColumnDemo()
}
}
}
@Composable
fun LazyColumnDemo(){
LazyColumn(){
items(200){
Text(
text = "Person ${it+1}",
fontSize = 36.sp,
modifier = Modifier.padding(8.dp)
)
Divider(color = Color.Gray, thickness = 1.dp)
}
}
}
  • how many times you loop for some fixed functionality (this is what we do in Demo1, Demo2, Demo3),
  • or a list of items (which you would normally do with data coming from some repository — we will use that in Demo4 and Demo5).

Demo 3: LazyColumn with Click Listener

  1. At your onCreate method create a call to our composable function
setContent {
ComposeAppTheme {
Surface(color = MaterialTheme.colors.background) {
LazyColumnClickableDemo{
Toast.makeText(
this, "Person $it", Toast.LENGTH_SHORT).show()
}
}
}
}
@Composable
fun LazyColumnClickableDemo(selectedPerson: (Int) -> Unit){
LazyColumn(){
items(200){
Surface(
modifier = Modifier
.clickable {selectedPerson(it+1)})
{
Text(
text = "Person ${it+1}",
fontSize = 36.sp,
modifier = Modifier.padding(8.dp)
)
Divider(color = Color.Gray, thickness = 1.dp)
}
}
}
}

PART B: Improved Project

Prerequisites

data class Person(
val id: Int,
val section: Int,
val name: String,
val imageUrl: String,
val landingPage: String
)
val persons = arrayListOf<Person>()
var section = 1
for (i in 1..200){
if (i % 15 == 0){
section++
}

persons.add(
Person(
id = i,
section = section,
name = "Ioannis Anifantakis",
imageUrl = "https://anifantakis.eu/wp-content/uploads/2021/05/ioannis-anifantakis-firebase-small.jpg",
landingPage = "https://anifantakis.eu"
)
)
}
implementation("io.coil-kt:coil-compose:1.3.2")

Demo 4: LazyColumn with improved UI and provided dataset

1. Define the Image Loader

@Composable
fun ImageLoader(imageUrl: String){
Image(
painter = rememberImagePainter(imageUrl),
contentDescription = null,
contentScale = ContentScale.Crop,
modifier = Modifier.size(120.dp)
)
}

2. Define the ListItem

@Composable
fun ListItem(person: Person, selectedPerson: (Person)->Unit){
Card(
modifier = Modifier
.padding(8.dp)
.fillMaxWidth()
.clickable {selectedPerson(person)},
elevation = 8.dp,
) {
Row{
ImageLoader(person.imageUrl)
Spacer(modifier = Modifier.width(8.dp))
Text(
person.name+' '+person.id,
style = MaterialTheme.typography.h5,
modifier = Modifier.padding(8.dp)
)
}
}
}

3. Populate ListItems for each ArrayList entry to our LazyColumn (RecyclerView)

@Composable
fun LazyColumnClickableAdvDemo(persons: List<Person>, selectedPerson: (Person)->Unit){
LazyColumn(){
items(
items = persons,
itemContent = {
ListItem(
person = it,
selectedPerson = selectedPerson
)
}
)
}
}
  • We pass a list of Persons (persons: List<Person>) to our composable method that contains the LazyColumn.
  • As we are passing a list of Persons to our items block (instead of iterating statically 200 times like before), we define the items to be the list of persons (items = persons).
  • Thus the it now does not represent the current iteration number of the loop, but the current Person instance of the provided PersonsList.
  • Our lambda parameter (selectedPerson: (Person)->Unit) instead of returning an integer, it will now return an instance of a Person object.

4. Add it to the setContent of our Activity

val persons = arrayListOf<Person>()
...
...
...
setContent {
ComposeAppTheme
Surface(color = MaterialTheme.colors.background) {

LazyColumnClickableAdvDemo(persons){
Toast.makeText(
this, "${it.name} ${it.id}",
Toast.LENGTH_SHORT
).show()
}
}
}
}

Demo 5: Adding Sticky Header to Demo4 code

@Composable
fun LazyColumnClickableAdvDemo(persons: List<Person>, selectedPerson: (Person)->Unit){
LazyColumn(){
items(
items = persons,
itemContent = {
ListItem(
person = it,
selectedPerson = selectedPerson
)
}
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyColumnClickableAdvDemo(persons: List<Person>, selectedPerson: (Person)->Unit){
val grouped = persons.groupBy{it.section}

LazyColumn(){
grouped.forEach { (section, sectionPersons) ->
stickyHeader {
Text(
text = "SECTION: $section",
color = Color.White,
modifier = Modifier
.background(color = Color.Black)
.padding(8.dp)
.fillMaxWidth()
)
}


items(
items = sectionPersons,
itemContent = {
ListItem(
person = it,
selectedPerson = selectedPerson
)
}
)
}
}
}

How do Sticky Headers work?

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store
Ioannis Anifantakis

Ioannis Anifantakis

MSc Computer Science. — Software engineer and programming instructor. Actively involved in Android Development and Deep Learning.