[Compose] TextField의 state를 관리하는 방법
다음 글을 읽고 정리하였습니다.
텍스트를 동기적으로 갱신해야 한다
``TextField``는 내부적으로 state를 3곳에 저장한다.
- Keyboard: 맞춤법 검사나 다음 단어 추천 등의 로직을 수행하기 위해 state를 저장한다.
- State holder: 사용자가 정의한 state이다. ``mutableStateOf("state")`` 등이 해당된다.
- Internal: Keyboard와 state holder의 값을 동기화하는 역할을 맡는다.
우리는 3개의 사본 중 state holder만 관리하면 된다. 나머지는 안드로이드가 관리해줄 것이다.
주의할 점은 State를 즉시 갱신해야 한다는 점이다. 아래 그림처럼 비동기 작업을 수행한 후 갱신하면 안 된다.
이렇게 하면 Keyboard, Internal, State holder의 값이 일치하지 않는 문제가 생긴다. 키보드 자체 로직을 적용하거나 타이핑을 빨리 하는 과정에서 에러가 더 많이 발생할 수 있다.
요약하면 새로 입력된 값을 갱신하기 전에 비동기 작업을 수행하면 안 된다는 것이다. 비동기 작업을 수행하고 싶다면 state를 갱신한 후에 수행해야 한다. 이렇게.
var username by mutableStateOf("")
private set
fun updateUsername(newUsername: String) {
username = newUsername // 먼저 업데이트한 후
viewModelScope.launch { ... } // 비동기 수행
}
Synchronous 작업은 갱신하기 전에 수행해도 된다. 핵심은 비동기이기 때문.
``Flow`` 대신 ``MutableState``를 사용하자
위에서 텍스트를 즉시 갱신해야 한다고 말했다. 따라서 TextField의 상태를 나타낼 때 ``Flow`` 대신 synchronous state를 사용해야 한다. 위 글에서는 ``MutableState``를 권장하고 있다.
class HomeViewModel : ViewModel() {
var username by mutableStateOf("")
private set
// ...
}
나는 ViewModel에서 Android API를 최대한 사용하지 않기 위해 ``StateFlow``를 사용했었는데, UI와 관련 없는 Compose API 정도는 사용해도 되는 듯하다. 구글 개발자들도 사용하는 걸 보니.
``StateFlow``를 사용하고 싶다면, ``Main``에서 업데이트 로직을 최대한 빨리 수행하기 위해 ``Dispatchers.Main.immediate``를 사용해야 한다. 그런데 코루틴을 잘 모른다면 그냥 ``MutableState``를 쓰는 게 좋다. 글에서는 이렇게 언급하고 있다.
* Since the collection is synchronous, it’s possible the UI is in a not-ready-to-operate-on state when it happens.
* Interferes with Compose’s threading and rendering phases, amongst other things, because it assumes that recomposition happens on the Main thread.
동기적으로 갱신하기만 한다면, State를 선언하는 위치는 아무래도 상관없다. 비즈니스 로직을 적용해야 한다면 ``ViewModel``에 선언해야 하고, 별도의 state holder 클래스를 선언해도 된다. 일반적으로 state는 최대한 state를 사용하는 곳에 가깝게 선언해야 한다.
결론
``TextField``의 state는 반드시 동기적으로 갱신되어야 하며, ``Flow`` 등 비동기적인 객체를 사용하여 선언해서는 안 된다. 이제 ``State``를 각각 비동기적으로, 동기적으로 변환하는 ``snapshotFlow { ... }`` 와 ``derivedStateOf { ... }`` 함수를 활용하여 여러 state를 선언해 보자.
class HomeViewModel : ViewModel() {
var username by mutableStateOf("")
private set
// 새로운 state를 비동기적으로 계산
val isValidUsername: StateFlow<Boolean> =
snapshotFlow { username }
.mapLatest { ... }
// 새로운 state를 동기적으로 계산
val isValidUsername2 by derivedStateOf { ... }
}