์๋ ํ์ธ์ ๐
์ด์ ํฌ์คํ ์์ 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๋ฅผ ํ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์๋ณด์์ต๋๋ค.
์ด ํฌ์คํ ์ด ์ฌ๋ฌ๋ถ์๊ฒ ๋ง์ ๋์์ด ๋์์ผ๋ฉด ์ข๊ฒ ๋ค์! ๐
์ค๋๋ ์ฆ์ฝํ์ธ์ :)