๊พธ์ค€ํ•จ์ด ์ง„๋ฆฌ๋‹ค!!

์–ด์ œ๋ณด๋‹ค ๋ฐœ์ „ํ•œ ์˜ค๋Š˜์ด ๋˜๊ณ ํ”ˆ ๐Ÿง‘๐Ÿปโ€๐Ÿ’ป ์˜ ๋ธ”๋กœ๊ทธ

Android/Kotlin

[Kotlin][Android] SharedFlow์™€ StateFlow ํ™œ์šฉํ•˜๊ธฐ (feat. UIState ํ™œ์šฉํ•˜์—ฌ ๋ฐ์ดํ„ฐ ํ‘œ์‹œํ•˜๊ธฐ)

๋ށ์š” 2023. 7. 5. 15:59

์•ˆ๋…•ํ•˜์„ธ์š” ๐Ÿ‘‹

 

์ด์ „ ํฌ์ŠคํŒ…์—์„œ LiveData๋ฅผ Coroutine Flow๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์„ ์•Œ์•„๋ณด์•˜๋Š”๋ฐ์š”

๊ด€๋ จ ํฌ์ŠคํŒ…์€ ์•„๋ž˜ ํฌ์ŠคํŒ…์„ ์ฐธ๊ณ ํ•ด ์ฃผ์„ธ์š” :)

 

 

[Kotlin][Android] ์•ˆ๋“œ๋กœ์ด๋“œ Coroutine Flow ์ ์šฉํ•˜๊ธฐ (2) ( LiveData๋ฅผ Flow๋กœ ๋ณ€๊ฒฝํ•˜๊ธฐ : LiveData ์™€ Flow ๋น„๊ต

์•ˆ๋…•ํ•˜์„ธ์š” ๐Ÿ‘‹ ์˜ค๋Š˜์€ ์ €๋ฒˆ ํฌ์ŠคํŒ…์— ์ด์–ด LiveData๋ฅผ Flow๋กœ ์ด์ „ํ•˜๋Š” ๊ณผ์ •์„ ํฌ์ŠคํŒ…ํ•ด๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค. ๋ฟ๋งŒ ์•„๋‹ˆ๋ผ, LiveData์™€ Flow์˜ ์ฐจ์ด์ ์— ๋Œ€ํ•ด์„œ๋„ ํ•จ๊ป˜ ๋‹ค๋ค„๋ณผ ์˜ˆ์ •์ž…๋‹ˆ๋‹ค ๐Ÿ˜ƒ ์ด์ „ ํฌ์ŠคํŒ…์€ ์•„

devyo-111commit.tistory.com

์˜ค๋Š˜์€ SharedFlow์™€ StateFlow๋ฅผ ํ™œ์šฉํ•˜์—ฌ ํ™”๋ฉด์„ ์–ด๋–ป๊ฒŒ ๊ตฌ์„ฑํ•˜๋Š”์ง€

UIState๋กœ ๊ด€๋ฆฌ๋ฅผ ์–ด๋–ป๊ฒŒ ํ•˜๋Š”์ง€ ํ•œ๋ฒˆ ์•Œ์•„๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค!

 

๋จผ์ € Data๋ฅผ ๋ฐ›์•„์˜ฌ๋•Œ ๊ฒฐ๊ณผ์— ๋Œ€ํ•œ Sealed class๋ฅผ ํ•˜๋‚˜ ๋งŒ๋“ค์–ด์ค๋‹ˆ๋‹ค.

 

sealed class DataResult<T> {
    data class Success<T>(val data: T) : DataResult<T>()
    data class Exception<T>(val message: String) : DataResult<T>()
}

์ด๋ฆ„ ๊ทธ๋Œ€๋กœ Success ๋•Œ๋Š” ์ •๋ณด๋ฅผ ์„ฑ๊ณต์ ์œผ๋กœ ๋ฐ›์•„์˜จ ์ƒํƒœ์ด๋ฉฐ Exception์ƒํƒœ๋Š” ์˜ˆ์™ธ๊ฐ€ ๋ฐœ์ƒํ•œ ์ƒํƒœ์ž…๋‹ˆ๋‹ค.

(์ƒํ™ฉ์— ๋”ฐ๋ผ Fail , Loading ๋“ฑ์„ ์ถ”๊ฐ€ํ•˜์—ฌ ๋งŽ์ด ์‚ฌ์šฉํ•˜๋‚˜ ์ €๋Š” ๊ฐ„๋‹จํ•œ ์˜ˆ์ œ์ด๊ธฐ์— ์˜ˆ์™ธ์ฒ˜๋ฆฌ์— ๋Œ€ํ•ด์„œ๋งŒ ์ •์˜ํ•˜์˜€์Šต๋‹ˆ๋‹ค.)


class MainRepositoryImpl @Inject constructor(
    private val userDao: UserDao,
) : MainRepository {
    override suspend fun insertUser(user: UserEntity) {
        userDao.addUser(user)
    }

    override suspend fun getAllUser() = flow<DataResult<List<UserEntity>>> {
        emit(DataResult.Success(userDao.getAllUser().toList()))
    }.catch { throwable ->
        emit(DataResult.Exception(throwable.message.toString()))
    }

    override suspend fun deleteAllUser() {
        userDao.deleteAllUser()
    }
}

 

์œ„ ์ฝ”๋“œ์—์„œ ๋ณด๋Š”๋ฐ”์™€ ๊ฐ™์ด getAllUser()๋ผ๋Š” ํ•จ์ˆ˜๋กœ Room์—์„œ ์ „์ฒด ์ €์žฅ๋œ ๊ฐ’์„ ๊ฐ€์ ธ์˜ค๋Š”๋ฐ

๊ฐ€์ ธ์˜ฌ ๋•Œ DataResult์— ๋‹ด๊ธด Flow๋กœ ๊ฐ’์„ return ํ•ด์ฃผ๋„๋ก ํ•˜์˜€์Šต๋‹ˆ๋‹ค.


sealed interface UiState {
    data class Loading(val list: List<UserEntity> = emptyList()) : UiState
    data class EmptyListState(val list: List<UserEntity> = emptyList()) : UiState
    data class ShowListState(val list: List<UserEntity>) : UiState
    data class OnErrorState(val message: String) : UiState
}

sealed interface ErrorEvent {
    data class Error(val errorMessage: String) : ErrorEvent
}

@HiltViewModel
class MainViewModel @Inject constructor(
    private val mainRepository: MainRepository,
) : ViewModel() {
	...
}

 

๋‹ค์Œ์œผ๋กœ๋Š” ViewModel์„ ๊ตฌ์„ฑํ•˜๋Š” ๋ฐฉ๋ฒ•์ž…๋‹ˆ๋‹ค.

์œ„์™€ ๊ฐ™์ด UIState๋ผ๋Š” sealed interface๋ฅผ ๊ฐ UI์ƒํƒœ์— ๋งž๊ฒŒ ์ •์˜๋ฅผ ํ•ด์ฃผ์—ˆ์Šต๋‹ˆ๋‹ค.

(ํ•ด๋‹น ๋ถ€๋ถ„๋„ ๊ฐ ์ƒํ™ฉ์— ๋งž๊ฒŒ ๊ตฌ์„ฑํ•ด ์ฃผ๋ฉด ๋˜๊ฒ ์Šต๋‹ˆ๋‹ค ๊ฐœ๋ฐœ์ž ์žฌ๋Ÿ‰!!)

 

ErrorEvent๋Š” ๋ฐœ์ƒํ•  ์ˆ˜ ์žˆ๋Š” ์—ฌ๋Ÿฌ Error์ƒํ™ฉ์— ๋Œ€ํ•ด ์ •์˜ํ•ด ๋†“์€ sealed interface๋กœ

๋งˆ์ฐฌ๊ฐ€์ง€๋กœ ๊ฐ ์ƒํ™ฉ์— ๋งž๊ฒŒ ๊ตฌ์„ฑ ํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค.

 

@HiltViewModel
class MainViewModel @Inject constructor(
    private val mainRepository: MainRepository,
) : ViewModel() {
    private val _uiState = MutableStateFlow<UiState>(UiState.Loading())
    val uiState: SharedFlow<UiState> = _uiState.asStateFlow()

    private val _errorEvent = MutableSharedFlow<ErrorEvent>()
    val errorEvent: SharedFlow<ErrorEvent> = _errorEvent.asSharedFlow()

    fun addUser(user: UserEntity) {
        viewModelScope.launch {
            mainRepository.insertUser(user)
        }
    }

    fun getAllUser() {
        viewModelScope.launch {
            mainRepository.getAllUser().collect { result ->
                when (result) {
                    is DataResult.Success -> {
                        if (result.data.isNotEmpty()) {
                            _uiState.value = UiState.ShowListState(result.data)
                        } else {
                            _uiState.value = UiState.EmptyListState()
                        }
                    }

                    is DataResult.Exception -> {
                        _uiState.value = UiState.OnErrorState(result.message)
                    }
                }
            }
        }
    }

    fun deleteAllUser() {
        viewModelScope.launch {
            mainRepository.deleteAllUser()
        }
    }

    /**
     * ์—๋Ÿฌ ๋ฐœ์ƒ์‹œ์— ํ•ด๋‹น ๋ถ€๋ถ„์„ ์ฒ˜๋ฆฌํ•˜๋Š” ํ•จ์ˆ˜๋กœ ์—ฌ๋Ÿฌ๊ฐ€์ง€ ๋™์ž‘์„ ์ •ํฌํ• ์ˆ˜ ์žˆ๋‹ค.
     * **/
    fun onError(message: String) {
        viewModelScope.launch {
            _errorEvent.emit(ErrorEvent.Error(message))
        }
    }
}

 

ViewModeld์—์„œ๋Š” UIState๋ฅผ ๋‚ด๋ณด๋‚ด๋Š” StateFlow๋ฅผ ๊ฐ€์ง€๊ณ 

๋ฐ์ดํ„ฐ๋ฅผ ํ˜ธ์ถœํ•˜์—ฌ ๊ฐ ์ƒํ™ฉ์— ๋งž๋Š” UIState๋ฅผ ๋„˜๊ฒจ์ค๋‹ˆ๋‹ค.


@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
    ...
    private fun collectData() {
        viewModel.run {
            lifecycleScope.launch {
                repeatOnLifecycle(Lifecycle.State.STARTED) {
                    launch {
                        uiState.collect { state ->
                            when (state) {
                                is UiState.Loading -> {
                                    binding.loading.isVisible = true
                                }

                                is UiState.ShowListState -> {
                                    binding.run {
                                        loading.isVisible = false
                                        rvUser.isVisible = true
                                        tvNoItem.isVisible = false
                                    }
                                    userAdapter.submitList(state.list)
                                }

                                is UiState.EmptyListState -> {
                                    binding.run {
                                        loading.isVisible = false
                                        rvUser.isVisible = false
                                        tvNoItem.isVisible = true
                                    }
                                }

                                is UiState.OnErrorState -> {
                                    onError(state.message)
                                }
                            }
                        }
                    }

                    launch {
                        errorEvent.collect { event ->
                            when (event) {
                                is ErrorEvent.Error -> {
                                    Toast.makeText(
                                        this@MainActivity,
                                        event.errorMessage,
                                        Toast.LENGTH_SHORT,
                                    ).show()
                                }
                            }
                        }
                    }
                }
            }
        }
    }
}

๊ฐ Flow๋Š” Activity์—์„œ collectํ•ด์ค๋‹ˆ๋‹ค.

UIState๋Š” ์ƒํ™ฉ์— ๋งž๊ฒŒ UI๋ฅผ ์—…๋ฐ์ดํŠธ ํ•˜๊ณ , SharedFlow๋Š” ์ „๋‹ฌ๋ฐ›์€ Event์— ๋”ฐ๋ผ ์ฒ˜๋ฆฌํ•ด์ฃผ๋ฉด ๋ฉ๋‹ˆ๋‹ค. ๐Ÿ˜Ž

 

์—ฌ๊ธฐ์„œ ์•Œ ์ˆ˜ ์žˆ๋“ฏ StateFlow๋Š” ๋ง๊ทธ๋Œ€๋กœ ์ƒํƒœ๋ฅผ ์ €์žฅํ•˜๊ณ  ์žˆ๋Š” Flow์ด๊ณ 

SharedFlow๋Š” ์ฃผ๋กœ ์ด๋ฒคํŠธ๋ฅผ ์ „๋‹ฌํ• ๋•Œ ์‚ฌ์šฉํ•˜๋Š” Flow๋กœ ๋ณด๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™์Šต๋‹ˆ๋‹ค.

 

ShredFlow์˜ ๊ฒฝ์šฐ replay , extraBufferCapacity , OnBufferOverflow์˜ ์˜ต์…˜์ด ์กด์žฌํ•˜๋ฉฐ ๊ฐ ์„ค๋ช…์€ ์•„๋ž˜ ํ‘œ๋กœ ํ™•์ธ ๋ถ€ํƒ๋“œ๋ฆฝ๋‹ˆ๋‹ค.

์ด๋ฆ„ ์„ค๋ช…
replay ๊ธฐ๋ณธ๊ฐ’์ด 0์œผ๋กœ ์ƒˆ๋กœ์šด ๊ตฌ๋…์ž๋“ค์—๊ฒŒ
์ด์ „ ์ด๋ฒคํŠธ๋ฅผ ๋ช‡๊ฐœ๋ฅผ ๋ฐฉ์ถœํ• ์ง€ ์ง€์ •ํ•˜๋Š” ์˜ต์…˜
(0์œผ๋กœ ๋˜์–ด์žˆ์„ ๊ฒฝ์šฐ์—๋Š” ๊ฐ€์žฅ ๋งˆ์ง€๋ง‰์— ์ €์žฅ๋œ ์ด๋ฒคํŠธ๊ฐ€ ๋ฐฉ์ถœ๋จ
์ฆ‰, StateFlow์™€ ๊ฐ™์€ ๋™์ž‘์ด ๋œ๋‹ค.)
extraBufferCapacity ๋ฒ„ํผ ์ €์žฅ ํฌ๊ธฐ๋ฅผ ์ง€์ •ํ•˜๋Š” ์˜ต์…˜
OnBufferOverflow ๋ฒ„ํผ ์ €์žฅํฌ๊ธฐ๋ฅผ ์ดˆ๊ณผ์‹œ ํ•  ๋™์ž‘

BufferOverflow.SUSPEND 
๋ฒ„ํผ๊ฐ€ ๊ฐ€๋“ ์ฐฌ ๊ฒฝ์šฐ ๋ฒ„ํผ์— ์—ฌ์œ ๊ณต๊ฐ„์ด ์ƒ๊ธธ๋•Œ ๊นŒ์ง€ suspend ํ•œ๋‹ค.

BufferOverflow.DROP_LATEST
๋ฒ„ํผ๊ฐ€ ๊ฐ€๋“ ์ฐฌ ๊ฒฝ์šฐ ๊ฐ€์žฅ ์ตœ๊ทผ ๋ฐ์ดํ„ฐ๋ฅผ drop ํ•˜๊ณ  ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฒ„ํผ์— ๋„ฃ๋Š”๋‹ค.

BufferOverflow.DROP_OLDEST 
๋ฒ„ํผ๊ฐ€ ๊ฐ€๋“ ์ฐฌ ๊ฒฝ์šฐ ๊ฐ€์žฅ ์˜ค๋ž˜๋œ ๋ฐ์ดํ„ฐ๋ฅผ dropํ•˜๊ณ  ์ƒˆ๋กœ์šด ๋ฐ์ดํ„ฐ๋ฅผ ๋ฒ„ํผ์— ๋„ฃ๋Š”๋‹ค.

 

์ด์ƒ์œผ๋กœ StateFlow์™€ SharedFlow๋ฅผ ํ™œ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ณด์•˜์Šต๋‹ˆ๋‹ค.

์ด ํฌ์ŠคํŒ…์ด ์—ฌ๋Ÿฌ๋ถ„์—๊ฒŒ ๋งŽ์€ ๋„์›€์ด ๋˜์—ˆ์œผ๋ฉด ์ข‹๊ฒ ๋„ค์š”! ๐Ÿ˜ƒ

 

์˜ค๋Š˜๋„ ์ฆ์ฝ”ํ•˜์„ธ์š” :)