본 포스트는 안드로이드 기초 코드랩을 학습하고 정리한 내용입니다.
https://developer.android.com/codelabs/kotlin-android-training-repository?index=..%2F..android-kotlin-fundamentals&hl=ko#0
이번에 만들어볼 앱은 Google Android developer relations team 에서 만든 튜토리얼 영상을 리스트 형식으로 보여주는 앱이다. 앱은 Retrofit을 이용해 video url list를 서버로부터 받아온다.
그리고 리싸이클러뷰를 이용해서 화면에 뿌려주는데 앱은 뷰모델과 라이브데이터를 이용해서 데이터를 hold해두고 ui상에 display해준다.
online-only 라는 것이다.
그래서 사용자는 인터넷에 연결이 되어있어야하는데 이번 코드랩에선 네트워크가 아닌 로컬 디비를 이용해서 데이터를 오프라인 캐싱하는 기능을 배울것이다.
앱이 서버로부터 데이터를 가져오면 앱은 디바이스 저장소에 데이터를 저장할 수 있다. 이를 캐싱이라고 하는데 캐싱을 하는 이유는 디바이스가 offline 모드로 전환 되었을 때, 같은 데이터에 다시 접근하고 싶을 때 캐싱된 데이터를 사용할 수 있기 때문이다.
key:value 쌍으로 제공되는 SharedPreference를 이용할 수 있다.
거대한 양의 구조화된 데이터를 저장하기에는 적합하지 않다.
앱 패키지 이름은 안드로이드 파일 시스템 내에 특정한 위치에 있는 내부 저장 디렉터리를 지정한다.
파일 시스템 내의 디렉토리는 앱마다 private하게 유지되며, 앱이 삭제되면 자동으로 지워진다.역시 복잡하고 구조화된 데이터를 저장하기엔 적합하지 않다.
local sqlite database를 이용하는것이다.
로컬 디비에 저장하는것도, 간단한 데이터의 경우 프리퍼런스를 이용해서 저장하는것도 모두 캐싱에 해당되는 이야기다.
가장 중요한 컨셉은 앱이 실행될 때마다 매번 네트워크에 요청을 시도하지 말라는 것이다. 대신 디비에서 데이터를 가져와서 화면에 뿌려주는것이 좋다. 이렇게 하는게 앱 로딩시간을 줄여 줄 수 있다.
네트워크를 이용해 데이터를 가져올 때 화면에 바로 뿌려주지 말고 우선 데이터베이스에 저장해두자.
api 호출 → 로컬 데이터베이스 저장 → 로컬 데이터베이스 접근 후 데이터 화면에 표시 이 흐름을 사용하자. 그래야 오프라인 모드에서도 항상 데이터를 가져올 수 있기 때문이다. 앱이 오프라인 상태가 되어도 여전히 로컬 캐시데이터를 가져올 수 있다.
앱의 영역에서 데이터 소스영역을 분리 시키고 앱에 data에 접근할 수 있는 Clean API를 제공해줄 수 있다.
레포지토리 클래스를 사용하는것이 코드를 분리시키고 아키텍처 측면에서 가장 추천하는 방법이다.레포지토리 모듈에서는 데이터 작업을 처리하고 여러 백엔드를 사용할 수 있게 해준다. 실제로 코드랩이 아닌 앱을 구현하게 되면 데이터를 네트워크에서 가져 올 것인지 ? 아니면 로컬 디비에 캐싱한 데이터로 부터 가져올 것인지 에 대한 로직을 구현하게 된다.
레포지토리 패턴은 이런 경우 코드를 모듈화 하고 테스트 가능하게 도와준다. repository를 쉽게 mock up하고 나머지 코드를 테스트 할 수 있게된다.
이전 챕터에서 룸 디비를 생성하고 crud하는 쿼리를 dao 형태로 생성해두었는데 Room db는 `오프라인 캐시를 관리할 로직을 가지고 있지 않다. 오직 data에 접근하고 가져오는 메서드만 있을뿐` 레포지토리는 네트워크에서 데이터를 가져오고 database를 최신 상태로 유지하는 로직을 구현할 수 있다.
안드로이드의 데이터베이스는 파일 시스템이나, 디스크에 저장하기 위해 disk I/O를 반드시 수행하게 된다.
disk로부터 데이터를 읽고 쓰는 disk I/O 작업은 느리고 항상 I/O 작업이 끝나기 전 까지 해당 쓰레드를 블락 시켜버린다. 이러한 이유로 disk I/O는 코루틴의 I/O dispatcher를 이용해 실행시켜야 한다.
I/O dispatcher는 블락되는 I/O 작업을 공유 쓰레드 풀을 이용해서 처리할 수 있도록 설계 되어있다.`
먼저 레포지토리 클래스를 만들자.
class VideosRepository(private val database: VideosDatabase) {
suspend fun refreshVideos() {
withContext(Dispatchers.IO) {
Timber.d("refresh videos is called");
val playlist = DevByteNetwork.devbytes.getPlaylist()
database.videoDao.insertAll(playlist.asDatabaseModel())
}
}
}
필요에 따라 api 호출 로직을 수행하는 레트로핏 프로퍼티를 추가해줄 수 있다
그리고 디비에서 데이터를 받아오는 로직을 추가해준다.
val videos: LiveData<List<DevByteVideo>> = Transformations.map(database.videoDao.getVideos()) {
it.asDomainModel()
}
위 코드에서 유심히 살펴봐야 할 부분이 있는데 Transformations.map()과 database.videoDao.getVideos() 메서드다.
database.videoDao.getVidoes() 메서드를 살펴보면
@Dao
interface VideoDao{
@Query("select * from databasevideo")
fun getVideos(): LiveData<List<DatabaseVideo>>
}
refreshVideos() 호출
database.videoDao.insertAll() 호출
데이터베이스에 데이터를 새로 넣게 될 경우 기존 db에서 업데이트가 발생한것이므로 getVideos()자동으로 호출 (즉 라이브데이터 갱신)
Transformations.map() 메서드 역시 첫번째 인자로 받는 라이브데이터가 갱신 될 경우 자동으로 트리거 되기 때문에 vidoes 데이터 갱신
이런 흐름을 가질 수 있게된다.
기존에 만들어 둔 DevByteViewModel에서 레포지토리를 생성하자. 우선 멤버변수로 Repository를 추가해주자.
/**
* The data source this ViewModel will fetch results from.
*/
private val videosRepository = VideosRepository(getDatabase(application))
이제 Repository에서 데이터를 가져오는 로직을 추가하자
private fun refreshDataFromRepository() {
viewModelScope.launch {
try {
videosRepository.refreshVideos()
_eventNetworkError.value = false
_isNetworkErrorShown.value = false
} catch (networkError: IOException) {
// Show a Toast error message and hide the progress bar.
if(playlist.value.isNullOrEmpty())
_eventNetworkError.value = true
}
}
}
코루틴으로 구현 했기 때문에 쓰레드를 신경 쓸 필요 없이 동기처럼 코드를 작성해줄 수 있다.
만약 refreshVidoes()메서드에서 에러가 발생해서 IOException을 던진다면 catch 구문에 걸려서 catch로직이 수행 될 것이다.위 로직이 수행되면 repository내에서 서버에서 데이터를 받아서 로컬 디비에 저장 하게 되는데 아까 위에서 설명했던 repository.videos live data도 자동으로 갱신되게 된다.
해당 데이터를 viewModel에서 참조할 수 있게 해주자//viewModel 코드
val playlist = videosRepository.videos
// Fragment 코드
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
super.onViewCreated(view, savedInstanceState)
viewModel.playlist.observe(viewLifecycleOwner, Observer<List<DevByteVideo>> { videos ->
videos?.apply {
viewModelAdapter?.videos = videos
}
})
}
즉 정리하자면 ViewModel 생성 → repository.refreshVideos() 호출 → 서버에서 데이터 fetch, 로컬 db 저장, 자동으로 갱신된 데이터 fetch → liveData observer onChanged() 콜백 메서드 트리거 → recyclerView data 갱신 및 notifyDataSetChanged() 호출 → 화면에 표시
이렇게 동작 하게 된다.
코드랩에서는 어떻게 데이터를 로컬 디비에 캐싱해야 하는지 방법과 repository 패턴에 대해서 설명 해주고있다. 지금 코드는 viewModel이 init()될 때마다 위 로직이 트리거 되는데 실제 앱을 구현하게 될 경우 더 복잡하고 다양한 데이터 refresh 방법을 구현 해야할 것이다.