[Compose Study] ViewModel에서의 상태

728x90
반응형

* 화면 또는 UI 상태는 화면에 표시할 내용을 나타낸다(예: 작업 목록). 이 상태는 애플리케이션 데이터를 포함하므로 대개 계층 구조의 다른 레이어에 연결된다.

UI 상태는 화면에 표시할 내용을 설명하지만 앱의 로직은 앱의 동작 방식을 설명하고 상태 변경에 반응해야 한다. 로직 유형에는 두 가지가 있다. (UI 동작 또는 UI 로직 비즈니스 로직)

  • UI 로직은 화면에 상태 변경을 표시하는 방법(예: 탐색 로직 또는 스낵바 표시)과 관련이 있다.
  • 비즈니스 로직은 상태 변경 시(예: 결제하기 또는 사용자 환경설정 저장) 실행할 작업이다. 이 로직은 대개 비즈니스 레이어나 데이터 영역에 배치되고 UI 레이어에는 배치되지 않는다.

ViewModel은 UI 상태와 앱의 다른 레이어에 있는 비즈니스 로직에 대한 액세스 권한을 제공한다. 또한 ViewModel은 구성 변경 후에도 유지되므로 컴포지션보다 전체 기간이 더 길다. Compose 콘텐츠 호스트의 수명 주기(즉, 활동이나 프래그먼트, Compose Navigation을 사용하는 경우 탐색 그래프의 대상)를 따를 수 있다.

경고: ViewModel은 컴포지션의 일부가 아니다. 따라서 메모리 누수가 발생할 수 있으므로 컴포저블에서 만든 상태(예: remembered value)를 보유해서는 안 된다.

 

 

* 이전 단계에서는 구성 가능한 함수에서 상태를 직접 관리하는 방법을 보여주었지만 UI 로직과 비즈니스 로직을 UI 상태와 분리하여 ViewModel로 이전하는 것이 좋다.

* UI 상태, 목록을 ViewModel로 이전하고 비즈니스 로직도 ViewModel로 추출해 보자.

 

1. 기존 WellnessTask Data class를 WellnessViewModel.kt로 이동시킨다.

data class WellnessTask(val id: Int, val label: String)

 

이전과 마찬가지로 toMutableStateList를 사용하여 내부 _tasks 변수를 정의하고 tasks를 목록으로 노출하여 ViewModel 외부에서 수정할 수 없도록 한다.

목록의 내장 remove 함수에 위임하는 간단한 remove 함수를 구현한다.

class WellnessViewModel : ViewModel() {
    private val _tasks = getWellnessTasks().toMutableStateList()
    val tasks: List<WellnessTask>
        get() = _tasks

   fun remove(item: WellnessTask) {
       _tasks.remove(item)
   }
}

private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }

 

 

2. viewModel() 함수를 호출하여 컴포저블에서 이 ViewModel에 액세스할 수 있다.

 

이 함수를 사용하려면 app/build.gradle.kts 파일을 열고 다음 라이브러리를 추가해야 한다. (9월 7일 기준 2.8.5)

implementation("androidx.lifecycle:lifecycle-viewmodel-compose:{latest_version}")

 

3. 화면 컴포저블의 매개변수로 viewModel()을 호출하여 wellnessViewModel을 인스턴스화한다.

따라서 이 컴포저블을 테스트할 때 교체하고 필요에 따라 끌어올릴 수 있다. WellnessTasksList에 작업 목록을 제공하고 onCloseTask 람다에 remove 함수를 제공한다.

import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun WellnessScreen(
    modifier: Modifier = Modifier,
    wellnessViewModel: WellnessViewModel = viewModel()
) {
   Column(modifier = modifier) {
       StatefulCounter()

       WellnessTasksList(
           list = wellnessViewModel.tasks,
           onCloseTask = { task -> wellnessViewModel.remove(task) })
   }
}

 

* viewModel()은 기존 ViewModel을 반환하거나 지정된 범위에서 새 ViewModel을 생성한다. ViewModel 인스턴스는 범위가 활성화되어 있는 동안 유지된다. 예를 들어 컴포저블이 활동에서 사용되는 경우 viewModel()은 활동이 완료되거나 프로세스가 종료될 때까지 동일한 인스턴스를 반환한다.

* 상태 일부와 비즈니스 로직이 포함된 ViewModel을 화면과 통합했다. 상태는 컴포지션 외부에 유지되고 ViewModel에 의해 저장되므로 목록의 변형은 구성이 변경되어도 유지된다.

ViewModel은 탐색 그래프의 활동이나 프래그먼트, 대상에서 호출되는 루트 컴포저블에 가까운 화면 수준 컴포저블에서 사용하는 것이 좋다. ViewModel은 다른 컴포저블로 전달하면 안 된다. 대신 필요한 데이터와 필수 로직을 실행하는 함수만 매개변수로 전달해야 한다.

 

선택한 상태 이전

마지막 refactoring은 선택된 상태와 로직을 ViewModel로 이전하는 것이다. 이렇게 하면 모든 상태가 ViewModel에서 관리되므로 코드가 더 간단해지고 테스트하기 쉬워진다.

 

1. 먼저 선택된 상태를 저장하고 false를 기본값으로 설정할 수 있도록 WellnessTask 모델 클래스를 수정한다.

data class WellnessTask(val id: Int, val label: String, var checked: Boolean = false)

 

2. ViewModel에서 선택된 상태의 새 값으로 수정할 작업을 수신하는 changeTaskChecked 메서드를 구현한다.

class WellnessViewModel : ViewModel() {
   ...
   fun changeTaskChecked(item: WellnessTask, checked: Boolean) =
       _tasks.find { it.id == item.id }?.let { task ->
           task.checked = checked
       }
}

 

 

3. WellnessScreen에서 ViewModel의 changeTaskChecked 메서드를 호출하여 목록의 onCheckedTask 동작을 제공한다.

@Composable
fun WellnessScreen(
    modifier: Modifier = Modifier,
    wellnessViewModel: WellnessViewModel = viewModel()
) {
   Column(modifier = modifier) {
       StatefulCounter()

       WellnessTasksList(
           list = wellnessViewModel.tasks,
           onCheckedTask = { task, checked ->
               wellnessViewModel.changeTaskChecked(task, checked)
           },
           onCloseTask = { task ->
               wellnessViewModel.remove(task)
           }
       )
   }
}

 

4. WellnessTasksList를 열고 WellnessTaskItem에 전달할 수 있도록 onCheckedTask 람다 함수 매개변수를 추가한다.

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

 

5. WellnessTaskItem.kt 파일을 정리한다. CheckBox 상태가 목록 수준으로 끌어올려지므로 더 이상 stateful 메서드가 필요하지 않다. 파일에는 다음과 같은 구성 가능한 함수만 있다.

@Composable
fun WellnessTaskItem(
   taskName: String,
   checked: Boolean,
   onCheckedChange: (Boolean) -> Unit,
   onClose: () -> Unit,
   modifier: Modifier = Modifier
) {
   Row(
       modifier = modifier, verticalAlignment = Alignment.CenterVertically
   ) {
       Text(
           modifier = Modifier
               .weight(1f)
               .padding(start = 16.dp),
           text = taskName
       )
       Checkbox(
           checked = checked,
           onCheckedChange = onCheckedChange
       )
       IconButton(onClick = onClose) {
           Icon(Icons.Filled.Close, contentDescription = "Close")
       }
   }
}

 

* 이 상태로 앱을 실행하면 checkedState를 추적하고 있지않아 Task를 체크했을때 바로 화면에 갱신되지않고 삭제 등 화면 갱신을 해야지 체크 상태가 변경된다.

* 이는 Compose에서 MutableList를 위해 추적하는 것이 요소 추가 및 삭제와 관련된 변경사항이기 때문이다. 삭제가 작동하는 이유가 바로 이것이다. 하지만 추적하도록 지시하지 않는 한 행 항목 값(여기서는 checkedState)의 변경사항을 인식하지 못한다.

문제를 해결하는 두 가지 방법은 다음과 같다.

  • 데이터 클래스 WellnessTask를 변경하여 checkedState가 Boolean 대신 MutableState<Boolean>이 되도록 한다. 그러면 Compose에서 항목 변경사항을 추적한다.
  • 변경하려는 항목을 복사하고 목록에서 항목을 삭제한 후 변경된 항목을 다시 목록에 추가한다. 그러면 Compose에서 이 목록 변경사항을 추적한다.

두 가지 방법에는 모두 장단점이 있디. 예를 들어 사용 중인 목록의 구현에 따라 요소를 삭제하고 읽는 데 비용이 많이 들 수 있다.

잠재적으로 비용이 많이 드는 목록 작업을 피하고, 더 효율적이고 Compose 직관적이므로 checkedState를 관찰 가능하도록 만들어보자.

data class WellnessTask(val id: Int, val label: String, val checked: MutableState<Boolean> = mutableStateOf(false))

 

* 이전에 본 것처럼 위임된 속성을 사용하면 이 경우에 checked 변수를 더 간단하게 사용할 수 있다.

WellnessTask를 데이터 클래스가 아닌 클래스가 되도록 변경한다. WellnessTask가 생성자에서 기본값이 false인 initialChecked 변수를 수신하도록 하면 팩토리 메서드 mutableStateOf로 checked 변수를 초기화하여 initialChecked를 기본값으로 사용할 수 있다.

class WellnessTask(
    val id: Int,
    val label: String,
    initialChecked: Boolean = false
) {
    var checked by mutableStateOf(initialChecked)
}

 

테스트

이제 결합된 내부 구성 가능한 함수가 아닌 ViewModel로 비즈니스 로직이 리팩터링되므로 단위 테스트가 훨씬 간단해진다.

계측 테스트를 사용하여 Compose 코드의 올바른 동작을 확인하고 UI 상태가 올바르게 작동하는지 확인할 수 있다.

 

 

 

출처: https://developer.android.com/codelabs/jetpack-compose-state?hl=ko#11

 

Jetpack Compose의 상태  |  Android Developers

이 Codelab에서는 상태를 관리하여 다양한 기능의 대화형 Compose 애플리케이션을 빌드하는 방법을 알아봅니다.

developer.android.com

 

728x90
반응형
TAGS.

Comments