[Kotlin][Android] ์๋๋ก์ด๋ Coroutine Flow ์ ์ฉํ๊ธฐ (1) (๋คํธ์ํฌ ํต์ Flow๋ก ์ด์ ํ๊ธฐ)
์๋ ํ์ธ์ ๐
์ค๋์ ์๋๋ก์ด๋ 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๋ก ๋ณ๊ฒฝํ์ฌ ์ฌ์ฉํ๋ ๋ฐฉ๋ฒ์ ๋ํด ์์๋ณด๋๋ก ํ๊ฒ ์ต๋๋ค ๐
๊ทธ๋ผ ์ค๋๋ ์ฆ์ฝํ์ธ์ :)