Jetpack Compose — Heterogenous list

In this story describes how to display an heterogenous list in Jetpack compose with an example.

The example that is explained in this tutorial will have a screen that shows the news Articles currently shown , news Articles already shown Today, news articles shown yesterday.

The Yesterday’s news articles will be displayed when an arrow displayed on section header is clicked.

The below are the steps to create a Heterogenous list in Jetpack compose:

Step1 : Prepare data for Heterogenous list

Create a sealed class that determines Header and child type of list item

sealed class OverViewListItem

data class OverViewChildItem(
val article: Article, val listType: ItemType, var canShowHistoryChild:Boolean = false
) : OverViewListItem()

data class OverViewHeaderItem(val listType: ItemType) : OverViewListItem()

In the above OverViewListItem contains two data classes to represent list item type whether its header or child.

There is an additional parameter to OverViewChildItem class whether to show child items or not for Yesterday’s articles.

Create an Enum class to determine item type whether its HEADER or CHILD

enum class ItemType {
IN_PROGRESS_HEADER,
HISTORY_TODAY_HEADER,
HISTORY_YESTERDAY_HEADER,
IN_PROGRESS_CHILD,
HISTORY_TODAY_CHILD,
HISTORY_YESTERDAY_CHILD
}

Creata an data class that represents Article:

@Parcelize
data class Article(
val articleName: String,
val description: String
) : Parcelable

Once the above steps are done create a class that provides data to be presented to List

class AdapterData  {

fun prepareAdapterData() : List<OverViewListItem>{
return fetchDataForInProgress(fetchArticleInfo()) + fetchDataForHistory(fetchArticleInfo()) + fetchDataForYesterdayHistory(fetchArticleInfo())
}


private fun fetchArticleInfo():List<Article>{
var articleList = mutableListOf<Article>()
repeat(2){
articleList.add(Article("Article${it+1}" ,"ArticleDescription${it+1}"))
}
return articleList
}

private fun fetchDataForInProgress(overList:List<Article>) : List<OverViewListItem> {
return arrayListOf(
OverViewHeaderItem(ItemType.IN_PROGRESS_HEADER)
) + overList.map {
OverViewChildItem(it, ItemType.IN_PROGRESS_CHILD)
}
}

private fun fetchDataForHistory(overList:List<Article>) : List<OverViewListItem> {
return arrayListOf(
OverViewHeaderItem(ItemType.HISTORY_TODAY_HEADER)
) + overList.map {
OverViewChildItem(it, ItemType.HISTORY_TODAY_CHILD)
}
}

private fun fetchDataForYesterdayHistory(overList:List<Article>) : List<OverViewListItem> {
return arrayListOf(
OverViewHeaderItem(ItemType.HISTORY_YESTERDAY_HEADER)
) + overList.map {
OverViewChildItem(it, ItemType.HISTORY_YESTERDAY_CHILD)
}
}
}

The above class provides data for all sections

Step2: Composable function to create Heterogenous list

We use LazyColumn to create list in Jetpack compose similar to RecyclerView.

There is no adapter needed in Jetpack Compose to map data to List.

The code to create a list is simpler in Jetpack compose.

@Composable fun DisplayOverViewScreen(
modifier: Modifier = Modifier, overViewList: List<OverViewListItem>
) {
val myList = remember { mutableStateListOf<OverViewListItem>() }
myList.swapList(overViewList)
LazyColumn(modifier = modifier) {
items(myList) { data ->
when (data) {
is OverViewHeaderItem -> {
HeaderItem(data, onArrowClick = { isExpanded ->
overViewList.map {
when (it) {
is OverViewChildItem -> {
if (it.listType == ItemType.HISTORY_YESTERDAY_CHILD) {
it.canShowHistoryChild = isExpanded
}
}
else -> {}
}
}
myList.swapList(overViewList)

})
}
is OverViewChildItem -> {
ChildListBasedOnListType(data)

}
}
}
}
}

In the above function, we observe the function takes the data created in the step1 .

The data is remembered to be recomposed when we need to refresh the list when some action is performed on any of the list item (Header/Child item)

val myList = remember { mutableStateListOf<OverViewListItem>() }
myList.swapList(overViewList)

where swapList is extension function that clears and creates new list during recomposition

import androidx.compose.runtime.snapshots.SnapshotStateList

fun <T> SnapshotStateList<T>.swapList(newList: List<T>){
clear()
addAll(newList)
}

The LazyColum composable is responsible to display list in Jetpack Compose. Each item of list we need to check whether the list is Header or child. If its Header we will make a call to a composable function to display Header Layout ,other we display ChildLayout.

HeaderItem Composable:

@Composable fun HeaderItem(overViewHeaderItem: OverViewHeaderItem,onArrowClick:(Boolean) -> Unit) {
var angle by remember {
mutableStateOf(0f)
}
var canDisplayChild by remember { mutableStateOf(false)
}
when (overViewHeaderItem.listType) {

ItemType.IN_PROGRESS_HEADER -> {
Column(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(color = Color(0xffdc8633)),
verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.Start
) {
Text(
text = stringResource(R.string.current), color = Color.White,
modifier = Modifier.padding(start = 16.dp)
)
}

}
ItemType.HISTORY_TODAY_HEADER -> {
Column(
modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(color = Color(0xffd7d7d7)),
verticalArrangement = Arrangement.Center, horizontalAlignment = Alignment.Start
) {
Text(
text = stringResource(R.string.history_label), color = Color.Black,
modifier = Modifier.padding(start = 16.dp)
)
}
}

else -> {

Row(modifier = Modifier
.fillMaxWidth()
.height(50.dp)
.background(color = Color(0xffd7d7d7)),
horizontalArrangement = Arrangement.Start, verticalAlignment = Alignment.CenterVertically) {
Text(
text = stringResource(R.string.history_yesterday), color = Color.Black,
modifier = Modifier.padding(start = 16.dp)
)
Spacer(Modifier.weight(1f))
Image(
painter = painterResource(id = R.drawable.ic_expand_more),
modifier = Modifier
.padding(end = 5.dp)
.rotate(angle)
.clickable {
angle = (angle + 180) % 360f
canDisplayChild = ! canDisplayChild
onArrowClick(canDisplayChild)
},
contentDescription = "Expandable Image"
)
}
}
}



}

The above function takes 2 arguments : one is the data to be displayed on Header and other is lamba to check whether arrow is clicked or not to expand or display child items for Yesterday’s articles. The lamba function’s result recomposes the list based on its click.

Composable function to display Child Items:

@Composable fun ChildListBasedOnListType(overViewChildItem: OverViewChildItem){

when(overViewChildItem.listType){
ItemType.HISTORY_YESTERDAY_CHILD -> {
println("overViewChildItem.canShowHistoryChild is ${overViewChildItem.canShowHistoryChild}")
if(overViewChildItem.canShowHistoryChild){
ChildListItem(overViewChildItem)
Divider(color = Color(0xffd7d7d7), thickness = 1.dp)
}
}
else -> {
ChildListItem(overViewChildItem)
Divider(color = Color(0xffd7d7d7), thickness = 1.dp)
}
}
}

@Composable fun ChildListItem(overViewChildItem: OverViewChildItem) {
ConstraintLayout(
constraintSet = getConstraints(),
modifier = Modifier
.fillMaxSize()
.height(60.dp)
.background(color = Color(0xffffffff))
) {
Text(
text = overViewChildItem.article.articleName, Modifier
.layoutId("articleLabel")
.padding(start = 16.dp)
.fillMaxWidth()
.wrapContentHeight()
)
Text(
text = overViewChildItem.article.description, Modifier
.layoutId("desc")
.padding(start = 16.dp)
.fillMaxWidth()
.wrapContentHeight()
)

Image(
painter = painterResource(id = R.drawable.ic_baseline_chevron_right_24),
modifier = Modifier.layoutId("detail"), contentDescription = "Detail Image"
)
}

}
@Composable fun getConstraints(): ConstraintSet {
return ConstraintSet {
val articleLabel = createRefFor("articleLabel")
val articleDesc = createRefFor("desc")
val detailImage = createRefFor("detail")
createVerticalChain(articleLabel, articleDesc, chainStyle = ChainStyle.Packed)
constrain(articleLabel) {
top.linkTo(parent.top)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
constrain(articleDesc) {
top.linkTo(articleLabel.bottom)
start.linkTo(parent.start)
end.linkTo(parent.end)
}
constrain(detailImage){
top.linkTo(parent.top)
end.linkTo(parent.end,5.dp)
bottom.linkTo(parent.bottom)
}
}
}

Following the above steps , Heterogenous list in Jetpack Compose can be implemented very easily.

--

--

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