Retrofit을 통한 통신 순서는
Compose View(Screen Event) -> ViewModel -> Repository -> Api interface
Response 결과를 통해 데이터를 반환 받는 순서는 그냥 반대로 하면된다
Api interface -> Repository -> ViewModel -> Compose View(Screen)
먼저 네트워크의 결과값을 상태에 따라 처리할 수 있도록 모듈로 빼준다.
NetworkResult.kt
sealed class NetworkResult<T>(var data: Any? = null, val message: String? = null) {
data class Success<T> constructor(val value: T) : NetworkResult<T>(value)
class Error<T> @JvmOverloads constructor(
var code: Int? = null,
var msg: String? = null,
var exception: Throwable? = null
) : NetworkResult<T>(code, msg)
class Loading<T> : NetworkResult<T>()
} // End of TestNetworkResult sealed class
sealed class로 성공, 로딩, 실패를 구분할 수 있는 클래스를 분리해놓고 나중에 API의 Response 결과에 따라서 상태를 구분할 수 있도록 해줌
Screen에서 버튼을 통해서 이벤트를 주고 통신을 한다고 가정해보자.
이벤트가 일어났을 때, ViewModel에 통신을 할 수 있는 함수를 하나 만들고 해당 함수를 실행시켜주면 된다.
ViewModel.kt
// ================================= getNavNFC =================================
private val _postNFCIdResponseSharedFlow = MutableSharedFlow<NetworkResult<NFC>>()
var postNFCIdResponseSharedFlow = _postNFCIdResponseSharedFlow
private set
fun postNFCId(nfcId: Int) {
viewModelScope.launch {
nfcRepo.postNFCId(nfcId).onStart {
_postNFCIdResponseSharedFlow.emit(NetworkResult.Loading())
}.catch {
_postNFCIdResponseSharedFlow.emit(
NetworkResult.Error(
null, it.message, it.cause
)
)
}.collectLatest { result ->
when {
result.isSuccessful -> {
_postNFCIdResponseSharedFlow.emit(
NetworkResult.Success(result.body()!!)
)
}
result.errorBody() != null -> {
_postNFCIdResponseSharedFlow.emit(
NetworkResult.Error(result.code(), result.message())
)
}
}
}
}
} // End of getNavNFC
MutableSharedFlow
를 통해서 네트워크 상태값을 관리하게 되는데 이 Flow의 상태값을 직접 emit()
으로 지정해 주어 상태를 변하게 할 수 있다.
_postNFCIdResponseSharedFlow
을 emit()
으로 상태값을 변경하는데, 여기서 Flow와 LiveData를 통한 Retrofit 통신처리의 차이점이 많이 드러난다.
emit()
을 짧막하게 설명하자면 LiveData에서 setValue
와 postValue
가 있는데, emit()
은 postValue
의 역할을 한다고 보면된다.
LiveData로 Retrofit 통신을 할 때는 ViewModel 부분에서 ViewModelScope를 통해서 nfcRepo.postNFCId(nfcId)
를 실행하게 된다. 이전 jetpack에서는 Repository의 Api interface를 호출하는게 전부였지만 Flow는 방식이 조금 달라진다.
Flow를 사용하면 여러가지 함수를 사용해서 Flow의 상태에 따라 어떤 상태값으로 지정할 지 커스텀이 훨씬 쉬워진다는 장단점이있다.
이런 부분에서 봤을 때, Flow를 한번 사용하게 되면 굳이(?) LiveData를 써야되나 라는 생각이 드는데 구글에서는 UI를 업데이트 하는 부분에서나 통신부를 분리해서 LiveData와 Flow를 같이 쓰는 것을 권장한다고는 하는데, LiveData와 Flow 둘다 Kotlin 코드이긴 하나 LiveData는 사실상 안드로이드에서 밖에 쓰이지 않기 때문에 Flow가 Kotlin 언어를 사용하여 Android개발을 하는데는 더 적합하지 않을까 라고 조심스럽게 생각해본다.
순서는 간단하게 함수가 호출되서 Repository의 Api 함수를 호출하게 되면 viewModelScope.launch
를 통해서 Coroutine으로 감싸서 비동기처리를 시작하게 된다.
onStart
에서는 가장 먼저 시작하면서 지정할 상태값을 설정하면 되는데 기본적으로 통신을 하는 동안 시간이 얼마나 걸릴지 모르니 당연히 처음 상태는 Loading()
상태로 지정해서 SharedFlow
로 상태값을 변환해준다.
.catch
부분에서는 에러가 발생했을 때 상태를 처리하는데 sealed class로 만들어놓은 상태에서 Error 타입으로 지정하여 catch에서 잡힌 에러 메세지를 매개변수로 넣어주면 된다. 이렇게 되면 SharedFlow
의 상태값은 Error 상태로 지정되게 된다.
collectLatest
부분에서는 에러가 생기지 않고 정상동작을 하는 부분이다.
result
를 결과값으로 사용하는데,nfcRepo.postNFCId(nfcId)
에서 나온 결과값에 따라서 처리한다.
만약 결과값인 result
가 isSuccessful이면 SharedFlow
를 Success상태로 emit
하고,
errorBody()가 null값이 아닐 경우에는 SharedFlow
를 Error 상태로 emit
한다.
Repositry.kt
// ==================================== postNFCData ====================================
suspend fun postNFCId(nfcId: Int): Flow<Response<NFC>> = flow {
val requestBodyJson = JsonObject().apply {
addProperty("id", nfcId)
}
emit(nfcApi.postNFCId(requestBodyJson))
}.flowOn(Dispatchers.IO)
Screen.kt
@Composable
fun NFCStartScreen(
lifecycleOwner: LifecycleOwner = LocalLifecycleOwner.current,
navController: NavController,
nfcViewModel: NFCViewModel = hiltViewModel(LocalContext.current as ComponentActivity),
nfcResponseViewModel: NFCResponseViewModel = hiltViewModel()
) {
// ... 일단 생략
// .. 아래에서 설명하면서 같이 보여줌
val nfcState = nfcViewModel.nfcState.collectAsState()
val nfcScreenState = remember { nfcState }
LaunchedEffect(key1 = nfcScreenState.value) {
if (nfcScreenState.value == "1") {
nfcResponseViewModel.postNFCId(nfcScreenState.value.toInt())
} else if (nfcScreenState.value == "SECOND") {
navController.navigate(route = Screen.Test.route)
}
}
NFCStartContent(nfcData = nfcScreenState.value)
} // End of NFCStartScreen
위의 Screen 예시는 NFC를 태그했을 때 데이터가 넘어오면 NFC에 저장되어 있는 값을 서버로 보내는 Screen이다.
기존의 Jetpack에서 Retofit을 사용한 예시는 Fragment에서 LiveData의 Observer를 통해서 상태를 관리했겠지만, 이번에는 Flow를 활용하고, observer를 직접적으로 사용할 수 없기 때문에, postNFCIdResponseSharedFlowState
의 값을 collectAsState()
를 통해서 상태 값이 변하게되면 내부의 코드가 value
에 따라서 동작하도록 설계되었다.
ViewModel.kt
@HiltViewModel
class NFCViewModel @Inject constructor(
) : ViewModel() {
private val _nfcState = MutableStateFlow("")
val nfcState = _nfcState.asStateFlow()
private val _getNFCData = mutableStateOf<NFC?>(null)
val getNFCData = _getNFCData
fun setNFCData(newNFCData: NFC) {
_getNFCData.value = newNFCData
} // End of setNFCData
fun setNFCState(newNFCState: String) {
_nfcState.value = newNFCState
} // End of setNFCState
private val _sharedNFCStateFlow = MutableSharedFlow<String>()
val sharedNFCStateFlow = _sharedNFCStateFlow.asSharedFlow()
fun setNFCSharedFlow(newSharedNFCState: String) {
viewModelScope.launch {
_sharedNFCStateFlow.emit(newSharedNFCState)
}
} // End of setNFCSharedFlow
} // End of NFCViewModel class
ViewModel에서는 viewModelScope를 사용해서 비동기처리를 통해
Repository에 있는 메소드를 실행한다.
Repository.kt
// ====================== postNFCData ==================
suspend fun postNFCId(nfcId: Int): Flow<Response<NFC>> = flow {
val requestBodyJson = JsonObject().apply {
addProperty("id", nfcId)
}
emit(nfcApi.postNFCId(requestBodyJson))
}.flowOn(Dispatchers.IO)
위의 함수는 viewModel에서 넘겨받는 매개변수 nfcId
를 JsonBody타입으로 "id"를 key값으로 nfc의 데이터를 담아서 보내게 된다.
이전의 LiveData를 사용했을 때는 postValue()
를 사용해서 ViewModel의 LiveData가 바라보도록 했지만, 현재는 Repository에서는 Flow를 통한 비동기 처리로 진행을 하여 emit()
을 통해 실행하도록 구현했다.
.flowOn()
을 통해서 쓰레드 종류를 설정할 수 있다.
이제 이 함수를 return값인 Flow<Response<NFC>>
가 다시 ViewModel로 돌아간다.
ViewModel.kt
// ================================= getNavNFC =================================
private val _postNFCIdResponseSharedFlow = MutableSharedFlow<NetworkResult<NFC>>()
var postNFCIdResponseSharedFlow = _postNFCIdResponseSharedFlow
private set
fun postNFCId(nfcId: Int) {
viewModelScope.launch {
nfcRepo.postNFCId(nfcId).onStart {
_postNFCIdResponseSharedFlow.emit(NetworkResult.Loading())
}.catch {
_postNFCIdResponseSharedFlow.emit(
NetworkResult.Error(
null, it.message, it.cause
)
)
}.collectLatest {
result ->${result.body()}")
when {
result.isSuccessful -> {
_postNFCIdResponseSharedFlow.emit(
NetworkResult.Success(result.body()!!)
)
}
result.errorBody() != null -> {
_postNFCIdResponseSharedFlow.emit(
NetworkResult.Error(result.code(), result.message())
)
}
}
}
}
} // End of getNavNFC
위에서 설명한 ViewModel부분이 여기로 다시 돌아온다.
Repository의 API호출 구현체와 결괏값의 상태를 따라서 ViewModel에서 구현할 수 있다.
해당 결과값을 Screen으로 보내줄 때는 다시 SharedFlow에 담아서 Screen이 ViewModel의 데이터를 참조할 수 있도록 구현하면 된다.
여기서는 _postNFCIdResponseSharedFlow
상태를 emit()
하여 해당 SharedFlow를 가지고 Screen에서 특정 상황이나 결과값에 따라 View를 구성할 것 이다.
Screen.kt
val postNFCIdResponseSharedFlowState =
nfcResponseViewModel.postNFCIdResponseSharedFlow.collectAsState(null)
LaunchedEffect(key1 = postNFCIdResponseSharedFlowState.value) {
when (postNFCIdResponseSharedFlowState.value) {
is NetworkResult.Success -> {
val data = postNFCIdResponseSharedFlowState.value!!.data
if (postNFCIdResponseSharedFlowState.value!!.data != null) {
nfcViewModel.setNFCData(data as NFC)
navController.navigate(route = Screen.Main.route) {
popUpTo(Screen.NFCStart.route) {
inclusive = true
}
}
}
}
is NetworkResult.Loading -> {
// 로딩에서 보이게 될 화면 작성
}
is NetworkResult.Error -> {
// 에러시 보이게 될 화면이나 이벤트 코드 작성
// 에러 메세지나 코드에 따라서 다르게 동작할 수 있음
}
else -> {
// null값이 들어가기 때문에 Sealed Class이지만 else문이 필요함
// Nullable이 아니었다면, 없었음
}
}
}
아까 위에서 생략된 Screen파트가 나온다.
ViewModel의 SharedFlow를 .collectAsState()
를 만들어 이 값을 변수로 빼놓고
이 값이 변할때 마다 내부가 동작하도록 해서 Flow의 상태에 따라서 Screen이 알맞게 동작하도록 구현하기만 하면 된다.
Screen에서 사용하기 위한 ViewModel 참조값의 타입이 현재 <NetworkResult<NFC>>
으로 되어있기 때문에
.value
에는 NetworkResult의 Sealed cLass가 담겨있고 내부인 .data
에는 NetworkResult의 상태값에 담긴 NFC 데이터가 들어가 있을 것이다.
현재 위의 예시에서는 Success일 때만 구현체가 만들어져 있고, Error나 Loading에 대한 구현체가 특별히 없다.
만약 로딩을 하는 부분에서 프로그레스바가 보이길 원한다면 is NetworkResult.Loading -> {
내부에 프로그레스바가 동작하도록 구현하면 될 것이고
에러시 특정 메세지나 화면이 보이길 원한다면 마찬가지로 is NetworkResult.Error ->{
내부에 본인이 원하는 에러처리 View를 구현하면 된다.
따라서 우리는
val data = postNFCIdResponseSharedFlowState.value!!.data
위 부분을 통해서 NFC객체의 data를 꺼내서 사용하기만 하면된다.
이전 까지는 LiveData를 통해서 서버와의 통신코드를 구현했었는데,
Flow를 사용해보니까 LiveData보다는 Flow가 .catch
, .retry
등의 여러가지 제공하는 함수가 많다보니 더 사용하기도 좋고, 성능면에서도 좋은 것 같다는 생각이든다.
물론 장단점이 있겠지만, Flow자료를 찾아보면서 LiveData가 Deprecated된다는 얘기가 종종 보이던데...
좀 무섭긴 하지만,, Flow를 써보니 LiveData는 없어도 되는 부분이긴 한 것 같다.
Kotlin 코드이긴 하지만 사실상 안드로이드에서만 사용되는 부분이다 보니 없는게 더 낫지 않나 싶다는 생각이들었다...