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.