반응형

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 꾹 눌러주세요!
감사합니다.

반응형
반응형