jetpack compose 에 한창 포스팅 중입니다.
https://gift123.tistory.com/33
-이전편 Compose 이해정리
오늘은 Compose 상태관리를 알아보도록 하겠습니다.
Compose에서의 상태
*앱의 상태는 시간에 따라 변할 수 있는 값을 모두 포함한 것을 이야기한다.
(ex: Room 데이터베이스부터 클래스 변수까지 모든 변할 수 있는값을 뜻함)
상태개념은 Compose에서 핵심
-모든 안드로이드 앱은 사용자에게 앱의 상태를 표시함
->예를들어 아래와 같은경우들이 있음
- 네트워크 연결을 설정할 수 없을 때 표시되는 스낵바
- 블로그 게시물 및 관련 댓글
- 사용자가 클릭하면 버튼에서 재생되는 물결 애니메이션
- 사용자가 이미지 위에 그릴 수 있는 스티커
-Jetpack Compose 는 상태를 저장하거나 사용하는 위치와 방법들을 명시적으로 나타냄
컴포지션 및 리컴포지션
컴포지션: Jetpack Compose가 컴포저블을 실행할 때 빌드한 UI에 관한 설명
초기 컴포지션: 처음 컴포저블을 실행하여 컴포지션을 만듬
리컴포지션(재구성): 데이터가 변경될 때 컴포지션을 업데이트하기 위해 컴포저블을 다시 실행하는 것을 말합니다.
-컴포지션은 UI를 기술하는 역할을 하며 컴포저블을 실행하면 생성
(컴포지션은 UI를 기술하는 컴포저블의 트리 구조)
컴포지션 흐름
1) 초기 컴포지션 시 Compose는 컴포지션에서 호출하는 컴포저블을 추적
2) 추적 후 다음 앱 상태가 변경되면 Jetpack Compose는 리컴포지션을 예약
3) 리컴포지션에서는 상태 변경에 관한 응답으로 변할 수 있는 컴포저블을 실행
4) Jetpack Compose는 변경사항을 반영하도록 컴포지션을 업데이트
-컴포지션은 초기 컴포지션을 통해서만 생성되고 리컴포지션을 통해서만 업데이트
-컴포지션을 수정하는 유일한 방법은 리컴포지션을 통하는 것입니다.
상태 소개
예제 1. 텍스트를 적어도 아무것도 실행되지않는 Composable함수
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = "",
onValueChange = { },
label = { Text("Name") }
)
}
}
텍스트 박스에 글을 적어도 반응이 없습니다.
예제2. 이벤트가 작동하는 코드
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
var name by remember { mutableStateOf("") }
Text(
text = "Hello",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
}
}
Composable함수를 업데이트를 하려면
remember { mutableStateOf() }를 사용해야합니다.
OutlinedTextField의 상태를 나타내는 값을 전달하고, (value)
OutlinedTextField의 값이 변경될 때 상태를 업데이트하는 코드를 추가합니다. (onValueChance)
그러면 Composable함수를 업데이트를 하기 때문에
아래와 같이 동작합니다.
Comosable 함수는
remember Composable을 사용해서 메모리에 단일 객체를 저장할 수 있습니다.
remember에 의해 계산된 값은 초기 컴포지션 중에 컴포지션에 저장되고
저장된 값은 리컴포지션(재구성) 중에 반환됩니다.
-remember는 객체를 컴포지션에 저장하고,
-remember를 호출한 컴포저블이 컴포지션에서 삭제되면 그 객체를 삭제합니다.
-remember는 변경 가능한 객체뿐만 아니라 변경할 수 없는 객체를 저장하는 데에도 사용할 수 있습니다.
-remember는 리컴포지션(재구성)에서 상태를 보존하는 데 도움이 됩니다.
(위 예제에서 remember를 함께 사용하지 않고 mutableStateOf를 사용하면 HelloContent 함수의 텍스트가 리컴포지션할때마다 빈 문자열로 다시 초기화됩니다.)
mutableStateOf
mutableStateOf는 Compose에서 observable type인 MutableState을 생성합니다.
interface MutableState<T> : State<T> {
override var value: T
}
위 value가 변경되면 그 값을 읽는 Comosable 함수의 리컴포지션(재구성)이 생깁니다.
아래와 같은 3가지 유형으로 mutableStateOf를 사용할 수 있습니다.
- val mutableState = remember { mutableStateOf(default) }
- var value by remember { mutableStateOf(default) }
- val (value, setValue) = remember { mutableStateOf(default) }
remember와 같이 사용하면
기억된 값을 다른 Composable의 매개변수로 사용하거나 구문의 로직으로
사용하여 표시할 Composable을 변경할 수 있습니다.
예를 들어
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
var name by remember { mutableStateOf("") }
if (name.isNotEmpty()) {
Text(
text = "Hello, $name!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
}
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = { Text("Name") }
)
}
}
if문으로 name변수가 비어 있지 않는 경우 인사말을 표시하는 분기처리도 가능합니다.
그러면 아래처럼 name에 text가 비어있으면 위의 인사말이 사라졌다가
text가 글이 있으면 인사말이 표시 됩니다.
remember가 리컴포지션(재구성) 과정 전체에서 상태를 유지하는 데 도움은 되지만
구성 변경시에 유지가 되지 않습니다.
( the state is not retained across configuration changes.)
이 경우에는 rememberSaveable을 사용해야 합니다.
rememberSaveable은 Bundle에 저장할 수 있는 모든 값을 자동으로 저장합니다.
다른 값의 경우에는 맞춤 Saver 객체를 전달할 수 있습니다.
//위 예제에서 var name by remember {mutableStateOf("")} 이걸 지우고 아래껄 사용하면
//구성 변경 환경에서도 상태가 저장됨
var name by rememberSaveable { mutableStateOf("") }
예시)
구성 변경이나 전화 통화 같은 중단 상황이 발생하면 내부 상태를 유지할지 선택해야합니다.
유지해야 하는 경우라면 rememberSaveable을 사용하여 상태를 저장합니다.
Stateful 와 stateless
remember객체를 저장하는 데 사용하는 Composable은 자체적으로 상태를 생성하여 Composable을 Stateful 합니다.
이것은 호출자가 상태를 제어 할 필요가없고 상태를 직접 관리하지 않고도 사용할 수있는 상황에서 유용 할 수 있습니다.
그러나 Composable이 자체 상태를 갖는 경우 컴포저블의 재사용과 테스트가 어렵게 됩니다
재사용과 테스트를 쉽게 하려면 상태를 갖지않는 Composable인 Stateless Composable도 만들어야 합니다.
재사용 가능한 Composable를 개발할 때
동일한 컴포저블의 스테이트풀(Stateful) 버전과 스테이트리스(Stateless) 버전을 모두 노출하고자 할 수 있습니다.
스테이트풀(Stateful) 버전은 상태를 염두에 두지 않는 호출자에 편리하며,
스테이트리스(Stateless) 버전은 상태를 제어하거나 끌어올려야 하는 호출자에 필요합니다.
상태 끌어올리기 State Hosting
상태 끌어올리기는 구성요소를 스테이트리스(Stateless)로 만들기 위해 상태를 트리 위로 옮기는 패턴입니다.
이 패턴이 컴포저블에 적용되는 경우 컴포저블에 매개변수 2개가 추가될 때가 많습니다.
- value: T: 표시할 현재 값
- onValueChange: (T) -> Unit: T가 제안된 새 값인 경우 값을 변경하도록 요청하는 이벤트
(2개만 사용한다는 얘기는 아닙니다. 필요하다면 파라미터를 더 만들어서 쓰면 됩니다.)
예제.
@Composable
fun HelloScreen() {
var name by rememberSaveable { mutableStateOf("") }
HelloContent(name = name, onNameChange = { name = it })
}
@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}
위 예에서 HelloContent에서 name과 onValueChange를 추출한 다음,
이 항목을 트리 상단에 HelloContent를 호출하는 HelloScreen Composable로 옮깁니다.
HelloContent는 변경할 수 없는 String 매개변수의 형태로 상태에 액세스할 수 있습니다.
또한 상태 변경을 요청하고자 할 때 호출할 수 있는 람다 onNameChange에도 액세스할 수 있습니다.
람다는 Composable의 이벤트를 설명하는 가장 일반적인 방법입니다.
위 예에서 (String) -> Unit를 사용해서 onNameChange 이벤트를 정의합니다.
HelloContent에서 상태를 끌어올리면 더 쉽게 Composable을 추론하고
여러 상황에서 재사용하며 테스트할 수 있습니다.
HelloContent는 상태의 저장 방식과 분리됩니다.
분리된다는 것은 HelloScreen을 수정하거나
교체할 경우 HelloContent의 구현 방식을 변경할 필요가 없다는 의미입니다.
상태가 내려가고 이벤트가 올라가는 패턴을 단방향 데이터 흐름이라고 합니다.
이 경우 상태는 HelloScreen에서 HelloContent로 내려가고
이벤트는 HelloContent에서 HelloScreen으로 올라갑니다.
단방향 데이터 흐름을 따르면 UI에 상태를 표시하는
Composable과 상태를 저장하고 변경하는 앱 부분을 서로 분리할 수 있습니다.
또 다른 예제.
@Composable
fun ExpandingCard(title: String, body: String) {
var expanded by remember { mutableStateOf(false) }
ExpandingCard(
title = title,
body = body,
expanded = expanded,
onExpand = { expanded = true },
onCollapse = { expanded = false }
)
}
@Composable
fun ExpandingCard(
title: String,
body: String,
expanded: Boolean,
onExpand: () -> Unit,
onCollapse: () -> Unit
) {
Card {
Column(
Modifier
.width(280.dp)
.animateContentSize() // automatically animate size when it changes
.padding(top = 16.dp, start = 16.dp, end = 16.dp)
) {
Text(title)
if (expanded) {
Spacer(Modifier.height(8.dp))
Text(body)
IconButton(onClick = onCollapse, Modifier.fillMaxWidth()) {
Icon(imageVector = Icons.Default.ExpandLess, contentDescription = "Expand less")
}
} else {
IconButton(onClick = onExpand, Modifier.fillMaxWidth()) {
Icon(imageVector = Icons.Default.ExpandMore, contentDescription = "Expand more")
}
}
}
}
}
- 단일 소스 저장소: 상태를 복제하지 않고 위로 옮겼기 때문에expanded의 소스 저장소가 하나만 있습니다. 버그 방지에 도움이 됩니다.
- 캡슐화: 스테이트풀(Stateful) ExpandingCard만 상태를 수정할 수 있습니다.
- 공유 가능: 끌어올린 상태를 여러 Composable과 공유 가능합니다. (예를 들어 Card가 펼쳐질 때 Fab 버튼을 숨기려고 한다면 끌어올리기를 통해 그 작업이 가능합니다.)
- 가로채기 가능: 스테이트리스(Stateless) ExpandingCard의 호출자는 상태를 변경하기 전에 이벤트를 무시할지 수정할지 결정할 수 있습니다.
- 분리됨: 스테이트리스(Stateless) ExpandingCard의 상태는 어디에나 저장할 수 있습니다. 예를 들어 이제는 title, body, expanded를 ViewModel로 옮길 수 있습니다.
위예제도 마찬가지로 단방향 데이터 흐름을 따르게 됩니다.
이 두번째 예제도 마찬가지로
상태는 스테이트풀(Stateful) 컴포저블에서 아래로 전달되고
이벤트는 스테이트리스(Stateless) 컴포저블에서 위로 흐릅니다.
*상태를 끌어올릴 때 상태의 이동 위치를 쉽게 파악할 수 있는 세 가지 규칙이 있습니다.*
- 상태는 적어도 그 상태를 사용하는 모든 컴포저블의 가장 낮은 공통 상위 요소로 끌어올려야 합니다(읽기).
- 상태는 최소한 변경될 수 있는 가장 높은 수준으로 끌어올려야 합니다(쓰기).
- 동일한 이벤트에 대한 응답으로 두 상태가 변경되는 경우 두 상태를 함께 끌어올려야 합니다.
Compose에서 다른 유형의 상태 사용
Compose에서는 상태를 보존하기 위해 MutableState<T>를 사용할 필요가 없습니다.
Compose는 observable type을 지원합니다.
observable type을 읽으려면 상태를 State<T>로 변환해야 합니다.
그래야 상태가 변할 때 자동으로 리컴포지션(재구성)됩니다.
Compose에는 이미 안드로이드에서 자주 사용 하는observable type에서 State<T>를 만들 수 있는 함수가 내장되어 있습니다.
앱에 observable한 커스텀 클래스를 사용하는 경우 observable type을 읽어오기 위한
Compose용 확장 함수를 빌드할 수 있습니다.
모든 변경사항을 수신하도록 Compose에 허용하는
모든 객체를 State<T>로 변환하고 Composable을 통해 읽어올 수 있습니다.
Compose는 State<T> 객체를 읽어오면서 자동으로 리컴포지션(재구성)됩니다.
Compose에서 LiveData 같은 다른 observable type을 사용할 경우
Composable에서 LiveData<T>.observeAsState() 같은 Composable 함수를 사용하여
그 type을 읽어오려면 유형을 State<T>로 변환해야 합니다.
주의할점: Compose에서 ArrayList<T> 또는 mutableListOf()
같은 변경 가능 객체를 상태로 사용하면 앱에 잘못되거나 오래된 데이터가 표시될 수 있습니다.
변경 가능한 객체 중 ArrayList<T> 또는 변경 가능한 데이터 클래스
같은 observe가 불가능한 객체는
그 객체가 변경될 때 컴포지션을 트리거하도록 Compose에서 관찰할 수 없습니다.
observe가 불가능하면서 변경 가능한 객체를 사용하는 대신에
State<List<T>> 및 변경 불가능한 listOf() 같은 관찰 가능 데이터 홀더를 사용하는 것이 좋습니다.
ViewModel 및 상태
Compose에서 ViewModel을 사용하면 상태를 observable type( LiveData, Flow,Rx java 등등)에
노출하고 그 상태에 영향을 주는 이벤트를 처리할 수도 있습니다.
예제.
ViewModel
class HelloViewModel : ViewModel() {
private val _name = MutableLiveData("")
val name: LiveData<String> = _name
fun onNameChange(newName: String) {
_name.value = newName
}
}
Activity
@Composable
fun HelloScreen(helloViewModel: HelloViewModel = viewModel()) {
val name: String by helloViewModel.name.observeAsState("")
HelloContent(name = name, onNameChange = { helloViewModel.onNameChange(it) })
}
@Composable
fun HelloContent(name: String, onNameChange: (String) -> Unit) {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello, $name",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.h5
)
OutlinedTextField(
value = name,
onValueChange = onNameChange,
label = { Text("Name") }
)
}
}
observeAsState는 LiveData<T>를 관찰하고,
LiveData가 변경될 때마다 업데이트되는 State<T> 객체를 반환합니다.
State<T>는 J Compose가 직접 사용할 수 있는 관찰 가능한 유형입니다.
observeAsState는 컴포지션에 있는 동안에만 LiveData를 관찰합니다.
val name: String by helloViewModel.name.observeAsState("")
observeAsState에서 반환된 상태 객체를 자동으로 래핑 해제하는 구문 슈가입니다.
또한 할당 연산자(=)를 사용하여 상태 객체를 할당할 수 있습니다.
이 경우 String 대신 State<String>이 됩니다.
val nameState: State<String> = helloViewModel.name.observeAsState("")
ViewModel 사용시,
상태는 HelloViewModel에서 아래로 흐르고
이벤트는 HelloScreen에서 위로 흐르는 단방향 데이터 흐름 패턴을 따릅니다.
- 이벤트: 사용자가 문자를 입력시 그 응답으로 onValueChange가 호출
- 상태 업데이트: HelloViewModel.onNameChange가 처리를 진행 후 변경 가능한 LiveData, _name의 상태를 설정
- 상태 표시: HelloViewmodel.name의 값이 변경 -> observeAsState에서 관찰됩니다. 그러면 HelloScreen이 name의 새 값에 따라 UI를 기술하도록 다시 실행
Compose 상태 복원
- 액티비티 또는 프로세스가 다시 생성된 이후 rememberSaveable을 사용하여 UI 상태를 복원
- rememberSaveable은 재구성 과정 전체에서 상태를 유지
- rememberSaveable은 활동 및 프로세스 재생성 전반에 걸쳐 상태를 유지
상태를 저장하는 방법
@Composable
fun MyExample() {
var selectedId by rememberSaveable<String?> { mutableStateOf(null) }
/*...*/
}
Bundle에 추가되는 모든 데이터 유형은 자동으로 저장됩니다.
Bundle에 추가할 수 없는 항목을 저장하려는 경우 몇 가지 옵션이 있습니다.
가장 간단한 방법은 객체에 @Parcelize 주석을 추가하는 것입니다.
그러면 객체가 parcelable이 되며 번들로 제공될 수 있습니다.
@Parcelize
data class City(val name: String, val country: String)
@Composable
fun MyExample() {
var selectedCity = rememberSaveable { mutableStateOf(City("Madrid", "Spain")) }
}
MapSaver
어떤 이유로 @Parcelize가 적합하지 않을 경우
mapSaver를 사용하여 시스템이 Bundle에 저장할 수 있는 값 집합으로
객체를 변환하는 고유한 규칙을 정의할 수 있습니다.
data class City(val name: String, val country: String)
val CitySaver = run {
val nameKey = "Name"
val countryKey = "Country"
mapSaver(
save = { mapOf(nameKey to it.name, countryKey to it.country) },
restore = { City(it[nameKey] as String, it[countryKey] as String) }
)
}
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
}
ListSaver
맵에 대한 키를 정의 할 필요가 없도록 listSaver 인덱스를 키로 사용 하고 사용할 수도 있습니다 .
data class City(val name: String, val country: String)
val CitySaver = listSaver<City, Any>(
save = { listOf(it.name, it.country) },
restore = { City(it[0] as String, it[1] as String) }
)
@Composable
fun CityScreen() {
var selectedCity = rememberSaveable(stateSaver = CitySaver) {
mutableStateOf(City("Madrid", "Spain"))
}
}
컴포즈에 대해 알면 알수록 더 빠져드는것 같습니다.
이상 Compose 상태관리에 대한 정리였습니다.
'안드로이드 Android' 카테고리의 다른 글
안드로이드 개발 (11) Compose Side-effects (0) | 2021.06.11 |
---|---|
안드로이드 개발 (10) Compose Composable Lifecycle (0) | 2021.06.07 |
안드로이드 개발 (8) Compose 이해 정리 (0) | 2021.06.02 |
안드로이드 개발 (7) Compose 를 사용하는 이유 (1) | 2021.06.01 |
안드로이드 개발 (6) - Glide 캐시 처리 (0) | 2021.05.30 |