Jetpack Compose의 상태를 사용하는 것과 관련한 핵심 개념 학습
State<T>
API를 사용하여 상태를 자동으로 추적하는 방법remember
및 rememberSaveable
API 사용mutableStateListOf
및 toMutableStateList
API 사용ViewModel
을 사용하는 방법빌드할 항목 | 주요 기능 |
---|---|
1. 물 섭취량을 추적하는 워터 카운터 .......................................................................... 2. 하루 동안 해야할 웰니스 작업 목록 |
앱의 상태는 시간이 지남에 따라 변할 수 있는 값으로 이는 매우 광범위한 정의로서 Room 데이터베이스로부터 클래스 변수까지 모든 항목이 포함된다.
Android 앱에서는 사용자에게 상태가 표시된다. 다음은 Android 앱 상태의 몇 가지 예시이다.
상태에 따라 특정 시점에 UI에 표시되는 항목이 결정된다.
Android 앱에서는 이벤트에 대한 응답으로 상태가 업데이트 된다.
Compose에서 상태 관리는 상태와 이벤트가 서로 상호작용하는 방식을 이해하는 것이 중요하다.
이벤트는 애플리케이션 외부 또는 내부에서 생성되는 입력으로 아래 예시를 봐보자
앱 상태로 UI에 표시할 항목에 관한 설명이 제공되고, 이벤트를 통해 상태와 UI가 변경된다.
Compose 앱은 Composable function을 호출하여 데이터를 UI로 변환한다. 컴포저블을 실행할 때 Compose에서 빌드한 UI에 관한 설명을 컴포지션이라고 한다. 상태가 변경되면 Compose는 영향을 받는 Composable function을 새상태로 다시 실행하여 업데이트된 UI가 만들어진다(리컴포지션). 또한 Compose는 데이터가 변경된 구성요소만 재구성하기 때문에 각 컴포저블에 필요한 데이터를 확인한다.
업데이트를 받을 경우 리컴포지션을 예약할 수 있도록 Compose가 추적할 상태를 알아야한다.
컴포저블내의 상태를 추가하기 위해선 mutableStateOf
함수를 사용하면 된다.
State
를 읽는 다면 리컴포지션한다.State 및 MutableState는 어떤 값을 보유하고 그 값이 변경될 때마다 UI업데이트(리컴포지션)을 트리거하는 인터페이스이다.
여러 리컴포지션 간에 상태를 유지하기 위해서 remember
를 같이 사용하여 변경 가능한 상태를 기억하도록 한다.
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
...
@Composable
fun Greeting() {
val expanded = remember { mutableStateOf(false) }
...
}
Compose에는 특정 상태를 읽는 컴포저블의 리컴포지션을 예약하는 상태 추적 시스템을 통해 Compose가 세분화되어 전체 UI가 아닌 변경해야 하는 컴포저블만 재구성할 수 있다. 이 작업은 '쓰기'(즉, 상태 변경)뿐만 아니라 상태에 대한 '읽기'도 추적하여 실행된다.
Compose의 State
및 MutableState
유형을 사용하여 Compose에서 상태를 관찰할 수 있도록 한다.
Compose는 상태 value
속성을 읽는 각 컴포저블을 추적하고 그 value
가 변경되면 리컴포지션을 트리거 한다.
mutableStateOf
함수를 사용하여 관찰 가능한 MutableState
를 만들 수 있으며, 이 함수는 초깃값을 State
객체에 래핑된 매개변수로 수신한 다음, value
의 값을 관찰 가능한 상태로 만든다.
예시
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
val count: MutableState<Int> = mutableStateOf(0)
Text("You've had ${count.value} glasses.")
Button(onClick = { count.value++ }, Modifier.padding(top = 8.dp)) {
Text("Add one")
}
}
}
value
를 업데이트하여 상태를 업데이트할 수 있고 Compose는 value를 읽는 함수에 리컴포지션을 트리거 한다.리컴포지션 예약은 잘 작동하나 리컴포지션이 발생하면 count 변수가 다시 0으로 초기화되기 때문에 이를 유지할 방법이 필요하다.
이를 위해서 remember composable 인라인 함수를 사용할 수 있다.
remember
로 계산된 값은 컴포지션에 저장되고 저장된 값은 리컴포지션 간에 유지된다.
remember 사용은 private val 속성이 객체에서 실행하는 것과 같은 방식으로 단일 객체를 컴포지션에 저장하는 메커니즘으로 생각하면 된다.
일반적으로 remember
와 mutableStateOf
는 컴포저블 내에서 함께 사용된다.
val count: MutableState<Int> = remember { mutableStateOf(0) }
var count by remember { mutableStateOf(0) }
📌by
키워드를 사용하여 count를 var로 정의할 수 있다. 이럴 경우 MutableState의 value
속성을 명시적으로 참조하지 않고도 count를 간접적으로 읽고 변경할 수 있다.
참고 : 이미 LiveData, StateFlow, Flow, RxJava의 Observable과 같은 다른 관찰 가능한 유형을 사용하여 상태를 앱에 저장하고 있을 수 있다. Compose에서 이 상태를 사용하고 상태가 변경될 때 자동으로 재구성하도록 하려면 이를
State<T>
에 매핑해야 한다.
이를 위해 설계된 확장 함수가 있으므로 Compose 및 기타 라이브러리 문서에서 이를 찾아야 한다.
만약 디바이스가 회전한다면 전체 Activity가 다시 시작되므로 상태가 유지되지 않는 문제가 있다.
remember
를 사용하는 대신 rememberSaveable
을 사용하면 구성 변경(ex. 회전, 다크모드로 변경)과 프로세스가 중단될 경우에도 각 상태를 저장할 수 있다.
Compose는 선언형 UI 프레임워크이다. 상태가 변경 될 때 UI구성요소를 삭제하거나 visivilty 상태를 변경하는 대신 특정 상태의 조건에서 UI가 어떻게 표현되어야 하는지를 설명한다. 재구성이 호출되고 UI가 업데이트된 결과, 컴포저블이 결국 컴포지션을 시작하거나 종료할 수 있다.
remember
는 컴포지션에 객체를 저장하고, remember
가 호출되는 소스 위치가 리컴포지션 중에 다시 호출되지 않으면 객체를 삭제한다.
remember
를 사용하여 객체를 저장하는 컴포저블에는 내부 상태가 포함되어 있어 컴포저블을 스테이트풀(Stateful)로 만든다.
@Composable
fun WaterCounter(modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
var count by remember { mutableStateOf(0) }
if (count > 0) {
var showTask by remember { mutableStateOf(true) }
if (showTask) {
WellnessTaskItem(
onClose = { showTask = false },
taskName = "Have you taken your 15 minute walk today?"
)
}
Text("You've had $count glasses.")
}
Row(Modifier.padding(top = 8.dp)) {
Button(onClick = { count++ }, enabled = count < 10) {
Text("Add one")
}
Button(
onClick = { count = 0 },
Modifier.padding(start = 8.dp)) {
Text("Clear water count")
}
}
}
}
이 코드로 예시를 들면 Add one 버튼을 누를 때 마다 count는 증가되는 걸 알 수 있다.
여기서 count > 0 라면, showTask를 컴포지션에 저장하는데, 만약 clear 버튼을 누른다면 count가 0이 되서 if (count > 0) 문을 실행하지 않게 되어 showTask가 호출되지 않았기 때문에 showTask를 삭제하게 된다. 결과적으로 다시 count를 증가시키면 showTask = true 인 상태로 컴포지션에 다시 저장하게 된다.
만약, 디바이스를 가로모드로 회전 시킨다면 Activity는 재생성되기 때문에 앞에서 선언한 count가 다시 0으로 선언되게 된다.
이런 경우를 의도한게 아니라면 rememberSaveable
을 사용하여 activity가 재생성 되어도 상태를 유지할 수 있도록 해줄 수 있다.
rememberSaveable
은 Bundle
에 저장할 수 있는 모든 값을 자동으로 저장한다.rememberSaveable
은 Activity 및 프로세스 재생성 전반에 걸처서도 상태를 유지할 수 있다.📌 앱의 상태 및 UX 요구사항에 따라 remember
를 사용할지 rememberSaveable
을 사용할지 고려해야 한다.
상태를 보유하지 않은 컴포저블을 Stateless 컴포저블이라고 하며 상태 state hoisting을 사용하면 쉽게 만들 수 있다.
📌 Stateless란 ? : Composable 함수에서 모든 상태를 추출할 수 있는 경우에 Stateless 컴포저블 함수라고 한다.
State hoisting은 UDF(단방향 데이터 흐름) 아키텍처를 Compose에서 구현하는 방법이다.
📌 Stateful과 Stateless 비교
Stateful
: 시간이 지남에 따라 변할 수 있는 상태를 소유하고 있는 컴포저블
Stateless
: 상태를 소유하지 않는 컴포저블. 즉, 새 상태를 보유하거나 정의하거나 수정하지 않는다.
예시
stateless 컴포저블
@Composable
fun StatelessCounter(count: Int, onIncrement: () -> Unit, modifier: Modifier = Modifier) {
Column(modifier = modifier.padding(16.dp)) {
if (count > 0) {
Text("You've had $count glasses.")
}
Button(onClick = onIncrement, Modifier.padding(top = 8.dp), enabled = count < 10) {
Text("Add one")
}
}
}
Stateful 컴포저블
@Composable
fun StatefulCounter() {
var waterCount by remember { mutableStateOf(0) }
var juiceCount by remember { mutableStateOf(0) }
StatelessCounter(waterCount, { waterCount++ })
StatelessCounter(juiceCount, { juiceCount++ })
}
이런식으로 코드를 작성하면 물과 주스의 잔 개수를 계산할 때 독립된 두 가지 상태를 표시할 수 있다.
따라서 다음과 같은 장점이 있다.
1. Stateless 컴포저블을 재사용할 수 있다.
예를 들어 juiceCount가 수정되면 StatefulCounter가 리컴포지션이 되고, StatefulCounter는 juiceCount를 읽는 함수만 식별하고 트리거하여 리컴포지션을 수행한다. 즉, waterCount를 읽는 컴포지션은 리컴포지션되지 않는다.
@Composable
fun StatefulCounter() {
var count by remember { mutableStateOf(0) }
StatelessCounter(count, { count++ })
AnotherStatelessMethod(count, { count *= 2 })
}
Compose에서 관찰할 수 있는 MutableList
객체를 만들어야 한다.
mutableStateListOf
를 사용하여 객체를 만들거나 확장 함수 mutableStateListOf()
를 사용한다.
mutableStateListOf
및 mutableStateListOf
함수는 SnapshotStateList<T>
유형의 객체를 반환한다.getWellnessTasks()
를 호출하여 목록 리스트를 불러와 toMutableStateList
를 사용하여 목록을 만든다.@Composable
fun WellnessScreen(modifier: Modifier = Modifier) {
Column(modifier = modifier) {
StatefulCounter()
val list = remember { getWellnessTasks().toMutableStateList() }
WellnessTasksList(list = list, onCloseTask = { task -> list.remove(task) })
}
}
private fun getWellnessTasks() = List(30) { i -> WellnessTask(i, "Task # $i") }
val list = remember { mutableStateListOf<WellnessTask>() }
list.addAll(getWellnessTasks())
WellnessTaskList
함수 수정, onCloseTask를 추가하고 WellnessTaskItem에 전달@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) })
}
}
}
items
메서드는 key
매개변수를 받고, 각 상목의 상태는 목록에 있는 위치를 기준으로 키가 지정된다. 만약 삭제로 인해 위치가 변경된 item은 list 데이터 세트가 변경될 때 문제가 발생한다. WellnessTaskItem
의 id
를 각 항목의 키로 사용하면 쉽게 해결이 가능하다.WellnessTaskItem
을 수정한다. WellnessTaskItem
컴포저블에 checkedState를 선언하여 Stateful 컴포저블로 만들어준다.@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,
)
}
위 코드는 그림처럼 동작하게 된다.
📌list를 rememberSaveable()
를 사용하여 저장하려고 하면 런타임 예외가 발생한다.
rememberSaveable()
을 사용하지 않도록 한다.📌 ViewModel은 화면 수준의 컴포저블에서 사용하는 것을 권장한다.
📌주의 : ViewModel은 컴포지션의 일부가 아니기 때문에 메모리 누수가 발생할 수 있으므로 컴포저블에서 만든 State를 보유해서는 안 된다.
getWellnessTasks()
를 WellnessViewModel
로 이동toMutableStateList
를 사용하여 내부 _tasks
변수를 정의하고 tasks
목록으로 노출하여 ViewModel 외부에서 수정할 수 없도록 만든다.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") }
viewModel()
을 호출하여 wellnessViewModel
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()
은 Activity가 완료되거나 프로세스가 종료될 때까지 동일한 객체를 반환한다.애니메이션이 완료될 때까지 애니메이션에 의해 객체의 value
가 계속 업데이트되는 상태 객체를 반환한다.
@Composable
public fun animateDpAsState(
targetValue: Dp,
animationSpec: AnimationSpec<Dp>,
label: String,
finishedListener: ((Dp) -> Unit)?
): State<Dp>
@Composable
private fun Greeting(name: String) {
var expanded by remember { mutableStateOf(false) }
val extraPadding by animateDpAsState(
if (expanded) 48.dp else 0.dp,
animationSpec = spring(
dampingRatio = Spring.DampingRatioMediumBouncy,
stiffness = Spring.StiffnessLow
)
)
Surface(
...
Column(modifier = Modifier
.weight(1f)
.padding(bottom = extraPadding.coerceAtLeast(0.dp))
...
)
}