-
Notifications
You must be signed in to change notification settings - Fork 0
3. Presentation
presentation layer에서 작성하는 코드들은 이미 많은 정보와 예시들이 google에 올라와 있습니다.
이번 wiki에서는 domain, data 레이어의 기능을 어떻게 presentation에서 사용하였는지를 중점으로 보여드리겠습니다.
컨벤션은 Fragment, Activity, Adapter 등등 여러 예시들과 똑같습니다. 따라서 생략하겠습니다.
ViewModel은 Fragment, Activity에 종속되지 않는 life cycle을 가지고 있으며
화면 회전과 같은 이벤트에도 데이터를 유지하는 클래스입니다.
특히 viewmodel scope를 따로 가지고 있는데요, 이곳에서 rest api를 활용하여 여러 데이터를 주고 받는 코드를 작성 합니다.
이 때에 많은 중복 코드가 발생하여 저는 메서드를 따로 정의하여 활용하곤 합니다.
abstract class BaseViewModel(application: Application) : AndroidViewModel(application) {
private val _errorMessageFlow = MutableStateFlow("")
val errorMessageFlow = _errorMessageFlow.asStateFlow()
protected val context: Context by lazy {
getApplication()
}
protected fun <T : ApiQuery, A : ApiBody<MetaData, Any>> runApiUseCase(
apiUseCase: ApiUseCase<T, A>,
query: T,
resultFlow: MutableStateFlow<ApiResponse<A>?>,
): Job {
return viewModelScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
resultFlow.emit(ApiResponse.Loading())
val result = apiUseCase(query)
result.onSuccess {
resultFlow.emit(ApiResponse.Success(it))
}.onError { _, message ->
_errorMessageFlow.emit("$message")
}.onException {
_errorMessageFlow.emit("$it")
}
}
}
protected fun <T : ApiQuery, A : ApiBody<MetaData, Any>> runApiUseCase(
apiUseCase: ApiUseCase<T, A>,
query: T,
onFinished: (A) -> Unit,
): Job {
return viewModelScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
val result = apiUseCase(query)
result.onSuccess {
onFinished(it)
}.onError { _, message ->
_errorMessageFlow.emit("$message")
}.onException {
_errorMessageFlow.emit("$it")
}
}
}
private val coroutineExceptionHandler =
CoroutineExceptionHandler { _, throwable ->
throwable.printStackTrace()
}
}저는 Domain Layer에서 UseCase 중에 Rest API를 활용하여 데이터를 가져오는 UseCase들을 ApiUseCase로 추상화 하였습니다.
따라서 이러한 UseCase들은 ApiUseCase 타입으로 묶일 수 있습니다.
예시)
abstract class GetBusInStationUseCase : ApiUseCase<BusInStationQuery, BusInStationInfoBody> {
abstract override suspend operator fun invoke(query: BusInStationQuery): ApiResponse<BusInStationInfoBody>
}
class GetBusInStationUseCaseImpl(private val stationRepository: StationRepository) :
GetBusInStationUseCase() {
override suspend operator fun invoke(query: BusInStationQuery): ApiResponse<BusInStationInfoBody> {
return stationRepository.getBusInStation(query)
}
}이러한 UseCase들을 처리하는 메소드 "runApiUseCase"를 활용하여 아래와 같이 타입에 상관 없이 일괄적으로 success, error, loading을 처리할 수 있습니다.
//apiUseCase: 실행 시킬 추상화된 useCase(Api를 호출 시키는 useCase로, 항상 ApiQuery를 매개변수로 받고, ApiBody를 반환하는 invoke 함수이다.)
//query: useCase에 넣을 쿼리
//resultFlow: 뷰에서 관찰 중인 StateFlow
protected fun <T : ApiQuery, A : ApiBody<MetaData, Any>> runApiUseCase(
apiUseCase: ApiUseCase<T, A>,
query: T,
resultFlow: MutableStateFlow<ApiResponse<A>?>,
): Job {
return viewModelScope.launch(Dispatchers.IO + coroutineExceptionHandler) {
resultFlow.emit(ApiResponse.Loading()) //통신이 시작되어 먼저 Loading이 되었다는 것을 Flow로 방출한다.
val result = apiUseCase(query) //받은 useCase를 실행하여 결과 값을 받는다.
result.onSuccess {
resultFlow.emit(ApiResponse.Success(it)) //성공이면 result를 방출한다.
}.onError { _, message ->
_errorMessageFlow.emit("$message")
}.onException {
_errorMessageFlow.emit("$it")
}
}
}그리고 위의 코드에서 일괄적으로 error를 처리하기 위해 errorMessageFlow를 필드로 선언하였습니다.
private val _errorMessageFlow = MutableStateFlow("")
val errorMessageFlow = _errorMessageFlow.asStateFlow()이제 이 BaseViewModel을 상속받는 ViewModel들은 간단하게 Api호출을 할 수 있게 됩니다.
예시)
@HiltViewModel
class BusNumberSearchViewModel @Inject constructor(
application: Application,
private val getBusInStationUseCase: GetBusInStationUseCase
) : BaseViewModel(application) {
private val _searchedBusList = MutableStateFlow<ApiResponse<BusInStationInfoBody>?>(null)
val searchedBusList = _searchedBusList.asStateFlow()
fun getBusInStation(cityCode: String, stationId: String) {
val key = context.getString(com.moonyh.data.R.string.apiKey)
val query=BusInStationQuery(key, cityCode, stationId, 1, 15)
runApiUseCase(getBusInStationUseCase, query,_searchedBusList) //간단히 호출
}
}
또한 아래와 같이 쉽게 Fragment에서 데이터 변경 사항을 관찰하여 뷰에 데이터를 노출시킬수 있게 됩니다.
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.searchedBusList.collectLatest {
if(it==null)
return@collectLatest
it.onSuccess {result->
busNumberAdapter.submitList(result.items)
}.onLoading {
setFragmentLoading()
}
}
}
viewLifecycleOwner.lifecycleScope.launchWhenStarted {
viewModel.errorMessageFlow.collectLatest
//모든 에러를 사용자에게 일괄적으로 노출한다.
Toast.makeText(requireContext(),it,Toast.LENGTH_SHORT).show()
}
}