[Android] DataBinding + StateFlow + Sealed class 예제
Android Studio Arctic fox 버전부터 AAC DataBinding 에 Kotlin Coroutines 에서 제공하는 StateFlow 를 결합할 수 있게 되었습니다. 이로 인해 코틀린 코루틴을 사용하여 개발하던 개발자는 이제 LiveData 에 대한 의존성 없이도 데이터바인딩을 사용하는 것이 가능해졌습니다.
LiveData 와 StateFlow 에 대한 비교에 대해서는 제가 이전에 작성한 글을 참고하시면 되겠습니다.
[Android] LiveData VS StateFlow, 왜 StateFlow 를 써야 할까?
StateFlow
StateFlow 는 굉장히 심플한 컨셉을 갖고 있습니다. 말 그대로 하나의 상태 값을 반드시 들고 있는 인터페이스라고 보시면 되는데요. 실제 내부 구현도 아래와 같이 간단하게 구현되어 있습니다.
public interface StateFlow<out T> : SharedFlow<T> {
/** * The current value of this state flow. */
public val value: T
}
SharedFlow 에 대해서는 추후 이벤트 처리와 관련하여 별도 포스팅으로 다루도록 하겠습니다.
위 StateFlow 는 기본적으로 read-only 이기 때문에 값을 수정하기 위해서는 MutableStateFlow 로 선언하여 사용하면 되겠습니다.
class CounterModel {
private val _counter = MutableStateFlow(0) // private mutable state flow
val counter = _counter.asStateFlow() // publicly exposed as read-only state flow
fun inc() {
_counter.value++
}
}
DataBinding
AAC DataBinding 은 레이아웃과 데이터 사이의 다리 역할을 하는 프레임워크입니다. 데이터바인딩은 개발자가 작성해야 하는 보일러 플레이트 코드를 확연히 줄여주고, 개발자로 하여금 데이터를 처리하는 로직에만 집중할 수 있게 해 줘 생산성을 향상해줍니다.
데이터바인딩은 lifecycle 을 알고 있기 때문에 UI 가 화면에 보이는 경우에만 트리거 됩니다.
DataBinding With StateFlow
이번에는 StateFlow 와 DataBinding 을 함께 사용해보도록 하겠습니다.
class ViewModel() {
val username: StateFlow<String>
}
//code in xml
<TextView
android:id="@+id/name"
android:text="@{viewmodel.username}" />
보시다시피 LiveData 를 사용했을 때와 큰 차이점은 없어보입니다.(Two-way 바인딩도 마찬가지입니다.)
따라서 레이아웃에서 어떻게 처리하냐 보다는 StateFlow 를 어떻게 구성하는지 잘 아는 것이 관건인 것 같습니다.
관련해서는 StateFlow 가 결국 코루틴 플로우의 하위 타입이기 때문에 Flow 와 관련하여 제공되는 다양한 API 를 활용하면 좋습니다. (참고)
sealed class 와 함께 사용하기
이번에는 조금 심화된 버전으로 sealed 클래스를 활용하여 하나의 StateFlow 객체를 통해 여러 가지 상태의 데이터를 처리하는 예제에 대해 살펴보겠습니다.
예제는 제가 리그오브레전드 API 를 통해 작성한 샘플 앱을 기반으로 가져왔습니다.
sealed class UiState {
object Loading: UiState()
data class Success<T>(val data: T): UiState()
data class Error(val error: Throwable?): UiState()
}
UiState 클래스는 Loading, Success, Error 3가지 상태가 있습니다.
다음은 ViewModel 클래스에서 위에서 정의한 UiState 타입을 갖는 StateFlow 를 선언하겠습니다. Repository 의 세부 구현 사항은 포스트에 포함하지 않고, 전체 코드가 궁금하신 분은 Github 링크를 참고해주시면 되겠습니다.
class MainViewModel constructor(
private val mainRepository: MainRepository
): ViewModel() {
val uiState: StateFlow<UiState> = mainRepository.getAllChampions()
.stateIn(
scope = viewModelScope,
started = SharingStarted.WhileSubscribed(5000L),
initialValue = UiState.Loading
)
}
위 예제 코드를 보면 stateIn 이라는 함수를 통해 flow builder 를 StateFlow 로 변환해주었고 uiState 의 값을 초기값으로 Loading 값을 설정해준 다음, 가져오는 데이터의 결과에 따라 UiState.Success 나 UiState.Error 값으로 변환해주도록 하였습니다.
그런 다음 바인딩 어댑터를 아래와 같이 선언하고 레이아웃에 연결하여 주면 됩니다.
@BindingAdapter("show")
fun ProgressBar.bindShow(uiState: UiState) {
visibility = if (uiState is UiState.Loading) View.VISIBLE else View.GONE
}
@BindingAdapter("toast")
fun View.bindToast(uiState: UiState) {
if (uiState is UiState.Error) {
uiState.error?.message?.let { errorMessage ->
Toast.makeText(context, errorMessage, Toast.LENGTH_SHORT).show()
}
}
}
@BindingAdapter("championItems")
fun RecyclerView.bindChampionItems(uiState: UiState) {
val boundAdapter = this.adapter
if (boundAdapter is ChampionAdapter && uiState is UiState.Success<*>) {
boundAdapter.submitList(uiState.data as List<Champion>)
}
}
전체 코드가 궁금하신 분은 Github 저장소를 보시면 되겠습니다.
도움이 되었다면 저장소에 STAR⭐ 꾹 눌러주세요!
감사합니다.