State 의 정의
어플리케이션에서 State란 시간이 지남에 따라 계속 변화할 수 있는 모든 값을 의미한다. state 는 Room database 에 저장된 값일 수도 있고, 클래스의 프로퍼티일 수도 있다.
Android Application 을 예로 들자면 다음과 같은 것들이 모두 상태를 유저에게 보여주는 것이라고 할 수 있다.
- 블로그 게시물과 댓글들
- 네트워크 연결 오류 스낵바
- 유저가 클릭시 보여지는 버튼 애니메이션
Android의 UI Update
Android 에서 UI 업데이트는 대게 위와 같은 과정을 따라 수행된다.
- 유저에 의해, 혹은 어떤 식으로든 이벤트가 발생한다. (버튼 클릭, 게시물 추가, 댓글 추가 등등)
- 이벤트에 의해 State 가 변경됨
- 변경된 State 를 보여준다 (새로운 State 를 화면에 표시한다)
최근 Android의 추세는 이런 State를 ViewModel 에서 관리하고, View 나 Activity 에서 사용자에 의해 이벤트가 발생하면, ViewModel 에서 State를 업데이트 하는 형식으로 구성하고 있다. View 나 Activity 는 ViewModel 의 State (LiveData, State, StateFlow) 를 관찰하면서, State 가 변경되면 자동으로 변경된다.
Undirectional Data Flow
Undirectional Data Flow 란 단방향 데이터 흐름을 뜻하는데, State 와 Event 가 단방향으로 전달되는 것을 뜻한다.
예를 들어 기존 View 기반의 UI 시스템에서 EditText를 생각해보자. EditText는 사용자의 입력에 따라 Event 를 발생시키고, 이것을 ViewModel에 전달한다. ViewModel 은 EditText 이벤트를 기반으로 String value(State) 를 변경시키고, 변경된 State 가 View에 전달된다. 이것을 그림으로 표현하면 다음과 같다.
이 패턴을 Undirectional data flow 라 명하는데, 다음과 같은 이점을 얻을 수 있다
- 테스트 용이성 - 상태를 UI와 분리함으로써, ViewModel 과 View 를 테스트하기가 쉬워짐
- State encapsulation - State 는 ViewModel 에서만 업데이트 될 수 있기 때문에, UI의 규모가 커져도 일관적인 상태를 유저에게 보여줄 수 있음
- UI 일관성 - Observable State Holder가 변경된 State를 바로바로 사용자에게 보여준다.
Compose and State
Compose 에선 @Composable 어노테이션이 붙은 함수로 뷰를 구성할 수 있다. Composable 함수는 매개변수를 받을 수 있는데, 이로 인해 외부에서 데이터를 받아서 그 데이터를 사용자에게 보여줄 수 있다.
ComposeExample이라는 Composable 함수는 매개변수로 문자열을 받아, 화면에 출력해준다. 이 함수는 단순히 외부에서 데이터 를 받아 그것을 출력해주는 역할을 하고, name 과 관련해 어떤 연산도 하지 않는다. 따라서 이런 Composable 함수를 stateless 하다고 한다. 즉 State 를 직접 바꾸지 않는다는 뜻이다.
Composable 함수는 뷰가 제일 처음 구성될 때 한 번 호출되고, 이후 해당 Composable 과 관련된 State 가 바뀔 때마다 다시 호출된다.
이것을 recomposition 이라고 하는데, 아주 중요한 개념이니 기억해두도록 하자. 어쨌든 Composable을 상태에 따라 recomposition 시킬 수 있는 데이터는 State<T> 클래스고, 제네릭 타입인 T 는 무슨 데이터를 State 로 관리할지를 의미한다.
State 를 바꾸지 않고선 Composable 로 그려진 UI 를 다시 업데이트하는 것이 아예 불가능하다.
이해를 위해 다음과 같이 동작하는 Compose UI 를 구현해 보겠다.
- EditText 로 사용자로부터 문자열을 입력받는다
- EditText의 문자열을 가지고 안녕하세요 $EditText 님 이라는 텍스트뷰를 출력한다.
@Composable
fun ComposeExample() {
var name = ""
Column {
Text("$name 님 안녕하세요")
TextField(value = name, onValueChange = { name = it })
}
}
name 이라는 문자열을 EditText 역할을 하는 TextField에 넘겨주었다. 그러나 키보드를 겁나 뚜들겨도 아무 일도 일어나지 않을 것이다.
이유는 TextField 에서 표시할 문자열인 name이 단순한 문자열이기 때문이다. Composable 을 다시 호출할 수 있는 방법은 State 를 변경하는 것이라 했기 때문에, 이 name 문자열을 State 로 감싸보자.
@Composable
fun ComposeExample() {
var name = mutableStateOf("")
Column {
Text("$name 님 안녕하세요")
TextField(value = name.value, onValueChange = { name.value = it })
}
}
name 변수를 변경 가능한 State 로 감쌌다. 그러나 이번엔 컴파일러에 의해 mutableStateOf() 부분에 에러가 뜬다. 그 이유가 뭘까 ?
일단 name 이라는 변수는 어떻게 보면 ComposeExample 이라는 함수의 지역변수다. 함수를 다시 호출하면 지역변수도 처음부터 다시 초기화되기 때문에, 사실 위 코드가 실행되서 아무런 값이나 입력하더라도 화면에 변화는 없을 것이다.
Compose 에선 지역변수로 선언되는 state 를 계속해서 저장하기 위해 remember 이라는 함수를 제공한다. remember 블록안에 변수 타입을 넣으면, Compose UI Tree 에서 해당 remember 변수를 위한 저장공간이 생기게 되고, Composable 함수가 재호출되더라도 remember 로 기억하는 변수들은 항상 이전의 값을 유지한다.
@Composable
fun ComposeExample() {
var name = remember { mutableStateOf("") }
Column {
Text("$name 님 안녕하세요")
TextField(value = name.value, onValueChange = { name.value = it })
}
}
이제 remember 함수로 문자열 State를 생성했다. TextField가 잘 동작하는 것을 확인할 수 있다.
rememberSaveable
Android 는 화면 회전이나, 화면 설정 변경등과 같은 이벤트가 발생하면 Activity 를 파괴하고 재생성한다. 이것은 Compose 에서도 마찬가지로, 우리가 위에서 remember 함수로 State 를 감싸도 화면 회전과 같은 이벤트로 Activity 가 아예 재생성된다면 모든 값들은 초기화된다.
Compose 에서는 위 문제를 해결하기 위해서 rememberSaveable 이란 함수를 제공한다. 사용방법은 remember 와 똑같은데, rememberSaveable 로 감싼 데이터는 액티비티가 재생성되어도 유지된다.
@Composable
fun ComposeExample() {
var name = rememberSaveable { mutableStateOf("") }
Column {
Text("${name.value} 님 안녕하세요")
TextField(value = name.value, onValueChange = { name.value = it })
}
}
'안드로이드' 카테고리의 다른 글
Retrofit Interceptor 로 response 타입을 원하는 대로 바꿔보자 (0) | 2022.02.04 |
---|---|
Jetpack Compose Staggered Grid Layout (0) | 2022.01.23 |
Comment