Diego Palomar
Diego Palomar

Reputation: 7061

Jetpack Compose: How to wrap list items inside a Card

I have a screen where I need to show a header and a list of items wrapped in a card view. The whole screen has to be scrollable (like shown in the image below).

enter image description here

I know how to do this with a scrollable Column but I want to be able to use a LazyColumn (because each list item will have its own ViewModel due to the complexity of the view and I thought LazyColumn will be more resources-efficient). For the header, I can use item and for the list, I can use items. Below is the code I tried:

@Composable
fun Screen(
  items: List<String>
) {
  Column(
    Modifier.fillMaxSize()
  ) {
    TopAppBar(title = { Text(text = "My Activity") })

    LazyColumn {

      // Header
      item {
        Text("Title", Modifier.padding(32.dp))
      }

      // I cannot use Box in this way here
      Box(Modifier.padding(32.dp)) {
        Card {
          items(items.size) {
            Text("Item $it")
          }
        }
      }
    }
  }
}

The problem with that code is that I cannot wrap the list items in a card view because Card is not a LazyListScope. Using LazyColumn, how can I wrap the list items in a Card?

Upvotes: 4

Views: 2552

Answers (5)

@Composable
fun TariffsScreen(viewModel: MobileTariffsViewModel, modifier: Modifier = Modifier) {

    val pullToRefreshState = rememberPullToRefreshState()
    val lazyListState = rememberLazyListState()

    Box(
        modifier = modifier.nestedScroll(pullToRefreshState.nestedScrollConnection)
    ) {
        LazyColumn(
            modifier = Modifier.fillMaxSize(),
            state = lazyListState
        ) {
            item {
                Text(text = "Title")
            }
            mobileTariffItemContent(viewModel = viewModel)
        }
    }
}

fun LazyListScope.mobileTariffItemContent(
viewModel: MobileTariffsViewModel
) {

    item {

        val mobileTariffCombine by viewModel.mobileTariffCombine.collectAsStateWithLifecycle(emptyList())

        ElevatedCard(
            modifier = Modifier
                .fillMaxWidth()
                .padding(start = 16.dp, end = 16.dp)
        ) {
            Column(
                modifier = Modifier
                    .background(Color.White)
            ) {
                mobileTariffCombine.forEachIndexed { index, tariff ->
                    MobileTariffItem(
                        tariff = tariff,
                        viewModel = viewModel
                    )
                }
            }
        }
    }
}

Upvotes: 0

Bob Castro
Bob Castro

Reputation: 29

Alternatively you can do this,

Column(
    Modifier.fillMaxSize()
) {
    TopAppBar(title = { Text(text = "My Activity") })

    LazyColumn {

        // Header
        item {
            Text("Title", Modifier.padding(32.dp))
        }

        // I cannot use Box in this way here
        item {
            Card {
                items.forEach { 
                    Text("Item $it")
                }
            }
        }
    }

}

Upvotes: 0

trinadh thatakula
trinadh thatakula

Reputation: 923

I was searching for the same but unfortunately couldn't find a solution so following a suggestion by @JanBína under another similar question I have customized each item card and got desired output and the code looks like this

@Composable
fun TransactionItem(
    transaction: Transaction,
    index: Int = 0,
    totalSize: Int = 1,
    onTransactionClicked: (Transaction) -> Unit
) {
    val transactionDateTime = Instant.fromEpochMilliseconds(transaction.date).toLocalDateTime(
        TimeZone.currentSystemDefault()
    )
    val cardMod = Modifier
        .padding(horizontal = 5.dp)
        .padding(bottom = if (index == totalSize - 1) 5.dp else 0.dp)
        .fillMaxWidth()
        .padding(horizontal = 5.dp)

    if (totalSize == 1) {
        cardMod.padding(vertical = 5.dp)
    } else when (index) {
        0 -> cardMod.padding(top = 5.dp)
        totalSize - 1 -> cardMod.padding(bottom = 10.dp)
        else -> cardMod.padding(vertical = 0.dp)
    }

    ElevatedCard(
        onClick = {
            onTransactionClicked(transaction)
        },
        modifier = cardMod,
        shape = when (totalSize) {
            1 -> RoundedCornerShape(15.dp)
            else -> when (index) {
                0 -> RoundedCornerShape(topStart = 15.dp, topEnd = 15.dp)
                totalSize - 1 -> RoundedCornerShape(bottomStart = 15.dp, bottomEnd = 15.dp)
                else -> RoundedCornerShape(0)
            }
        }
    ) {
       ///Item Content goes here
    }
}

this is not the final solution I am still searching for one where we can achieve the same using a single card, final result looks like this

like this

Upvotes: 0

Vvv998
Vvv998

Reputation: 1

As a workaround, you can emulate Card by drawing a custom Shape.

It would look something like this: screen_record

sealed class Item
class HeaderItem(...): Item()
class ContentItem(...) : Item()

...

val items: SnapshotStateList<Item> = mutableStateListOf()

...

LazyColumn(
        modifier = Modifier.fillMaxSize(),
    ) {
        itemsIndexed(
            items = state.items,
        ) { index, item ->

            val prevItem = state.items.getOrNull(index - 1)
            val nextItem = state.items.getOrNull(index + 1)

            Column {
                when (item) {
                    is HeaderItem -> Header(item)

                    is ContentItem -> {
                        Box(
                            modifier = Modifier
                                .heightIn(min = 48.dp)
                                .fillMaxWidth()
                                .clip(shape = getShape(prevItem, nextItem, 16.dp))
                                .background(Color.Green.copy(0.3F))
                        ) {
                            Item(item)
                        }
                    }
                }
            }
        }
fun getShape(prevItem: Item?, nextItem: Item?, corner: Dp): Shape {
    return if (prevItem is ContentItem && nextItem is ContentItem) {
        //FLAT
        RectangleShape
    } else if (prevItem !is ContentItem && nextItem !is ContentItem) {
        //ROUNDED_ALL
        RoundedCornerShape(corner)
    } else if (prevItem !is ContentItem) {
        //ROUNDED_TOP
        RoundedCornerShape(topStart = corner, topEnd = corner)
    } else {
        //ROUNDED_BOTTOM
        RoundedCornerShape(bottomStart = corner, bottomEnd = corner)
    }
}

Upvotes: 0

Blundell
Blundell

Reputation: 76534

You had it slightly mixed up, you should put the Card inside of the items call:

    LazyColumn {
      // Header
      item {
        Text("Title", Modifier.padding(32.dp))
      }
      
      items(items.size) {
          // You can use Box in this way here
          Box(Modifier.padding(32.dp)) {
            Card {
              Text("Item $it")
            }
          }
       }
    }

Upvotes: -1

Related Questions