읽기 전

  • 불필요한 코드나 잘못 작성된 내용에 대한 지적은 언제나 환영합니다.
  • 개인적으로 실습하면서 배운 점을 정리한 글입니다,

Jetpack Compose의 State 코드랩Jetpack Compose State 공식문서의 내용을 정리합니다. Compose에서의 변경 가능한 목록 구현을 위해 어떤 코드를 작성해야 하는지 다룹니다.

관련 자료

Android | Jetpack Compose State 복원, 목록 사용에서 이어지는 포스트입니다.

변경 가능한 목록으로 전환

이전 포스트에서 목록을 구현하여 체크박스 기능까지 지원하였다. 하지만, 삭제 기능은 현재 빈 람다를 반환하여 동작하지 않았다. 흔히 변경 가능한 리스트를 생각하면 ArrayList<T>mutableListOf()를 사용한다. 그러나, Compose에서는 이들을 사용한다고 해서 목록에 변동이 생길 경우 UI의 리컴포지션을 예약한다고 Compose에 알리지 않는다.

목록 변동을 Compose가 관찰할 수 있도록 MutableList 인스턴스를 만들어야 한다. 이를 위해 안드로이드에서는 toMutableStateList() 확장함수를 제공한다. 이를 통해 관찰 가능한 MutableList를 만들 수 있다.

Compose UI에서 상태를 관찰하기 위해선 State<T>로 매핑해야 한다고 설명했었다. 마찬가지로 mutableStateOf 함수는 MutableState<T> 타입의 객체를 반환하며 mutableStateListOftoMutableStateList 함수는 SnapshotStateList<T> 타입의 객체를 반환하며 해당 타입의 객체를 문서에서 Observable MutableList라 설명한다.

Task 리스트를 Observable MutableList로 전환

WellnessScreen.kt

@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
   Column(modifier = modifier) {
       StatefulCounter()

       val list = remember { getWellnessTasks().toMutableStateList() }
       WellnessTasksList(list = list, onCloseTask = { task -> list.remove(task) })
   }
}
// copy from WellnessTaskList.kt
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }

StatefulCounter 컴포저블이 상단에 위치하고 list 변수에 remember를 사용하여 MutableStateList 타입의 객체를 담았다.

주의할 점

mutableStateListOf API를 사용하여 목록을 만들 수 있으나 이를 사용하면 예기치 않은 리컴포지션이 발생할 수 있음을 숙지해야 한다. 잦은 리컴포지션의 발생은 UI 성능 저하를 유발할 수 있기 때문에 민감하게 받아들여야 한다.

만약 아래와 같이 MutableStateListremember하는 변수 선언을 한 뒤 해당 상태 객체에 데이터를 담는 코드를 작성했다고 해보자.

// Don't do this!
val list = remember { mutableStateListOf<WellnessTask>() }
list.addAll(getWellnessTasks())

이 경우 리스트에 아이템이 담길 때마다 상태가 변경된 것으로 인식하기 때문에 매번 리컴포지션이 발생한다. 현재는 초기 값을 설정할 수 있으므로 한꺼번에 구성해서 remember하는 것이 불필요한 리컴포지션을 줄일 수 있다.

// Do this instead. Don't need to copy
val list = remember {
mutableStateListOf<WellnessTask>().apply { addAll(getWellnessTasks()) }
}

보통 데이터를 연속적으로 요청하지 않고 일정 부분을 받기에 객체에 담은 뒤 remember하는 코드를 작성하면 이후에 해당 State 객체에 item을 담는 코드를 작성하지 않아 리컴포지션을 줄였다.

WellnessTaskList 컴포저블 함수 수정

이전 포스트에서 WellnessTaskList 컴포저블의 목록을 기본값으로 설정했었다. 이를 삭제하고 외부에서 받게끔 작성한 뒤 목록이 Screen 레벨로 호이스팅되었기에 WellnessTask 아이템 삭제를 담당하는 onCloseTask 람다 파라미터를 새롭게 정의한다. 그리고 onCloseTaskWellnessTaskItem에 전달하여 각 item의 우측 삭제 버튼 클릭 시 실행하도록 수정한다.

추가로 Mutable한 리스트를 Compose에 출력하는 동안 데이터가 변경될 때 추가 조치가 필요하다. 목록 출력을 위해 사용했던 items 함수에 key 매개변수를 추가로 작성해야 한다. 만약 key를 추가하지 않으면 기본값으로 목록에 있는 항목의 위치를 기준으로 키가 지정된다. 따라서, 데이터 변동은 위치를 변경함을 의미하고 이는 기억된 상태를 잃는다는 뜻이다. 항목의 위치를 기준올 설정된 기본 키 값에서 별도의 id 값으로 지정하면 이 문제를 해결할 수 있다.

앞서 Wellnesstask 데이터 클래스를 정의할 때 id 변수를 정의했었다. 해당 변수를 itemskey 변수로 넣어주자.

WellnessTaskList.kt - WellnessTaskList

@Composable
fun WellnessTasksList(
   list: List<WellnessTask>,
   onCloseTask: (WellnessTask) -> Unit,
   modifier: Modifier = Modifier
) {
   LazyColumn(modifier = modifier) {
       items(
           items = list,
           key = { task -> task.id }
       ) { task ->
           WellnessTaskItem(taskName = task.label, onClose = { onCloseTask(task) })
       }
   }
}

이로써 목록 컴포저블은 외부에서 아이템 삭제 시 실행할 람다 파라미터를 받고 각 아이템에 전달하는 역할을 수행한다.

onClose람다 함수를 Stateful 컴포저블로 옮기고 Stateless 컴포저블에서 호출

WellnessTaskItem.kt

@Composable
fun WellnessTaskItem(
   taskName: String, onClose: () -> Unit, modifier: Modifier = Modifier
) {
   var checkedState by rememberSaveable { mutableStateOf(false) }

   WellnessTaskItem(
       taskName = taskName,
       checked = checkedState,
       onCheckedChange = { newValue -> checkedState = newValue },
       onClose = onClose,
       modifier = modifier,
   )
}

WellnessScreen에서 삭제 시 동작할 람다 함수를 onClose로 전달하였고 목록 컴포저블을 거쳐 Stateless 컴포저블까지 전달 후 실행하게끔 작성하였다. 이제 목록에서 아이템을 삭제할 수 있다.

각 목록 아이템의 우측 X를 클릭하면 이벤트가 상태를 소유한 목록까지 이동하므로 목록에서 항목이 삭제되며 Compose는 화면을 재구성한다.

Android_Compmose_MutableList_001

그림에 따라 상태는 하단으로 전파되고 아이템을 삭제하겠다는 이벤트는 하단 컴포저블로부터 상단까지 상승하는 단방향 흐름을 볼 수 있다.

목록 사용 시 주의점

앞서 코드에서 remember를 사용하여 목록을 저장한 바 있다. 그렇다면 컴포지션 종료 후 복원 간 유지를 위해 rememberSaveable을 사용하여 보관할 수 있을까에 대한 의문이 든다. 바로 적용해보면 런타임 에러가 발생하며 이는 Android | Jetpack Compose State 복원, 목록 사용에 작성하였듯이 맞춤 Saver를 제공하지 않았기 때문이다. 그리고 rememberSaveableBundle객체에 데이터를 보관 후 복원 절차를 수행한다. Bundle 객체에는 가급적 텍스트, 부울 등 기본 자료형 저장을 권장하며 용량 제한은 50KB정도이다. 따라서, 직렬화/역직렬화가 필요한 데이터 구조나 대량의 데이터를 저장하는 데 rememberSaveable을 사용하는 것은 부적합하다.

따라서 목록 등 대량의 데이터 저장과 앱 상태 홀더의 역할을 수행하는 ViewModel을 도입해야 한다.

참고자료

  1. Google Codelab - Jetpack Compose의 상태 #11. 관찰 가능한 MutableList
  2. Android | Jetpack Compose State 복원, 목록 사용

+ Recent posts