[Kotlin][Android] ์๋๋ก์ด๋ Coroutine Flow ์ ์ฉํ๊ธฐ (2) ( LiveData๋ฅผ Flow๋ก ๋ณ๊ฒฝํ๊ธฐ : LiveData ์ Flow ๋น๊ตํ๊ธฐ )
์๋ ํ์ธ์ ๐
์ค๋์ ์ ๋ฒ ํฌ์คํ ์ ์ด์ด LiveData๋ฅผ Flow๋ก ์ด์ ํ๋ ๊ณผ์ ์ ํฌ์คํ ํด๋ณด๋ ค ํฉ๋๋ค.
๋ฟ๋ง ์๋๋ผ, LiveData์ Flow์ ์ฐจ์ด์ ์ ๋ํด์๋ ํจ๊ป ๋ค๋ค๋ณผ ์์ ์ ๋๋ค ๐

์ด์ ํฌ์คํ ์ ์๋ ๋งํฌ๋ฅผ ํตํด ์ฐธ๊ณ ๋ฐ๋๋๋ค. ๐
[Kotlin][Android] ์๋๋ก์ด๋ Coroutine Flow ์ ์ฉํ๊ธฐ (1) (๋คํธ์ํฌ ํต์ Flow๋ก ์ด์ ํ๊ธฐ)
์๋ ํ์ธ์ ๐ ์ค๋์ ์๋๋ก์ด๋ MVVMํจํด์์ Flow๋ฅผ ์ด๋ป๊ฒ ์ ์ฉํ๋์ง๋ฅผ ์์๋ณด๋ ค๊ณ ํฉ๋๋ค ๐ Flow์ ๋ํ ๊ธฐ๋ณธ ํฌ์คํ ์ ์๋ ๋งํฌ๋ฅผ ์ฐธ๊ณ ๋ฐ๋๋๋ค ๐ [Kotlin] ์ฝํ๋ฆฐ Coroutine์ ๋ํ์ฌ (3) -
devyo-111commit.tistory.com
LiveData๋ฅผ Flow๋ก ๋ณ๊ฒฝํ๊ธฐ์ ์์ ๋์ ์ฐจ์ด์ ๊ณผ ์ Flow๋ฅผ ์ฌ์ฉํ๋์ง์ ๋ํด์ ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค.
LiveData
LiveData๋ ๊ด์ฐฐ ๊ฐ๋ฅํ data holder class์ ๋๋ค.
๋ํ, LiveData๋ ๋ค๋ฅธ ์๋๋ก์ด๋ ๊ตฌ์ฑ์์์์ ๊ด์ฐฐํ ์ ์๋ ๋ฐ์ดํฐ ์งํฉ์ ๋ณด์ ํ ์ ์์ผ๋ฉฐ ์๋ช ์ฃผ๊ธฐ๋ฅผ ์ธ์ํ๊ธฐ ๋๋ฌธ์ ๋ฐ์ดํฐ๋ฅผ ๊ด์ฐฐํ๋ ๊ตฌ์ฑ ์์๊ฐ ํ๊ดด๋๊ฑฐ๋ ํ์ฑํ๋์ง ์์ผ๋ฉด ํด๋น observer์๊ฒ ๋ฐ์ดํฐ ๊ฒ์๋ฅผ ์ค์งํฉ๋๋ค.
LiveData์ ์ฅ์
- ๋ฉ๋ชจ๋ฆฌ ๋์๊ฐ ์๋ค.
- ์๋ช ์ฃผ๊ธฐ๋ฅผ ์ธ์ํ๊ธฐ ๋๋ฌธ์ ์ค์ง๋ ์ปดํฌ๋ํธ๋ก ์ธํ ์ถฉ๋์ด ์๊ณ , ์๋์ผ๋ก ์๋ช ์ฃผ๊ธฐ๋ฅผ ๊ด๋ฆฌํด ์ค ํ์๊ฐ ์๋ค.
- UI์ Data์ ์ผ์น์ฑ์ ๋ณด์ฅํ๋ค.
๊ทธ๋ ๋ค๋ฉด ์ Flow๋ฅผ ์ฌ์ฉํ๋ ค ํ ๊น์? ๐ค
LiveData ๊ฐ์ | Android ๊ฐ๋ฐ์ | Android Developers
LiveData๋ฅผ ์ฌ์ฉํ์ฌ ์๋ช ์ฃผ๊ธฐ๋ฅผ ์ธ์ํ๋ ๋ฐฉ์์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ฒ๋ฆฌํฉ๋๋ค.
developer.android.com
Android Developer์ ๋ฐ๋ฅด๋ฉด LiveData๋ ๋น๋๊ธฐ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ ์ฒ๋ฆฌํ๋๋ก ์ค๊ณ ๋์ด ์์ง ์๋ค๊ณ ํฉ๋๋ค.
LiveData๋ ๊ธฐ๋ณธ์ ์ผ๋ก ๋ณ๊ฒฝ๋ ๊ฐ์ Main Thread์์ ๊ด์ฐฐํ๊ธฐ ๋๋ฌธ์ ๋ค๋ฅธ ๋ ์ด์ด์์ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ ๊ด์ฐฐํ๊ณ ์ถ์ ๊ฒฝ์ฐ์๋
Flow๋ฅผ ์ฌ์ฉํ ๋ค์ asLiveData()๋ฅผ ํตํด ViewModel์์ ๋ณํํ์ฌ ์ฌ์ฉํ๋ ๊ฒ ์ข๋ค๊ณ ๋์์์ต๋๋ค.
[Android] ์๋๋ก์ด๋ Clean Architecture์ ๋ํด์ ( 1 ) - Clean Architecture์ ๊ฐ๋
์๋ ํ์ธ์ ๐ ์ค๋์ Clean Architecture์ ๋ํด์ ๊ณต๋ถํ ๋ด์ฉ์ ํฌ์คํ ํด๋ณด๋ ค ํฉ๋๋ค. ํ๋ก์ ํธ์ ์ ์ฉํ๋ ๋ฐฉ๋ฒ๊น์ง ํฌ์คํ ํ๊ธฐ์ ๋ด์ฉ์ด ๊ธธ์ด์ง ๊ฒ ๊ฐ์ ์ค๋์ ๊ฐ๋ ์ ๋ํด์๋ง ๊ฐ๋ตํ๊ฒ ํฌ
devyo-111commit.tistory.com
๋ฟ๋ง ์๋๋ผ, ์๋๋ก์ด๋์ Clean Architecture ์ธก๋ฉด์์ Domain Layer์๋
์๋๋ก์ด๋ ์์กด์ฑ์ด ์๋ ์์ ์๋ฐ/์ฝํ๋ฆฐ ์ฝ๋๋ง์ด ์กด์ฌํด์ผ ํ๋๋ฐ ํด๋น Layer์์ LiveData๋ฅผ ์ฌ์ฉํ๊ธฐ์๋
๋ถ์ ํฉํ๋ฏ๋ก Flow๋ฅผ ํตํด ํด๋น ๋ถ๋ถ์ ํด์ํ ์ ์์ต๋๋ค.
Android) LiveData์ Coroutine Flow ๋น๊ตํด๋ณด๊ธฐ
์์ฆ์ ์ฝ๋ฃจํด์ flow๋ฅผ ์ฌ์ฉํด์ ์๋๋ก์ด๋ ๊ฐ๋ฐ์ ํ๊ณ ์๋๋ฐ, LiveData์ ํจ๊ป flow๋ฅผ ์ฌ์ฉ ์ ์ฌ๋ฐ๋ฅด๊ฒ ์ฌ์ฉํ๊ณ ์๋ ๊ฑด์ง์ ๋ํ ๊ถ๊ธ์ฆ์ด ์๊ฒจ์ ์ ํฉํ ๋ฐฉ๋ฒ์ ๋ํด ๊ธ์ ์จ๋ณด๋ ค๊ณ ํฉ๋๋ค.
yoon-dailylife.tistory.com
ํ์ง๋ง, Flow๋ LiveData์๋ ๋ค๋ฅด๊ฒ ์๋ช ์ฃผ๊ธฐ๋ฅผ ์ธ์ํ์ง ๋ชปํ๋ฉฐ ์ํ๊ฐ ์กด์ฌํ์ง ์์ ํ์ฌ ๋ฌด์จ ๊ฐ์ ๊ฐ์ง๊ณ ์๋์ง ์๊ธฐ ์ด๋ ต์ต๋๋ค. ๋ฟ๋ง ์๋๋ผ, Cold Stream์ผ๋ก ์ฌ์ฉ์๊ฐ collect ํ๊ธฐ ์ ๊น์ง๋ ๊ฐ์ ๋ฐํํ์ง ์์ ์ฐ์ํด์ ๋ค์ด์ค๋ ๊ฐ์ ๋ํ ์ฒ๋ฆฌ๊ฐ ์ด๋ ต์ต๋๋ค.
๊ทธ๋ ๊ธฐ์ ์ ์๋ฃ์ ๋ฐ๋ฅด๋ฉด
LiveData๋ ๊ตฌ์ฑ ๋ณ๊ฒฝ ์์ ์์ ์ฑ์ ์ ๊ณตํ๊ณ ์ต์ ๋ฐ์ดํฐ๋ฅผ ๋ทฐ๋ก ์ ๋ฌํ๋ ์ญํ ์ ํ๋ค.
Flow๋ UseCase, Repository, DataSources Layer์ ๊ธด๋ฐํ๊ฒ ์๋ํด ๋ฐ์ดํฐ๋ฅผ ์์ง ๋ฐ ์ฒ๋ฆฌํด์
์๋ก ๋ค๋ฅธ ์ฝ๋ฃจํด ๋ฒ์์์ ์์ ์ ์คํํ๋ค.
๊ทธ๋ฌ๋ฏ๋ก ViewModel, View ์ฌ์ด์ ์ํธ์์ฉ์ LiveData, ๋ ๊น์ ๋ ์ด์ด์ ์ฐ๋ ๋ฉ ๊ฐ์ ๋ ๋ณต์กํ ์ฒ๋ฆฌ๋ Flow๊ฐ ์ฒ๋ฆฌํ๋ค.
์ฆ, LiveData์ Flow๋ ์๋ก์ ๋จ์ ์ ๋ณด์ํด ์ฃผ๋ฉฐ Repository์์ ์ฒ๋ฆฌ๋๋ ์ผ๋ จ์ ๋ฐ์ดํฐ ์คํธ๋ฆผ์ Flow๋ฅผ ํตํด ์ฒ๋ฆฌํ๊ณ
์ ๋ฌ๋ฐ์ View์์์ ์ฒ๋ฆฌ๋ LiveData๋ฅผ ํตํด ์ฌ์ฉํ๋ ๊ตฌ์กฐ๋ก ๋ง์ด ์ฌ์ฉํ๋ค๊ณ ๋ณด๋ฉด ๋ ๊ฒ ๊ฐ์ต๋๋ค.
ํ์ง๋ง โ
StateFlow์ ๋ฑ์ฅ
StateFlow๋ ํ์ฌ ์ํ์ ์๋ก์ด ์ํ ์ ๋ฐ์ดํธ๋ฅผ ์์ง๊ธฐ์ ๋ด๋ณด๋ด๋ ๊ด์ฐฐ ๊ฐ๋ฅํ ์ํ ํ๋ ํ๋ฆ์ผ๋ก value ์์ฑ์ ํตํด ํ์ฌ ์ํ๊ฐ์ ์ฝ์ ์ ์์ต๋๋ค.
์์์ ์ธ๊ธํ Flow์ ๋จ์ ์ ํด์ํ์ฌ LiveData์ ๊ฐ์ด ๋ง๋ ๊ฒ์ด ๋ฐ๋ก ์ด StateFlow์ ๋๋ค.
๊ทธ๋ ๋ค๋ฉด ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ์ด๋ป๊ฒ ๋ ๊น์? ๐
- StateFlow๋ ๊ธฐ๋ณธ์ ์ผ๋ก Read-Only์ ๋๋ค. ํด๋น ๊ฐ์ ๋ณ๊ฒฝํ๊ธฐ ์ํด์๋ MutableStateFlow๋ฅผ ์ฌ์ฉํด์ผ ํฉ๋๋ค.
- StateFlow๋ LiveData์๋ ๋ค๋ฅด๊ฒ ๊ธฐ๋ณธ๊ฐ์ ์ค์ ํด์ฃผ์ด์ผ ํฉ๋๋ค.
- StateFlow๋ ๋ค๋ฅธ Flow์์ ์์งํ๋ ๊ฒฝ์ฐ LiveData์๋ ๋ค๋ฅด๊ฒ View๊ฐ stop ๋์์ ๋ ์๋์ผ๋ก ๊ด์ฐฐ์ ๋ฉ์ถ์ง ์์
repeatOnLifeCycle {} ํน์ flowWithLifecycle.collect{} ์ ํตํด ๊ฐ์ ์์งํด์ผ ํฉ๋๋ค.
- repeatOnLifeCycle{} ์ ์ฌ๋ฌ ๊ฐ์ Flow๋ฅผ ์์งํ ๋ ์ฌ์ฉํ๋ฉฐ
flowWithLifecycle.collect {}๋ ํ๋์ Flow๋ง ์์งํ ๋ ์ฌ์ฉํฉ๋๋ค.
์ง๊ธ๋ถํฐ๋ ์ด์ ํฌ์คํ ์ ์ด์ด LiveData๋ฅผ Flow๋ก ๋ณ๊ฒฝํ๋ ์์ ์ ๋ํด์ ๋ง์๋๋ฆฌ๊ฒ ์ต๋๋ค.
[MyViewModel.kt] ๋คํธ์ํฌ ํต์ ๊ฒฐ๊ณผ๋ฅผ ๊ด์ฐฐํ๋ LiveData๋ฅผ stateFlow๋ก ๋ณ๊ฒฝํด์ฃผ์์ต๋๋ค.
๊ธฐ์กด
@HiltViewModel
class MyViewModel @Inject constructor(
private val getBeachCongestionList: GetBeachCongestionList
) : ViewModel(){
private val _networkResult = MutableLiveData<NetworkResult<BeachCongestionList>>()
val networkResult : LiveData<NetworkResult<BeachCongestionList>>
get() = _networkResult
private val _beachCongestionList = MutableLiveData<BeachCongestionList>()
val beachCongestionList : LiveData<BeachCongestionList>
get() = _beachCongestionList
fun setBeachCongestionList(beachCongestionList: BeachCongestionList){
_beachCongestionList.value = beachCongestionList
}
private val _toastMessage = MutableLiveData<String>()
val toastMessage : LiveData<String>
get() = _toastMessage
fun getBeachInfo(){
viewModelScope.launch {
getBeachCongestionList.invoke().collect{ result ->
_networkResult.value = result
}
}
}
fun onError(message : String) {
_toastMessage.value = message
}
}
๋ณ๊ฒฝ ํ
@HiltViewModel
class MyViewModel @Inject constructor(
private val getBeachCongestionList: GetBeachCongestionList
) : ViewModel(){
private val _networkResult = MutableStateFlow<NetworkResult<BeachCongestionList>>(NetworkResult.Success(BeachCongestionList(arrayListOf())))
val networkResult : StateFlow<NetworkResult<BeachCongestionList>>
get() = _networkResult
private val _beachCongestionList = MutableLiveData<BeachCongestionList>()
val beachCongestionList : LiveData<BeachCongestionList>
get() = _beachCongestionList
fun setBeachCongestionList(beachCongestionList: BeachCongestionList){
_beachCongestionList.value = beachCongestionList
}
private val _toastMessage = MutableLiveData<String>()
val toastMessage : LiveData<String>
get() = _toastMessage
fun getBeachInfo(){
viewModelScope.launch {
getBeachCongestionList.invoke().collect{ result ->
_networkResult.value = result
}
}
}
fun onError(message : String) {
_toastMessage.value = message
}
}
[MainActivity.kt] ์ ๋ฌ๋ฐ์ ๊ฐ์ collect ํ์ฌ ๊ฐ์ ๊ด์ฐฐํ๋ค.
๊ธฐ์กด
private const val TAG = "MainActivity"
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding : ActivityMainBinding
private val viewModel : MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater).also {
setContentView(it.root)
}
observeData()
}
override fun onResume() {
super.onResume()
viewModel.getBeachInfo()
}
private fun observeData() {
with(viewModel){
networkResult.observe(this@MainActivity){ result ->
when(result){
is NetworkResult.Success -> setBeachCongestionList(result.data) // ์ฑ๊ณต์
is NetworkResult.Failure -> onError(result.code.toString()) // ์คํจ์
is NetworkResult.Exception -> onError(result.errorMessage) // ์์ธ ๋ฐ์์
is NetworkResult.Loading -> { // ํต์ ์ค
// ํ์ฌ ํ๋ก์ ํธ์์๋ ํ ์ผ์ด ์์
}
}
}
beachCongestionList.observe(this@MainActivity){
Log.d(TAG, "observeData: $it")
}
toastMessage.observe(this@MainActivity){
Toast.makeText(this@MainActivity , it , Toast.LENGTH_SHORT).show()
}
}
}
}
๋ณ๊ฒฝ ํ ( flowWithLifecycle.collect {}๋ฅผ ์ฌ์ฉํ ๋ฐฉ๋ฒ )
private const val TAG = "MainActivity"
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding : ActivityMainBinding
private val viewModel : MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater).also {
setContentView(it.root)
}
observeData()
}
override fun onResume() {
super.onResume()
viewModel.getBeachInfo()
}
private fun observeData() {
with(viewModel){
lifecycleScope.launch{
networkResult.flowWithLifecycle(lifecycle , Lifecycle.State.STARTED).collect{ result -> // ํ๋์ Flow๋ง collectํ ๋ ์ฌ์ฉ
when(result){
is NetworkResult.Success -> setBeachCongestionList(result.data)
is NetworkResult.Failure -> onError(result.code.toString())
is NetworkResult.Exception -> onError(result.errorMessage)
is NetworkResult.Loading -> {
// Noting to do
}
}
}
}
beachCongestionList.observe(this@MainActivity){ list ->
Log.d(TAG, "observeData: $list")
}
toastMessage.observe(this@MainActivity){ message ->
Log.d(TAG, "observeData: $message")
}
}
}
}
๋ณ๊ฒฝ ํ ( repeatOnLifecycle {}์ ์ฌ์ฉํ ๋ฐฉ๋ฒ )
private const val TAG = "MainActivity"
@AndroidEntryPoint
class MainActivity : AppCompatActivity() {
private lateinit var binding : ActivityMainBinding
private val viewModel : MyViewModel by viewModels()
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater).also {
setContentView(it.root)
}
observeData()
}
override fun onResume() {
super.onResume()
viewModel.getBeachInfo()
}
private fun observeData() {
with(viewModel){
lifecycleScope.launch{
repeatOnLifecycle(Lifecycle.State.STARTED){ // ์ฌ๋ฌ๊ฐ์ Flow๋ฅผ collect ํ ๋ ์ฌ์ฉ
launch {
networkResult.collect{ result ->
when(result){
is NetworkResult.Success -> setBeachCongestionList(result.data)
is NetworkResult.Failure -> onError(result.code.toString())
is NetworkResult.Exception -> onError(result.errorMessage)
is NetworkResult.Loading -> {
// Noting to do
}
}
}
}
launch{
// ๋ค๋ฅธ Flow
}
}
}
beachCongestionList.observe(this@MainActivity){ list ->
Log.d(TAG, "observeData: $list")
}
toastMessage.observe(this@MainActivity){ message ->
Log.d(TAG, "observeData: $message")
}
}
}
}
(์ ์ฝ๋๋ฅผ ๋ณด๋ฉด ๋ชจ๋ LiveData๋ฅผ StateFlow๋ก ๋ณ๊ฒฝ์ํค์ง๋ ์์๋๋ฐ ๋ค๋ฅธ LiveData ๋ํ StateFlow๋ก ๋ณ๊ฒฝํด๋ ๋ฌด๊ดํฉ๋๋ค.)
์ด๋ ๊ฒ LiveData์ Flow์ ์ฐจ์ด์ ๋ฐ StateFlow๋ฅผ ํตํด LiveData๋ฅผ Flow๋ก ์ด์ ํ๋ ๋ฐฉ๋ฒ๊น์ง ๋ชจ๋ ์ดํด๋ณด์์ต๋๋ค.
StateFlow๊ฐ ๋ฑ์ฅํ๋ฉด์ ํ์ฌ ๋ง์ API๊ฐ ๊ฐ๋ฐ๋๊ณ ์๋ ๊ฒ ๊ฐ์๋ฐ์
๋์ค์๋ Kotlin๊ธฐ๋ฐ ์ฑ์์๋ LiveData๋์ Flow๋ฅผ ์ฃผ๋ก ์ฌ์ฉํ๊ฒ ๋์ง ์์๊น ์๊ฐํด ๋ด ๋๋ค. ๐ค
์ฌ๋ฌ๋ถ๋ค๋ LiveData๋ฅผ ์ฌ์ฉํ๋ ํ๋ก์ ํธ๋ฅผ StateFlow๋ก ์ด์ ํด ๋ณด๋ ๊ฑด ์ด๋จ๊น์?
๊ธด ๊ธ ์ฝ์ด์ฃผ์ ์ ๊ฐ์ฌ๋๋ฆฌ๋ฉฐ
์ด์ ํฌ์คํ ์ ๋ง์น๊ฒ ์ต๋๋ค.
์ค๋๋ ์ฆ์ฝํ์ธ์ :)