Android/Kotlin

[Kotlin][Android] ์•ˆ๋“œ๋กœ์ด๋“œ Coroutine Flow ์ ์šฉํ•˜๊ธฐ (1) (๋„คํŠธ์›Œํฌ ํ†ต์‹  Flow๋กœ ์ด์ „ํ•˜๊ธฐ)

๋ށ์š” 2023. 3. 22. 13:45

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

์˜ค๋Š˜์€ ์•ˆ๋“œ๋กœ์ด๋“œ MVVMํŒจํ„ด์—์„œ Flow๋ฅผ ์–ด๋–ป๊ฒŒ ์ ์šฉํ•˜๋Š”์ง€๋ฅผ ์•Œ์•„๋ณด๋ ค๊ณ  ํ•ฉ๋‹ˆ๋‹ค ๐Ÿ˜Ž

Flow์— ๋Œ€ํ•œ ๊ธฐ๋ณธ ํฌ์ŠคํŒ…์€ ์•„๋ž˜ ๋งํฌ๋ฅผ ์ฐธ๊ณ  ๋ฐ”๋ž๋‹ˆ๋‹ค ๐Ÿ™

 

[Kotlin] ์ฝ”ํ‹€๋ฆฐ Coroutine์— ๋Œ€ํ•˜์—ฌ (3) - Coroutine Flow(ํ”Œ๋กœ์šฐ)

์•ˆ๋…•ํ•˜์„ธ์š”๐Ÿ‘‹ ์˜ค๋Š˜์€ ์ฝ”๋ฃจํ‹ด ๊ด€๋ จ ๋งˆ์ง€๋ง‰ ํฌ์ŠคํŒ…์œผ๋กœ ์ฝ”๋ฃจํ‹ด Flow์— ๋Œ€ํ•ด ๊ณต๋ถ€ํ•œ ๋‚ด์šฉ์„ ํฌ์ŠคํŒ…ํ•ด๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค. 1. Coroutine Flow๋ž€? Flow๋Š” ์ˆœ์ฐจ์ ์œผ๋กœ ๊ฐ’์„ ๋‚ด๋ณด๋‚ด๊ณ  ์ •์ƒ์ ์œผ๋กœ ๋˜๋Š” ์˜ˆ์™ธ๋กœ ์™„๋ฃŒ๋˜

devyo-111commit.tistory.com


์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” ์ด์ „ Clean Architecture์— ๋Œ€ํ•œ ํฌ์ŠคํŒ…์„ ํ•  ๋•Œ ์‚ฌ์šฉํ–ˆ๋˜ ์˜ˆ์ œ๋ฅผ Flow๋กœ ์ด์ „ํ•˜๋ฉฐ ์‚ฌ์šฉํ•  ์˜ˆ์ •์ž…๋‹ˆ๋‹ค.

ํ•ด๋‹น ์˜ˆ์ œ์˜ ์ „์ฒด ์ฝ”๋“œ ๋ฐ ์„ค๋ช…์€ ์•„๋ž˜ ํฌ์ŠคํŒ…์„ ์ฐธ๊ณ ํ•ด์ฃผ์„ธ์š” ๐Ÿคฉ

 

[Android] ์•ˆ๋“œ๋กœ์ด๋“œ Clean Architecture์— ๋Œ€ํ•ด์„œ (2) - ์•ˆ๋“œ๋กœ์ด๋“œ ํ”„๋กœ์ ํŠธ์— ์ ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•

์•ˆ๋…•ํ•˜์„ธ์š” ๐Ÿ‘‹ ์˜ค๋Š˜์€ ์ง€๋‚œ ํฌ์ŠคํŒ…์— ์ด์–ด ์•ˆ๋“œ๋กœ์ด๋“œ ํ”„๋กœ์ ํŠธ์—์„œ ์–ด๋–ป๊ฒŒ Clean Architecture๋ฅผ ์ ์šฉ์‹œํ‚ค๋Š”์ง€์— ๋Œ€ํ•ด์„œ ํฌ์ŠคํŒ…ํ•ด๋ณด๋ ค ํ•ฉ๋‹ˆ๋‹ค. Clean Architecture์˜ ๊ฐœ๋…์— ๋Œ€ํ•ด์„œ๋Š” ์•„๋ž˜ ํฌ์ŠคํŒ…์„ ์ฐธ

devyo-111commit.tistory.com

 

๊ตฌํ˜„์ฒด๊ฐ€ ์ˆ˜์ •๋œ ๊ฒฝ์šฐ interface๋„ ํ•จ๊ป˜ ์ˆ˜์ •๋˜์—ˆ๋‹ค๋Š”์  ์ฐธ๊ณ  ๋ฐ”๋ž๋‹ˆ๋‹ค. ๐Ÿค”

 

Data Layer

[MyApi.kt] Response์— ๊ฐ์ฒด๋ฅผ ๋‹ด๋„๋ก ์ˆ˜์ •

 

๊ธฐ์กด

interface MyApi {
    @GET(DataConst.GET_CONGESTION_URL)
    suspend fun getBeachCongestion() : BeachCongestionListDTO
}

 

๋ณ€๊ฒฝ ํ›„

interface MyApi {
    @GET(DataConst.GET_CONGESTION_URL)
    suspend fun getBeachCongestion() : Response<BeachCongestionListDTO>
}

[BeachDataSourceImpl.kt] ๋ฐ˜ํ™˜๊ฐ’์„ seald class๋กœ ๊ฐ์‹ธ๊ณ  Flow๋กœ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ •

 

๊ธฐ์กด

class BeachDataSourceImpl @Inject constructor(
    private val myApi: MyApi
) : BeachDataSource{
    override suspend fun getCongestionList(): BeachCongestionList {
        return BeachMapper.mapperToBeachCongestionList(myApi.getBeachCongestion())
    }
}

๋ณ€๊ฒฝ ํ›„

class BeachDataSourceImpl @Inject constructor(
    private val myApi: MyApi
) : BeachDataSource{
    override suspend fun getCongestionList(): Flow<NetworkResult<BeachCongestionList>> {
        return BeachMapper.mapperToBeachCongestionList(myApi.getBeachCongestion())
    }
}

[MainRepositoryImpl.kt] ๋ฐ˜ํ™˜๊ฐ’์„ seald class๋กœ ๊ฐ์‹ธ๊ณ  Flow๋กœ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ •

 

๊ธฐ์กด

class MainRepositoryImpl @Inject constructor(
    private val beachDataSource: BeachDataSource
) : MainRepository {
    override suspend fun getBeachCongestion(): BeachCongestionList {
        return beachDataSource.getCongestionList()
    }
}

๋ณ€๊ฒฝ ํ›„

class MainRepositoryImpl @Inject constructor(
    private val beachDataSource: BeachDataSource
) : MainRepository {
    override suspend fun getBeachCongestion(): Flow<NetworkResult<BeachCongestionList>> {
        return beachDataSource.getCongestionList()
    }
}

 

[BeachMapper.kt]

์ „๋‹ฌ๋ฐ›์€ ๊ฐ์ฒด๋ฅผ domain์˜ model๊ณผ mapping ํ›„์— ์„ฑ๊ณต/์‹คํŒจ/์˜ˆ์™ธ ์—ฌ๋ถ€์— ๋”ฐ๋ผ ๊ฒฐ๊ด๊ฐ’์„ seald class์— ๋‹ด์•„ ๋ณด๋‚ด๋Š” Flow๋ฅผ ์ƒ์„ฑํ•˜์—ฌ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ •

 

๊ธฐ์กด

object BeachMapper {
    private fun mapperToBeach(beachDTO: BeachDTO) : Beach {
        val beach = beachDTO.run {
            Beach(poiNm, congestion)
        }
        return beach
    }

    fun mapperToBeachCongestionList(beachCongestionListDTO: BeachCongestionListDTO) : BeachCongestionList {
        val list = arrayListOf<Beach>()
        beachCongestionListDTO.getAllBeachList().forEach {
            val beach = mapperToBeach(it)
            list.add(beach)
        }

        return BeachCongestionList(list)
    }
}

๋ณ€๊ฒฝ ํ›„

object BeachMapper {
    private fun mapperToBeach(beachDTO: BeachDTO) : Beach {
        val beach = beachDTO.run {
            Beach(poiNm, congestion)
        }
        return beach
    }

     fun mapperToBeachCongestionList(response : Response<BeachCongestionListDTO>) : Flow<NetworkResult<BeachCongestionList>> {
        if(response.isSuccessful){
            val list = arrayListOf<Beach>()
            response.body()?.getAllBeachList()?.forEach {
                val beach = mapperToBeach(it)
                list.add(beach)
            }
            val beachCongestionList = BeachCongestionList(list)
            return flow { // ์„ฑ๊ณต์‹œ์— ๋ฐ˜ํ™˜ํ•  Flow
                emit(NetworkResult.Success(beachCongestionList))
            }
        } else {
            return flow<NetworkResult<BeachCongestionList>> {  // ์‹คํŒจ์‹œ์— ๋ฐ˜ํ™˜ํ•  Flow
                emit(NetworkResult.Failure(response.code()))
            }.catch { exception -> // ์˜ˆ์™ธ์ฒ˜๋ฆฌ
                emit(NetworkResult.Exception(exception.message!!))
            }
        }
    }
}

Domain Layer

[NetworkResult.kt] ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์˜ ๊ฒฐ๊ณผ๊ฐ’์„ ๋‹ด์•„ ๋ณด๋‚ด๋Š” sealed class

/**
 * ๋„คํŠธ์›Œํฌ ํ†ต์‹ ์˜ ๊ฒฐ๊ณผ๋“ค์„ ๋ชจ์•„๋†“์€ sealed class
 * **/
sealed class NetworkResult<T> {
    data class Loading<T>(val isLoading: Boolean) : NetworkResult<T>()
    data class Success<T>(val data: T) : NetworkResult<T>()
    data class Exception<T>(val errorMessage: String) : NetworkResult<T>()
    data class Failure<T>(val code: Int) : NetworkResult<T>()
}

[GetBeachCongstionList.kt] ๋ฐ˜ํ™˜๊ฐ’์„ seald class๋กœ ๊ฐ์‹ธ๊ณ  Flow๋กœ ๋ฐ˜ํ™˜ํ•˜๋„๋ก ์ˆ˜์ •

 

๊ธฐ์กด

class GetBeachCongestionList @Inject constructor(
    private val repository: MainRepository
) {
    suspend fun invoke() : BeachCongestionList {
        return repository.getBeachCongestion()
    }
}

 

๋ณ€๊ฒฝ ํ›„

class GetBeachCongestionList @Inject constructor(
    private val repository: MainRepository
) {
    suspend fun invoke() : Flow<NetworkResult<BeachCongestionList>> {
        return repository.getBeachCongestion()
    }
}

Presantation Layer

[MyViewModel.kt] ๊ฒฐ๊ณผ๊ฐ’์„ ๋ฐ›๋Š” LiveData๋ฅผ ์ถ”๊ฐ€ํ•˜์—ฌ ๊ฐ’์„ setting ํ•˜๋„๋ก ์ˆ˜์ •

 

๊ธฐ์กด

@HiltViewModel
class MyViewModel @Inject constructor(
    private val getBeachCongestionList: GetBeachCongestionList
) : ViewModel(){
    private val _beachCongestionList = MutableLiveData<BeachCongestionList>()
    val beachCongestionList : LiveData<BeachCongestionList>
    get() = _beachCongestionList

    private val _toastMessage = MutableLiveData<String>()
    val toastMessage : LiveData<String>
    get() = _toastMessage

    fun getBeachInfo(){
        val exceptionHandler = CoroutineExceptionHandler{ _ , exception ->
            when(exception){
                is SocketTimeoutException -> {onError("ํ†ต์‹ ์‹œ๊ฐ„ ์ดˆ๊ณผ!")}
            }
        }
        viewModelScope.launch(exceptionHandler) {
            _beachCongestionList.value = getBeachCongestionList.invoke()
        }
    }

    private fun onError(message : String) {
        _toastMessage.value = message
    }
}

๋ณ€๊ฒฝ ํ›„

@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
    }
}

 

[MainActivity.kt] LiveData์— ์ „๋‹ฌ๋œ ๊ฒฐ๊ณผ๊ฐ’์— ๋”ฐ๋ผ ๊ฐ๊ฐ์˜ ์ฒ˜๋ฆฌ๋ฅผ ํ•˜๋„๋ก ์ˆ˜์ •

 

๊ธฐ์กด

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){
            beachCongestionList.observe(this@MainActivity){
                Log.d(TAG, "observeData: $it")
            }
            toastMessage.observe(this@MainActivity){
                Toast.makeText(this@MainActivity , it , Toast.LENGTH_SHORT).show()
            }
        }
    }
}

๋ณ€๊ฒฝ ํ›„

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()
            }
        }
    }
}

์ด๋ฒˆ ํฌ์ŠคํŒ…์—์„œ๋Š” ๋„คํŠธ์›Œํฌ ํ†ต์‹  ๋™์ž‘์„ Flow๋กœ ์ด์ „ํ•ด๋ณด๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ๋‹ค๋ค„๋ณด์•˜์Šต๋‹ˆ๋‹ค.

๋‹ค์Œ ํฌ์ŠคํŒ…์—์„œ๋Š” LiveData๋ฅผ Flow๋กœ ๋ณ€๊ฒฝํ•˜์—ฌ ์‚ฌ์šฉํ•˜๋Š” ๋ฐฉ๋ฒ•์— ๋Œ€ํ•ด ์•Œ์•„๋ณด๋„๋ก ํ•˜๊ฒ ์Šต๋‹ˆ๋‹ค ๐Ÿ˜ƒ

 

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