일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- LinkedList
- graphQL
- ubuntu python
- mvvm
- Apollo GraphQL
- 우분투 파이썬
- Apollo Server
- Android test
- Design Pattern
- Data structure
- 안드로이드 디자인패턴
- Kotlin
- 웹크롤링
- java
- MVVM pattern
- Nexus GraphQL
- flutter
- Android
- 유니티
- 안드로이드 mvp
- 파이썬 크롤링
- unit test
- 안드로이드
- 자바
- dagger-hilt
- 자바기초
- 안드로이드 테스트
- prisma
- Dependency Injection
- PYTHON
- Today
- Total
Hun's Blog
Android Dagger-Hilt 적용기 (6) - ViewModel + Repository(Sandwich) 본문
아래의 오픈소스를 통해서 분석하고 학습하여 개인프로젝트에 Dagger-Hilt적용
github.com/android/architecture-samples/tree/dev-hilt
현재까지의 진행상황이다.
sandwich는 Pokedex 의 skydoves님이 제공하며 retrofit을 사용 할 때 네트워크 response와 error를 손쉽게 핸들링하도록 도와주는 라이브러리이다.
처음에는 sandwich를 사용하지 않고 구현한 후에 적용하였다.
이렇게 해야 라이브러리의 편리함을 절실히 느낄 수 있는 듯..
먼저 viewModel을 살펴보도록 하자.
class MainViewModel @ViewModelInject constructor(
private val mainRepository: MainRepository,
@Assisted private val savedStateHandle: SavedStateHandle
) : LiveCoroutinesViewModel() {
private var moviesFetchingLiveData: MutableLiveData<Int> = MutableLiveData(0)
val movies: LiveData<List<Movie>>
val isLoading: ObservableBoolean = ObservableBoolean(false)
private val _toastLiveData: MutableLiveData<String> = MutableLiveData()
val toastLiveData: LiveData<String> get() = _toastLiveData
init {
movies = moviesFetchingLiveData.switchMap {
launchOnViewModelScope {
isLoading.set(true)
this.mainRepository.getMovies(
page = it,
onSuccess = { isLoading.set(false) },
onError = { _toastLiveData.postValue(it) }
).asLiveData()
}
}
}
@MainThread
fun fetchMovieList(page: Int) {
moviesFetchingLiveData.value = page
}
}
기본 viewModel이 아닌 LiveCoroutinesViewModel()을 상속받고 있고 해당 클래스에는 다음과 같은 inline fun 메서드가 있다.
inline fun <T> launchOnViewModelScope(
crossinline block: suspend () -> LiveData<T>
) : LiveData<T> {
return liveData(viewModelScope.coroutineContext + Dispatchers.IO) {
emitSource(block())
}
}
그리고 해당 메서드는 viewModel의 init 블럭에서 호출되고 있다.
해당 프로젝트는 Retrofit2 + Coroutine을 활용하고 있고 viewModel 에서 사용하기위해 viewModelScope.lanucn(Dispatchers.IO) 블럭을 만들어서 구현해야한다.
mainViewModel의 init { } 블럭에서 처럼 launchOnViewModelScope 메서드를 사용하면 코루틴 블럭을 자동 생성하고 mainRepository로 부터 전달받은 Movie 리스트 객체를 LiveData<List<Movie>>에 담아 반환하는 것 까지 손쉽게 구현할 수 있다.
반환된 LiveData<List<Movie>>는 movies에 입력되고 해당 LiveData는 MainActivity의 recycler view 어댑터에서 관찰하고 있다.
<androidx.recyclerview.widget.RecyclerView
...
app:movies="@{viewModel.movies}"
.../>
여기서 어댑터의 동작을 설명하려면 너무 먼 길을 돌아와야하기 때문에 다시 viewModel로 돌아가보자.
launchOnViewModelScope{} 블럭은 moviesFetchingLiveData.switchMap {} 블럭에서 호출된다.
private var moviesFetchingLiveData: MutableLiveData<Int> = MutableLiveData(0) 처럼 MutableLiveData이다.
즉, int{} 블럭은 다음과 같이 동작한다.
처음 viewModel이 생성 될 때 moviesFetchingLiveData의 데이터 변동에 따라 launchOnViewModelScope 블럭이 실행되어 movies LiveData에 담겨진다.
moviesFetchingLiveData 의 타입이 int이고 이것이 바로 Page를 뜻한다.
그렇다면 페이지의 변동은 어디서 이루어질까? 바로 viewModel의 fetchMovieList()를 통해서 변동된다.
@MainThread
fun fetchMovieList(page: Int, refresh: Boolean) {
moviesFetchingLiveData.value = page
}
그리고 리사이클러뷰 xml 에서 app:paginationMovies를 통해 fetchMovieList() 를 실행하는 메서드에 viewModel을 입력하여 호출한다.
<androidx.recyclerview.widget.RecyclerView
...
app:movies="@{viewModel.movies}"
app:paginationMovies="@{viewModel}"
.../>
여기서 자세히 설명하기에는 내용이 너무 길어지기 때문에 간략히만 설명하도록 하겠다.
paginationMovies 메서드는 RecyclerViewPagination 클래스를 생성하는데 해당 클래스의 생성과 동시에 init 블럭에서 fetchMovieList() 에 1을 입력하여 메서드를 실행한다. 즉, 1page를 api 서버로부터 받아온다.
RecyclerViewPagination이라는 명칭처럼 이곳에서 페이지네이션이 구현된다. 나중에 자세히..
자 이제 repository의 getMovies 함수를 살펴보자. 아래는 Sandwich를 활용하지 않은 코드이다.
...
@WorkerThread
suspend fun getMovies(
page: Int,
onSuccess: () -> Unit,
onError: (String) -> Unit
) = flow {
val movies = movieDao.getMovies(page)
if (movies.isEmpty()) {
val newMovies = ArrayList<Movie>()
try {
retrofitClient.fetchMovies(page).run {
for (movie in this.results) {
movie.page = this.page
newMovies.add(movie)
}
movieDao.saveMovies(newMovies)
emit(newMovies)
onSuccess()
}
} catch (e: Exception) {
e.message?.let { onError(it) }
}
} else {
emit(movies)
onSuccess()
}
}.flowOn(Dispatchers.IO)
...
getMovies()는 page를 입력받고 상황에 따라 onSuccess()와 onError를 반환한다.
그 다음으로 아주 중요한 블럭이 나오는데 ..
**flow.{ }.flowOn(Dispatchers.IO)
suspend fun은 비동기로 동작하면서 하나의 값을 반환한다.
Flow는 비동기로 동작하면서 여러개의 값을 반환하는 경우 사용하는 코루틴 Builder이다.
해당 블럭안에서 list를 가져오는 로직을 구현한 후 emit()에 결과를 담아 반환하게 된다.
반환된 값은 collect {} 블럭을 통해 수집할 수있는데 viewModel의 launchOnViewModelScope 블럭에서 이 부분까지 자동으로 수집해준다.
flow 내부 코드는 간단하다. 로컬에서 데이터를 요청하고 있으면 onSuccess + 리스트를 리턴하고 없으면 네트워크를 통해 데이터를 요청하여 onSuccess + 리스트를 리턴한다.
retrofit 메서드는 try {} catch {} 를 통해 exception을 캐치하고 있고, exception이 발생할 경우 onError()를 반환한다.
try {} catch {} 하나만으로 HTTP요청에 대한 error와 exception을 전부 캐치하는건 무리가 있다. 더 중요한것은 캐치를 하지못하면 앱이 멈춘다는 것이다. 수많은 에러코드와 exception에 대처하기 위한 방대한 코드가 추가되야함을 의미한다.
이럴 때 바로 Sandwich가 유용하게 활용된다.
다음은 샌드위치를 사용해서 repository의 getMovies()를 변동한 코드이다.
@WorkerThread
suspend fun getMovies(
page: Int,
onSuccess: () -> Unit,
onError: (String) -> Unit
) = flow {
var movies = movieDao.getMovies(page)
if (movies.isEmpty()) {
val response = retrofitClient.getMovies(page)
response.suspendOnSuccess {
data.whatIfNotNull { response ->
movies = response.results
movies.forEach { movie -> movie.page = page }
movieDao.saveMovies(movies)
emit(movieDao.getMovies(page))
onSuccess()
}
}
.onError {
onError(message())
}
.onException {
onError(message())
}
} else {
emit(movies)
onSuccess()
}
}.flowOn(Dispatchers.IO)
local은 동일하다.
반면 retrofit의 getMovies()메서드는 우선 reponse 변수에 담는다.
그리고 response 변수는 Sandwich의 suspensOnSuccess { } 블럭을 실행할 수 있다.
해당 블럭은 data {} / onError{} / onException{} 블럭으로 나누어 결과값을 반환하게 된다. 위에서 고민했던 수많은 Error 코드와 Exception에 대한 방안을 위처럼 짧은 코드로 해결할 수 있게 되었다.
data블럭에선 whatIfNotNull 블럭이 함께 실행되는데 이것 또한 skydoves님이 제공하는 라이브러리이다.
각종 if-else 의 nullable 과 boolean을 손쉽게 표현하기 위한 라이브러리이다.
여기서 끝이아니다. response가 suspensOnSuccess { }블럭을 사용할 수 있는 이유는 해당 객체가 Sandwich의 ApiRespnse<> 에 감싸져 있기 때문이다. 다음과 같다.
@GET("discover/movie")
suspend fun getMovies(
@Query("page") page: String,
@Query("api_key") api_key: String,
@Query("sort_by") sort_by: String,
@Query("language") language: String,
): ApiResponse<MovieResponse>
retrofit의 getMovies 메서드는 네트워크 요청 결과 값을 sandwich의 ApiResponse<MovieResponse>에 담게되고 sandwich가 결과 값을 검사하여 data { emmit(networkResult), onSuccess() } / onError { onError(message()) } / onException { onError(message()) }으로 구분하여 반환해준다.
참으로 편리하다!!
ApiResponse<>에 담기는 MovieResponse 객체는 각각의 Network API가 어떻게 데이터를 반환하느냐에 따라 다르게 적용되기 때문에 자세한 설명은 생략
다음 글에선 pagination에 대해 알아보도록 하겠다.
'Android' 카테고리의 다른 글
개인 프로젝트(Bookmark-kotlin)를 통해 알아보는 MVC-MVP-MVVM (0) | 2020.12.03 |
---|---|
Start an activity using an animation (0) | 2020.11.22 |
[번역]Coroutines on Android (part I): Getting the background (0) | 2020.11.11 |
Android Dagger-Hilt 적용기 (5) - Repositoy (3) | 2020.11.11 |
Android Dagger-Hilt 적용기 (4) - MVVM (0) | 2020.11.11 |