[Android Compose] 상태란?
Compose에서 말하는 상태는 아래와 같다.
https://developer.android.com/develop/ui/compose/state?hl=ko
앱의 상태는 시간이 지남에 따라 변할 수 있는 값이다. Room DB부터 클래스 변수까지 모든 항목이 포함된다. 모든 안드로이드 앱에선 유저에게 상태가 표시된다. 아래는 상태의 몇 가지 예다.
- 네트워크 연결을 설정할 수 없을 때 표시되는 스낵바
- 블로그 게시물, 관련 댓글
- 클릭 시 버튼에서 재생되는 물결 애니메이션
- 이미지 위에 그릴 수 있는 스티커
Compose를 쓰면 앱에서 상태를 저장하고 사용하는 위치, 방법을 명시적으로 나타낼 수 있다...(중략)
이 상태를 활용해 컴포저블 함수를 구성해서 UI를 만들고, 이 화면에서 상태가 바뀌면 재구성을 통해 새 상태가 컴포저블 함수에 반영된다.
아래 코드는 안드로이드 디벨로퍼에 게시된 예시다.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.composeprac.ui.theme.ComposePracTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposePracTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
HelloContent()
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HelloContent() {
Column(modifier = Modifier.padding(16.dp)) {
Text(
text = "Hello!",
modifier = Modifier.padding(bottom = 8.dp),
style = MaterialTheme.typography.bodyMedium
)
OutlinedTextField(
value = "",
onValueChange = {},
label = {
Text("Name")
}
)
}
}
실행하면 안드로이드 14 에뮬레이터 기준으로 아래와 같은 화면이 표시된다.
실행 후 저 OutlinedTextField에 뭔가를 입력하려고 하면 아무것도 입력되지 않는다. 정상 동작이니 디버깅할 필요는 없다.
이 아무것도 입력되지 않는 현상의 원인은 OutlinedTextField가 자체적으로 업데이트되지 않고, 공백으로 설정된 value 매개변수가 바뀔 때 업데이트되기 때문인데, value 변수는 공백인 채 어떠한 변경도 가해지지 않는 걸 알 수 있다.
그리고 이는 Compose에서 컴포지션, 리컴포지션이 작동하는 방식과도 연관이 있다.
컴포저블의 상태
그럼 유저가 저 TextField에 입력되는 값을 어딘가에서 기억하게 하고, 이 기억된 값을 가져와서 TextField에 세팅하면 일반적으로 예상할 수 있듯 입력한 글자들이 표시될 것이다.
이걸 가능하게 하는 방법이 remember API로, 이 API의 특징은 아래와 같다.
- remember API가 계산한 값은 초기 컴포지션 중에 컴포지션에 저장되고, 저장된 값은 리컴포지션 중에 리턴된다
- remember는 변경 가능한 객체와 불변 객체를 저장하는 데 사용할 수 있다
- remember를 호출한 컴포저블 함수가 컴포지션에서 제거되면 그 객체도 삭제된다
잊지 말아야 할 것은 컴포저블은 함수라는 것이다. 그래서 특정 상태값을 지역 변수로 저장하면 리컴포지션 발생 시 함수가 새로 호출되기 때문에, 지역 변수에 저장된 상태값은 사라진다.
이걸 막기 위해 컴포즈에선 mutableStateOf를 사용한다.
interface MutableState<T> : State<T> {
override var value: T
}
이 value가 바뀌면 value를 읽는 컴포저블 함수의 리컴포지션이 예약된다.
즉 value가 바뀌면 State가 바뀌기 때문에 State를 관찰하는 컴포저블 함수의 리컴포지션이 발생한다.
MutableState 객체 선언 방법은 3가지 방법이 있다. 선언들은 모두 동일하며 읽기 쉬운 방식을 선택하면 된다.
// 1.
val mutableState = remember { mutableStateOf(default) }
// 2.
var value by remember { mutableStateOf(default) }
// 3.
val (value, setValue) = remember { mutableStateOf(default) }
by 위임을 사용하려면 import문을 추가해야 한다.
import androidx.compose.runtime.getValue
import androidx.compose.runtime.setValue
// 또는 아래 한 줄만 사용
import androidx.compose.runtime.*
수정된 코드는 아래와 같다.
import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import com.example.composeprac.ui.theme.ComposePracTheme
class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
ComposePracTheme {
Surface(
modifier = Modifier.fillMaxSize(),
color = MaterialTheme.colorScheme.background
) {
HelloContent()
}
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@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.bodyMedium
)
}
OutlinedTextField(
value = name,
onValueChange = { name = it },
label = {
Text("Name")
}
)
}
}
OutlinedTextField 안의 로직도 같이 바뀌었다. onValueChange 콜백에서 바뀐 값을 받으면 mutableStateOf에 의해 공백으로 초기화된 name에 저장하고, value 매개변수로도 name을 넘겨 입력된 글자들을 TextField 안에 표시한다.
if문으로 name에 담긴 값이 공백이 아닌 경우에만 표시되기 때문에, 처음에는 아무것도 표시되지 않는다.
아무 글자나 입력하면 if 안의 로직이 작동한다.
그럼 이 처리를 활용하면 구성 변경에도 대응할 수 있을까? 아니다. 화면을 세로 모드에서 가로 모드로 바꾸는 등의 구성 변경이 발생할 경우엔 rememberSaveable을 써야 한다.
그리고 상태를 유지하는 데 무조건 MutableState 원툴로 코드를 작성하지 않아도 된다. 안드로이드에선 다른 관찰 가능한 상태 유형들을 몇 가지 더 지원한다. 그러나 Compose에서 이 다른 상태 유형들을 읽으려면 State<T>로 바꿔서 상태가 바뀔 때 컴포저블 함수가 자동으로 리컴포지션되도록 해야 한다.
- Flow - collectAsStateWithLifecycle : 생명 주기를 인식하는 방식으로 Flow에서 값을 수집한다. Compose의 State에서 마지막으로 내보낸 값을 나타내므로 Flow를 수집할 때 사용하는 게 좋다
- Flow - collectAsState : Compose의 State로 바꾼단 점에선 collectAsStateWithLifecycle과 유사하지만, collectAsStateWithLifecycle은 안드로이드 전용이므로 플랫폼 제약이 없다면 collectAsState를 쓰는 게 좋다. collectAsState는 compose-runtime에서 쓸 수 있어 의존성을 추가할 필요가 없다.
- LiveData - observeAsState
- RxJava2, 3 - subscribeAsState