이번 영상에서는 Retrofit으로 얻어진 응답에 Android App Architecture 구조를 적용하는 방법에 대해 알아보도록 하겠습니다.
우선은 data/repository 패키지 밑에 다음과 같이 리포지토리 인터페이스를 작성합니다.
interface BookSearchRepository {
suspend fun searchBooks(
query: String,
sort: String,
page: Int,
size: Int,
): Response<SearchResponse>
}
그리고 이 인터페이스를 구현하는 클래스를 작성합니다. Retrofit api의 searchBooks를 실행시켜서 Response<BooksResponse>를 반환받도록 구현하면 됩니다.
class BookSearchRepositoryImpl : BookSearchRepository {
override suspend fun searchBooks(
query: String,
sort: String,
page: Int,
size: Int,
): Response<BookResponse> {
return api.searchBooks(query, sort, page, size)
}
}
다음은 이 리포지토리에서 받아온 데이터를 화면에 표시하는 ViewModel을 구성합니다. 다음과 같이 Lifecycle과 코루틴 Dependency를 추가합니다.
// Lifecycle
implementation 'androidx.lifecycle:lifecycle-viewmodel-ktx:2.4.1'
implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.4.1'
// Coroutine
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.1'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.1'
그리고 ui/viewmodel 패키지 아래에 ViewModel을 만들어줍니다. 리포지토리의 searchBooks를 코루틴 내부에서 수행하는 searchBooks 함수를 만들었습니다. 코루틴은 백그라운드 작업을 더 용이하게 수행하게 해 주는 기술로, 구체적인 원리와 동작방식에 대해서는 보강 이론인 Coroutine 기초를 참고하시기 바랍니다.
searchBooks에서는 리포지토리의 searchBooks를 실행하되 파라미터는 query 이외에는 고정값을 전달하도록 했습니다. Retrofit 서비스의 반환값은 MutableLiveData인 _searchResult에 저장하고, 외부에는 수정할 수 없는 LiveData로 변환한 searchResult를 노출하도록 합니다.
class BookSearchViewModel(
private val bookSearchRepository: BookSearchRepository,
) : ViewModel() {
// Api
private val _searchResult = MutableLiveData<SearchResponse>()
val searchResult: LiveData<SearchResponse> get() = _searchResult
fun searchBooks(query: String) = viewModelScope.launch {
val response = bookSearchRepository.searchBooks(query, "accuracy", 1, 15)
if (response.isSuccessful) {
response.body()?.let { body ->
_searchResult.postValue(body)
}
}
}
}
BookSearchViewModel은 초기값으로 BookSearchRepository를 전달받아야 하는데 ViewModel은 그 자체로는 생성시 초기값을 전달받을 수 없기 때문에 다음과 같이 초기값을 전달받아 뷰모델을 반환하는 ViewModelProviderFactory를 만들어 줍니다. isAssignableFrom에 리플렉션으로 BookSearchViewModel을 전달해서 만들어야 할 뷰모델이 인지 확인하고, 맞다면 bookSearchRepository를 담아 BookSearchViewModel을 반환하는 동작을 하게 됩니다.
@Suppress("UNCHECKED_CAST")
class BookSearchViewModelProviderFactory(
private val bookSearchRepository: BookSearchRepository
) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(BookSearchViewModel::class.java)) {
return BookSearchViewModel(bookSearchRepository) as T
}
throw IllegalArgumentException("ViewModel class not found")
}
}
마지막으로 MainActivity에서 뷰모델을 초기화시켜줍니다. 리포지토리 > 팩토리 > 뷰모델의 순서로 초기화하면 됩니다.
class MainActivity : AppCompatActivity() {
...
+ lateinit var bookSearchViewModel: BookSearchViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(binding.root)
setupBottomNavigationView()
if (savedInstanceState == null) {
binding.bottomNavigationView.selectedItemId = R.id.search_fragment
}
+ val bookSearchRepository = BookSearchRepositoryImpl()
+ val factory = BookSearchViewModelProviderFactory(bookSearchRepository)
+ bookSearchViewModel = ViewModelProvider(this, factory)[BookSearchViewModel::class.java]
}
...
}