Jetpack Compose State

State 의 정의


어플리케이션에서 State란 시간이 지남에 따라 계속 변화할 수 있는 모든 값을 의미한다. state 는 Room database 에 저장된 값일 수도 있고, 클래스의 프로퍼티일 수도 있다.

 

Android Application 을 예로 들자면 다음과 같은 것들이 모두 상태를 유저에게 보여주는 것이라고 할 수 있다.

  1. 블로그 게시물과 댓글들
  2. 네트워크 연결 오류 스낵바
  3. 유저가 클릭시 보여지는 버튼 애니메이션

 

Android의 UI Update


Android UI Update Loop

Android 에서 UI 업데이트는 대게 위와 같은 과정을 따라 수행된다.

 

  1. 유저에 의해, 혹은 어떤 식으로든 이벤트가 발생한다. (버튼 클릭, 게시물 추가, 댓글 추가 등등)
  2. 이벤트에 의해 State 가 변경됨
  3. 변경된 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 라 명하는데, 다음과 같은 이점을 얻을 수 있다 

 

  1. 테스트 용이성 - 상태를 UI와 분리함으로써, ViewModel 과 View 를 테스트하기가 쉬워짐
  2. State encapsulation - State 는 ViewModel 에서만 업데이트 될 수 있기 때문에, UI의 규모가 커져도 일관적인 상태를 유저에게 보여줄 수 있음
  3. 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 })
    }
}