Compose를 이용해서 앱을 만들어가는 중에
XML Layout 에서 사용하던 RecyclerView를 사용해야 하는 상황이 생겼다.
내 Local DB에 저장된 데이터를 불러와 데이터의 수만큼 아이템을 만들어야 됐다.
아래와 같은 화면을 만들기 위해...!
Compose에선 LazyColumn이란 함수를 이용하여 쉽게 RecyclerView를 대체 가능하다.
LazyColumn은 Jetpack Compose에서 제공하는 스크롤 가능한 리스트 구성 요소이다.
LazyColumn은 아이템이 스크롤에 표시될 때만 생성되기 때문에, 초기에 모든 아이템을 로드하지 않아도 된다.
즉 xml에서 RecyclerView를 사용하는 목적과 동일하다고 보면 된다.
먼저 list를 받아오고 LazyColumn에 넣어준다.
본인은 격자형 화면을 만들 것이므로 LazyVerticalGrid를 사용한다.
@Composable
fun MyDiaryList(diaryLists: List<ItemEntity>, navController: NavController) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier,
) {
items(diaryLists.size) { count ->
GridItem(diaryLists = diaryLists, count = count) { id ->
navController.navigate("detail/$id")
}
}
}
}
items 메서드는 각 항목의 인덱스(0부터 리스트 사이즈의 크기 -1 만큼)를 람다에 전달한다.
GridItem 함수는 아이템 리스트와 인덱스를 변수로 받는다.
@Composable
fun GridItem(
diaryLists: List<ItemEntity>,
count: Int,
onClicked: (id: Int) -> Unit
) {
Column(
modifier = Modifier.clickable { diaryLists[count].id?.let { onClicked(it) } },
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Card(
modifier = Modifier.padding(8.dp),
shape = RoundedCornerShape(8.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 5.dp),
) {
Box() {
Image(
painter = rememberImagePainter(data = if (diaryLists[count].imageUri != "null") diaryLists[count].imageUri else R.drawable.diary),
contentDescription = "MyDiaryImage",
modifier = Modifier.size(230.dp),
contentScale = if (diaryLists[count].imageUri != "null") ContentScale.Crop else ContentScale.Fit
)
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.TopEnd
) {
IconButton(onClick = {
mainViewModel.updateItem(
ItemEntity(
id = diaryLists[count].id,
title = diaryLists[count].title,
content = diaryLists[count].content,
imageUri = diaryLists[count].imageUri,
date = diaryLists[count].date,
like = !diaryLists[count].like
)
)
}) {
Icon(
imageVector = if (!diaryLists[count].like) Icons.Default.FavoriteBorder else Icons.Default.Favorite,
contentDescription = "favorite",
tint = Color.Red
)
}
}
}
}
Text(
text = diaryLists[count].title,
color = Color.Black,
modifier = Modifier.padding(8.dp),
textAlign = TextAlign.Center
)
}
}
코드가 좀 길어서 보기 불편하지만 결과적으로 받아온 리스트의 값을 사용할 때마다 diaryList[count]로 값을 불러와서 사용하는 걸 볼 수 있다.
항상 인덱스를 같이 넘겨주고 리스트의 값을 불러올 때 인덱스를 사용해서 불러오는 게 번거롭게 느껴진다.
그래서 이렇게 멍청하게 사용할리가 없다!! 는 생각에 찾아보니
import androidx.compose.foundation.lazy.grid.items
를 import하면 된다.
기존의 items 함수는
fun items(
count: Int,
key: ((index: Int) -> Any)? = null,
span: (LazyGridItemSpanScope.(index: Int) -> GridItemSpan)? = null,
contentType: (index: Int) -> Any? = { null },
itemContent: @Composable LazyGridItemScope.(index: Int) -> Unit
)
로 고차함수를 이용해 index를 반환하는 함수였지만
새로 import한 items 함수는 제네릭 인라인 함수를 사용한다.
inline fun <T> LazyGridScope.items(
items: List<T>,
noinline key: ((item: T) -> Any)? = null,
noinline span: (LazyGridItemSpanScope.(item: T) -> GridItemSpan)? = null,
noinline contentType: (item: T) -> Any? = { null },
crossinline itemContent: @Composable LazyGridItemScope.(item: T) -> Unit
) = items(
count = items.size,
key = if (key != null) { index: Int -> key(items[index]) } else null,
span = if (span != null) { { span(items[it]) } } else null,
contentType = { index: Int -> contentType(items[index]) }
) {
itemContent(items[it])
}
items 함수 내에서 인라인 items 함수를 호출하며, 이 내부 items 함수는 그리드의 아이템 개수(count)를 설정하고, 별다른 기능이 정의되지 않았으므로 기본값을 사용한다. (key, span 및 contentType은 기본값으로 설정됨)
itemContent(items[it])가 호출되면서, it는 그리드 아이템의 인덱스를 나타낸다. 이제 전달된 아이템(item)을 사용하여 MyDiaryList 함수의 람다로 돌아가서, 이를 GridItem 함수에 전달한다.
결과적으로 list의 인덱스를 반환했던 전 코드와 달리 item 자체를 람다로 받아오기 때문에 아래와 같이 코드를 수정할 수 있다.
@Composable
fun MyDiaryList(diaryLists: List<ItemEntity>, navController: NavController) {
LazyVerticalGrid(
columns = GridCells.Fixed(2),
modifier = Modifier,
) {
items(diaryLists) { item ->
GridItem(item) { id ->
navController.navigate("detail/$id")
}
}
}
}
@Composable
fun GridItem(
item: ItemEntity,
onClicked: (id: Int) -> Unit
) {
Column(
modifier = Modifier.clickable { item.id?.let { onClicked(it) } },
verticalArrangement = Arrangement.Center,
horizontalAlignment = Alignment.CenterHorizontally
) {
Card(
modifier = Modifier.padding(8.dp),
shape = RoundedCornerShape(8.dp),
elevation = CardDefaults.cardElevation(defaultElevation = 5.dp),
) {
Box() {
Image(
painter = rememberImagePainter(data = if (item.imageUri != "null") item.imageUri else R.drawable.diary),
contentDescription = "MyDiaryImage",
modifier = Modifier.size(230.dp),
contentScale = if (item.imageUri != "null") ContentScale.Crop else ContentScale.Fit
)
Box(
modifier = Modifier.fillMaxSize(),
contentAlignment = Alignment.TopEnd
) {
IconButton(onClick = {
mainViewModel.updateItem(
ItemEntity(
id = item.id,
title = item.title,
content = item.content,
imageUri = item.imageUri,
date = item.date,
like = !item.like
)
)
}) {
Icon(
imageVector = if (!item.like) Icons.Default.FavoriteBorder else Icons.Default.Favorite,
contentDescription = "favorite",
tint = Color.Red
)
}
}
}
}
Text(
text = item.title,
color = Color.Black,
modifier = Modifier.padding(8.dp),
textAlign = TextAlign.Center
)
}
}
리스트에 인덱스를 넣고 값을 불러오는 전과 달리 아이템 자체를 사용하여 값을 불러올 수 있게 되었다.