StateFlow vs LiveData

์†Œ์ •ยท2025๋…„ 5์›” 29์ผ
0

Kotlin

๋ชฉ๋ก ๋ณด๊ธฐ
40/40

1.StateFlow vs LiveData

ํ•ญ๋ชฉStateFlowLiveData
๋ฐ์ดํ„ฐ ๋ฐฉ์‹StateFlow / SharedFlow (์ฝ”๋ฃจํ‹ด ๊ธฐ๋ฐ˜)LiveData (Lifecycle-aware)
์‹คํ–‰ ์‹œ์ ํ•ญ์ƒ ์ตœ์‹ ๊ฐ’์„ ์œ ์ง€ํ•˜๊ณ , collectํ•˜๋Š” ์ˆœ๊ฐ„ ๋ฐ”๋กœ ์ „๋‹ฌ/ ViewModel ์•ˆ์—์„œ ๊ณ„์† ๋Œ์•„๊ฐ€๊ณ , collect๋งŒ ์žˆ์œผ๋ฉด ์–ธ์ œ๋“  ๋ฐ˜์‘Observer๊ฐ€ ํ™œ์„ฑ ์ƒํƒœ์ผ ๋•Œ๋งŒ ์ž๋™ ๋ฐ˜์˜/ LiveData๋Š” onStart()~onStop() ์‚ฌ์ด๋งŒ ๋ฐ˜์‘
์„ ์–ธ ๋ฐฉ์‹MutableStateFlow, MutableSharedFlow ์‚ฌ์šฉMutableLiveData ์‚ฌ์šฉ
๊ด€์ฐฐ ๋ฐฉ๋ฒ•collect ์‚ฌ์šฉ (์˜ˆ: lifecycleScope.launch { flow.collect {} })observe ์‚ฌ์šฉ (์˜ˆ: liveData.observe(...))
- ๋น„๋™๊ธฐ ์ŠคํŠธ๋ฆผ ์ฒ˜๋ฆฌ์— ์ ํ•ฉ- Android ์ƒ๋ช…์ฃผ๊ธฐ ์ž๋™ ๊ด€๋ฆฌ
ํŠน์ง•- Kotlin Coroutines ์นœํ™”์ - UI ์ปจํŠธ๋กค๋Ÿฌ์— ์ตœ์ ํ™”
- ์ค‘๋ณต ์ด๋ฒคํŠธ ๋ฐฉ์ง€ ์œ ์—ฐํ•จ (SharedFlow)
ViewModel-UI ์—ฐ๊ฒฐ ๋ฐฉ์‹Flow๋กœ ๋น„๋™๊ธฐ ์—ฐ์†์„ฑ / ์ƒํƒœ ๋ณ€๊ฒฝ ๋Œ€์‘UI ์ค‘์‹ฌ์˜ ๋‹จ์ผ ์‘๋‹ต ์ฒ˜๋ฆฌ์— ์ ํ•ฉ
๋ฐ์ดํ„ฐ ๋ˆ„์  ์ฒ˜๋ฆฌ๊ธฐ์กด ๋ฐ์ดํ„ฐ๋ฅผ ๋ˆ„์ ํ•ด์„œ ์ €์žฅ / .update๋กœ ๊ฐ„ํŽธ์ด์ „ ๊ฐ’์„ ๋ฎ์–ด์”€ (๋ˆ„์  X) / ๋ˆ„์ ํ•˜๋ ค๋ฉด ๋ณ„๋„ ๋กœ์ง ํ•„์š” (ex. .value = value + ์ƒˆ ํ•ญ๋ชฉ)

1-1) StateFlow

๐Ÿ’ก ํ•ต์‹ฌ

  • StateFlow๋Š” ํ•ญ์ƒ ํ˜„์žฌ ๊ฐ’์„ ๊ฐ€์ง€๊ณ  ์žˆ๋Š” ๋ฐ์ดํ„ฐ ํ๋ฆ„(Flow)
  • ๊ฐ’์ด ๊ณ„์† ๋ฐ”๋€Œ๋ฉด, ๊ณ„์† ๋ฐ˜์‘ํ•จ (์‹ค์‹œ๊ฐ„์œผ๋กœ)
  • Activity/Fragment๊ฐ€ ๊บผ์ ธ ์žˆ์–ด๋„ ๋ฐ์ดํ„ฐ๋Š” ๊ณ„์† ์œ ์ง€๋จ
  • ์ง€์†์ ์œผ๋กœ ๋ฐ”๋€Œ๋Š” ์ƒํƒœ๋ฅผ ๋ณด์—ฌ์ค„ ๋•Œ ์‚ฌ์šฉ
  • StateFlow๋Š” ๊ผญ ์ปฌ๋ ‰์…˜(collect)๊ณผ ํ•จ๊ป˜ ์จ์•ผ ํ•˜๋Š” ๊ฑด ์•„๋‹˜ ํ•˜์ง€๋งŒ ๋Œ€๋ถ€๋ถ„ ๊ฒฝ์šฐ์— collect๋ฅผ ์จ์„œ ๊ฐ’์„ ๋ฐ›์•„์˜ด โ†’ ์‹ค์‹œ๊ฐ„์œผ๋กœ ๋ฐ”๋€Œ๋Š” ๊ฐ’์„ ๊ด€์ฐฐํ•  ๋•Œ ์‚ฌ์šฉํ•˜๊ธฐ ๋•Œ๋ฌธ
  • stateFlow.value : ์ง€๊ธˆ ํ˜„์žฌ ๊ฐ’ โ†’ ๊ทธ๋ƒฅ ๊บผ๋‚ด ์“ฐ๋Š” ๊ฒƒ
    stateFlow.collect { ... } : ๊ฐ’์ด ๋ฐ”๋€” ๋•Œ๋งˆ๋‹ค ํ•˜๊ณ  ์‹ถ์€ ๋™์ž‘ โ†’ ํ๋ฆ„ ๊ฐ์ง€

๐Ÿ“์˜ˆ์ œ

class ContentsViewModel : ViewModel() {
    private val repository = ContentsRepository()

    private val _contentsGenreListStateFlow = MutableStateFlow<List<ContentsGenreData>>(emptyList())
    val contentsGenreListStateFlow: StateFlow<List<ContentsGenreData>> = _contentsGenreListStateFlow

    private val _contentsGenreErrorFlow = MutableSharedFlow<String>()
    val contentsGenreErrorFlow: SharedFlow<String> = _contentsGenreErrorFlow

    private val _contentsListStateFlow = MutableStateFlow<Map<String, List<String>>>(emptyMap())
    val contentsListStateFlow: StateFlow<Map<String, List<String>>> = _contentsListStateFlow

    private val _contentsListErrorFlow = MutableSharedFlow<String>()
    val contentsListErrorFlow: SharedFlow<String> = _contentsListErrorFlow

    fun getGenreList() {
        viewModelScope.launch {
            try {
                val response = repository.getGenreList()
                if (response.isSuccess) {
                    _contentsGenreListStateFlow.value = response.getOrThrow().data.genre
                } else {
                    _contentsGenreErrorFlow.emit("์žฅ๋ฅด ๋ชฉ๋ก ๋ถˆ๋Ÿฌ์˜ค๊ธฐ ์‹คํŒจ")
                }
            } catch (e: Exception) {
                _contentsGenreErrorFlow.emit("์—๋Ÿฌ: ${e.message}")
            }
        }
    }

    fun getContentsList(genreCode: String) {
        viewModelScope.launch {
            try {
                val response = repository.getContentsList(genreCode)
                if (response.isSuccess) {
                    val contentsMap = response.getOrThrow().data.contents.associate {
                        it.contentsName to listOf(it.contentsCode, it.developmentalElementName)
                    }
                    _contentsListStateFlow.update { it + contentsMap } // ๋ˆ„์  ์ €์žฅ
                } else {
                    _contentsListErrorFlow.emit("์ฝ˜ํ…์ธ  ๋ชฉ๋ก ์‹คํŒจ (์žฅ๋ฅด์ฝ”๋“œ: $genreCode)")
                }
            } catch (e: Exception) {
                _contentsListErrorFlow.emit("์—๋Ÿฌ: ${e.message}")
            }
        }
    }

}


### ๋ทฐ์—์„œ
contentsViewModel = ViewModelProvider(this)[ContentsViewModel::class.java]
 
Common.getGenreCode(requireContext(),binding.tvTitle.text.toString())
            ?.let {
                Log.d("SmartFragment","ํ™•์ธ : $it")
                Common.GENRE_CODE = it
                contentsViewModel.getContentsList(it)
            } 
 
private fun observeContents() {
        // ์ฝ˜ํ…์ธ  ๋ฆฌ์ŠคํŠธ ์ˆ˜์‹ 
        lifecycleScope.launch {
            repeatOnLifecycle(Lifecycle.State.STARTED) {
                contentsViewModel.contentsListStateFlow.collect { contentsMap ->
                    Common.CONTENTS_LIST_MAP.clear()
                    Common.CONTENTS_LIST_MAP.putAll(contentsMap)
                    // UI ๋ฐ˜์˜ํ•˜๊ฑฐ๋‚˜ ๋กœ๊ทธ ์ฐ๊ธฐ ๋“ฑ ํ•„์š” ์ž‘์—… ์ˆ˜ํ–‰
                    Log.d("SmartFragment","SmartFragment => ${contentsMap}")
                    if (contentsMap.isNotEmpty()) {
                        withContext(Dispatchers.Main) {
                            attachMainStagePlayFragment(contentsMap)
                        }
                    }
                }
            }
        }
        lifecycleScope.launch {
            contentsViewModel.contentsListErrorFlow.collect {
                Toast.makeText(requireContext(), it, Toast.LENGTH_SHORT).show()
            }
        }
    }

๐Ÿ“Œ ์•ฑ์ด ์‹คํ–‰ ์ค‘์ธ ๋‚ด๋‚ด ๊ณ„์† ์ƒํƒœ๋ฅผ ์œ ์ง€ํ•˜๊ณ , ๋ˆ„๊ฐ€ ์ƒˆ๋กœ ๋“ค์–ด์™€๋„ ๊ฐ€์žฅ ์ตœ์‹  ์ƒํƒœ๋ฅผ ๋ณด์—ฌ์ค˜์•ผ ํ•  ๋•Œ

cf) SharedFlow

  • ๋ˆ„๊ตฐ๊ฐ€ emit("์—๋Ÿฌ ๋‚ฌ์–ด์š”!")๋ผ๊ณ  ๋งํ•˜๋ฉด, ๊ทธ ์ˆœ๊ฐ„ ๋“ฃ๊ณ  ์žˆ๋˜ ์‚ฌ๋žŒ ๋“ค์Œ
  • StateFlow๋ž‘ ๋น„์Šทํ•˜์ง€๋งŒ ์ด์ „์— ๋งํ•œ ๊ฑด ๊ธฐ์–ต ์•ˆํ•จ (๊ฐ’ ์ €์žฅ ์•ˆ ํ•จ)
  • ์—๋Ÿฌ ๋ฉ”์‹œ์ง€, ํ† ์ŠคํŠธ ์•Œ๋ฆผ ๋“ฑ ํ•œ ๋ฒˆ๋งŒ ๋ณด์—ฌ์ค„ ์ด๋ฒคํŠธ์— ์ ํ•ฉ
val toastMessage = MutableSharedFlow<String>(
    replay = 0,       // ๊ณผ๊ฑฐ ๋ฉ”์‹œ์ง€ ์ €์žฅ ์•ˆ ํ•จ
    extraBufferCapacity = 1 // ๋ฒ„ํผ 1๊ฐœ ํ—ˆ์šฉ
)
// ํ™”๋ฉด์—์„œ collect
lifecycleScope.launch {
    viewModel.toastMessage.collect {
        Toast.makeText(context, it, LENGTH_SHORT).show()
    }
}

1-2) LiveData

๐Ÿ’ก ํ•ต์‹ฌ

  • ๊ฐ’์ด ํ•œ ๋ฒˆ ๋ฐ”๋€” ๋•Œ ํ•œ ๋ฒˆ๋งŒ ๋ฐ˜์‘ํ•จ
  • "๊ฐ’์ด ๋ฐ”๋€Œ๋ฉด UI๊ฐ€ ํ•œ๋ฒˆ ๋ฐ˜์‘ํ•˜๊ณ  ๋!"
  • ํ™”๋ฉด(Activity/Fragment)์ด ์ผœ์ ธ ์žˆ์„ ๋•Œ๋งŒ ๋ฐ˜์‘ํ•จ
class CompareRecordViewModel : ViewModel() {

    private val repository = CompareRecordRepository()

    private val _compareRecordLiveData = MutableLiveData<CompareRecordResponse>()
    val compareRecordLiveData: LiveData<CompareRecordResponse> get() = _compareRecordLiveData

    private val _compareRecordErrorLiveData = MutableLiveData<String>()  // ์‹คํŒจ ๋ฉ”์‹œ์ง€ ์ €์žฅ์šฉ
    val compareRecordErrorLiveData: LiveData<String> get() = _compareRecordErrorLiveData

    fun getCompareRecord(genreCode:String,childId:String) {
        viewModelScope.launch {
            try {
                val response = repository.getCompareRecord(genreCode,childId)
                if (response.isSuccess) {
                    _compareRecordLiveData.postValue(response.getOrThrow())
                } else {
                    val errorMessage = response.exceptionOrNull()?.message ?: "์•Œ ์ˆ˜ ์—†๋Š” ์˜ค๋ฅ˜ ๋ฐœ์ƒ"
                    _compareRecordErrorLiveData.postValue(errorMessage)
                }
            } catch (e: Exception) {
                val errorMessage = "๋น„๊ต ๊ธฐ๋ก ํŒŒ์‹ฑ ์ค‘ ์˜ค๋ฅ˜ ๋ฐœ์ƒ: ${e.message}"
                Log.d("TAG", errorMessage)
                _compareRecordErrorLiveData.postValue(errorMessage)
            }
        }
    }

}

####ํ™”๋ฉด
private lateinit var compareViewModel: CompareRecordViewModel

compareViewModel = ViewModelProvider(this)[CompareRecordViewModel::class.java]

private fun registerViewModelObserve() {
        compareViewModel.compareRecordLiveData.observe(viewLifecycleOwner) { response ->
            if (response.status) {

                //๋ทฐ ํŽ˜์ด์ €์˜ ์บ์‹ฑ ์‚ญ์ œ ํ›„ !! ์—ฌ๊ธฐ๊ฐ€ ์•„์ฃผ ํ•„์ˆ˜
                binding.radarChart.options.series = arrayListOf()
                binding.radarChart.update(binding.radarChart.options, true, true)

                val newData = response.data
                updateRadarDataToUi(newData)

            }
        }

        compareViewModel.compareRecordErrorLiveData.observe(viewLifecycleOwner) { error ->
            Toast.makeText(requireContext(), error, Toast.LENGTH_SHORT).show()
            Log.e("ERROR", "RadarChartFragment compareRecordError : $error")
        }
    }
    
    private fun updateRadarData() {
        compareViewModel.getCompareRecord(
            Common.GENRE_CODE,
            Common.getSharedPreferencesAiFitParent(requireContext(), "selectedChildId").toString()
        )
    }

๐Ÿ“Œ ํ™”๋ฉด์ด ๊บผ์กŒ๋‹ค๊ฐ€ ๋‹ค์‹œ ์ผœ์งˆ ๋•Œ ์ž๋™์œผ๋กœ ๋‹ค์‹œ ๋ฐ์ดํ„ฐ๋ฅผ ๋ณด์—ฌ์ค˜์•ผ ํ•  ๋•Œ, ๋ฐ์ดํ„ฐ๊ฐ€ ๊ทธ๋ ‡๊ฒŒ ์ž์ฃผ ๋ฐ”๋€Œ์ง€ ์•Š์„ ๋•Œ

2.์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋ฐฉ์‹

ํ•ญ๋ชฉStateFlowLiveData
์—๋Ÿฌ ์ „๋‹ฌ ๋ฐฉ์‹MutableSharedFlow<'String> ์‚ฌ์šฉ โ†’ ์—ฌ๋Ÿฌ ๋ฒˆ ๋ฐœํ–‰ ๊ฐ€๋Šฅ, ์ˆ˜์ง‘์ž ํ•„์š”MutableLiveData<'Sting> ์‚ฌ์šฉ โ†’ UI์—์„œ ๋‹จ์ˆœ ๊ด€์ฐฐ
์ฒ˜๋ฆฌ ๋ฐฉ์‹.emit(...).postValue(...)

3.์–ด๋–ค ๊ฒƒ์„ ์–ธ์ œ ์จ์•ผ ํ• ๊นŒ?

์ƒํ™ฉ๊ถŒ์žฅ ๋ฐฉ์‹
UI ์ƒ๋ช…์ฃผ๊ธฐ ๋”ฐ๋ผ ์•ˆ์ „ํ•˜๊ฒŒ ๋ฐ์ดํ„ฐ ์ฒ˜๋ฆฌLiveData (์˜ˆ: ๋น„๊ต ๊ธฐ๋ก)
๋น„๋™๊ธฐ/์ŠคํŠธ๋ฆผ/์ค‘๋ณต ์ด๋ฒคํŠธ ์ฒ˜๋ฆฌ ํ•„์š”StateFlow / SharedFlow (์˜ˆ: ์ฝ˜ํ…์ธ  ๋ชฉ๋ก, ์žฅ๋ฅด, ์—๋Ÿฌ ์ฒ˜๋ฆฌ ๋“ฑ)
์—ฌ๋Ÿฌ ๊ฐ’ ๋ˆ„์  ๋ฐ ์‹ค์‹œ๊ฐ„ ์—…๋ฐ์ดํŠธ ํ•„์š”StateFlow + .update { } ์‚ฌ์šฉ
๋‹จ์ผ ๊ฐ’ ์‘๋‹ต ์ฒ˜๋ฆฌ์— ์ถฉ๋ถ„ํ•œ ๊ฒฝ์šฐLiveData๋กœ ๊ฐ„๋‹จ ์ฒ˜๋ฆฌ

๐Ÿ’ก์š”์•ฝ ์ •๋ฆฌ

๊ตฌ๋ถ„StateFlowLiveData
๋ชฉ์ ์ƒํƒœ ์ŠคํŠธ๋ฆผ, ์—ฌ๋Ÿฌ ์žฅ๋ฅด/์ฝ˜ํ…์ธ  ๋ˆ„์ ๋‹จ๊ฑด ๋น„๊ต ๊ธฐ๋ก ์‘๋‹ต
์ฝ”๋ฃจํ‹ด์ฝ”๋ฃจํ‹ด ๊ธฐ๋ฐ˜, StateFlow/SharedFlowLiveData ๊ธฐ๋ฐ˜
์ˆ˜์‹  ๋ฐฉ์‹collect {}observe(...)
์—๋Ÿฌ ์ฒ˜๋ฆฌSharedFlow.emit(...)LiveData.postValue(...)
๋ฐ์ดํ„ฐ ๊ตฌ์กฐMap, List ๋“ฑ์„ ๋ˆ„์  ์ฒ˜๋ฆฌ๋‹จ์ผ ๊ฐ์ฒด ์ฒ˜๋ฆฌ
profile
๋ณด์กฐ๊ธฐ์–ต์žฅ์น˜

0๊ฐœ์˜ ๋Œ“๊ธ€