asStateFlow() 사용해야 하는 이유

홍성덕·2024년 8월 9일
class SearchViewModel(
    private val dao: FavoriteDao
) : BaseViewModel() {

    private val _searchedItems = MutableStateFlow(listOf<DocumentEntity>())
    val searchedItems: StateFlow<List<DocumentEntity>> = _searchedItems.asStateFlow()

보통 ViewModel에서 StateFlow를 사용할 때 이런식으로 StateFlow 타입의 읽기 전용 변수를 선언하여 외부에서 사용한다. 그런데 문득 asStateFlow() 함수를 꼭 사용해야 할까? 라는 생각이 들었다. 어차피 val 키워드로 선언해주었고, StateFlow 타입을 명시를 해주었는데 왜 asStateFlow()를 사용해주어야 하는걸까?


private val _searchedItems = MutableStateFlow(listOf<DocumentEntity>())
val searchedItems: StateFlow<List<DocumentEntity>> = _searchedItems

만약 이렇게 asStateFlow()를 사용 안해주게 되면 이것은 MutableStateFlow 타입의 _searchedItems 인스턴스가 단순히 StateFlow 타입으로 업캐스팅되었을 뿐이다. 그렇다는 것은 외부에서 MutableStateFlow로 다운캐스팅을 해주어 값을 변경하는 것이 가능하다는 뜻이다.

asStateFlow()를 사용하면 이와 같은 일이 방지 가능하다. 잠깐 asStateFlow()가 어떤 함수인지 함수의 선언문을 살펴보자.

public fun <T> MutableStateFlow<T>.asStateFlow(): StateFlow<T> =
    ReadonlyStateFlow(this, null)
    
// ...
    
private class ReadonlyStateFlow<T>(
    flow: StateFlow<T>,
    @Suppress("unused")
    private val job: Job? // keeps a strong reference to the job (if present)
) : StateFlow<T> by flow, CancellableFlow<T>, FusibleFlow<T> {
    override fun fuse(context: CoroutineContext, capacity: Int, onBufferOverflow: BufferOverflow) =
        fuseStateFlow(context, capacity, onBufferOverflow)
}

asStateFlow()를 ReadonlyStateFlow 타입의 객체로 바뀌고 ReadonlyStateFlow는 StateFlow interface를 구현하는 클래스인 것을 알 수 있다. 즉 MutableStateFlow 타입으로 다운캐스팅이 불가능해진 것이다.


ViewModel 파일이 아닌 외부에서 MutableStateFlow로 다운캐스팅을 시도했을 때 차이점을 바로 알 수 있다.

class SearchFragment : BaseFragment<FragmentSearchBinding>(
    layoutId = R.layout.fragment_search,
    binder = FragmentSearchBinding::bind
) {
	// ...

    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
        super.onViewCreated(view, savedInstanceState)

		// 다운캐스팅 시도
        (viewModel.searchedItems as MutableStateFlow).value = emptyList()
        
        // ...

만약 위와 같이 해당 ViewModel과 연결된 프래그먼트에서 as MutableStateFlow로 다운캐스팅을 하여 값 변경을 시도했을 때의 어떤 결과가 나오는지 실험해보았다.

실험 결과 asStateFlow()를 사용하지 않고 단순히 업캐스팅만 해주었을 때는 위의 코드가 잘 동작하여 emptyList()로 값이 변경된다.
하지만 asStateFlow()를 사용하였더니

java.lang.ClassCastException: kotlinx.coroutines.flow.ReadonlyStateFlow cannot be cast to kotlinx.coroutines.flow.MutableStateFlow

위와 같이 런타임에서 ClassCastException이 발생하였다.

이렇게 외부에서 값을 변경하지 않도록 좀더 확실히 제한을 두는 차원에서 asStateFlow()를 사용해야 한다는 것을 알게 되었다.

profile
안드로이드 주니어 개발자

0개의 댓글