Setup and starter code walkthrough
Step 1: 스터터 앱을 다운로드 받고 실행해보기
Step 2: (Optional) 네트워크 지연 시뮬레이션해보기
DevByteViewModel.kt
private fun refreshDataFromNetwork() = viewModelScope.launch {
try {
...
} catch (networkError: IOException) {
delay(2000)
// Show a Toast error message and hide the progress bar.
_eventNetworkError.value = true
}
}
테스트 후에는 delay문을 지운다.
Step 3: 코드 탐색하기
Tip: 이는 network, domain 그리고 database object를 분리하는 모범사례이다. 이 전략은 separation of concerns 원칙을 따른다. 네트워크 응답이나 데이터베이스 스키마가 변하면 앱의 전체 코드를 수정하지 않고 앱 컴포넌트를 바꾸고 관리할 수 있다.
Concept: Caching
Add an offline cache
Key concept: 앱이 시작될 때마다 네트워크로 데이터를 가져오지 않는다. 대신 데이터베이스로부터 가져온 데이터를 띄운다. 이 기술은 앱 로딩 시간을 줄여준다.
Step 1: Room 의존성 추가하기
build.gradle (Module:app)
dependencies {
...
// Room dependency
def room_version = "2.1.0-alpha06"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"
}
Step 2: 데이터베이스 객체 추가하기
DatabaseEntities.kt
/**
* DatabaseVideo represents a video entity in the database.
*/
@Entity
data class DatabaseVideo constructor(
@PrimaryKey
val url: String,
val updated: String,
val title: String,
val description: String,
val thumbnail: String)
/**
* Map DatabaseVideos to domain entities
*/
fun List<DatabaseVideo>.asDomainModel(): List<DevByteVideo> {
return map {
DevByteVideo(
url = it.url,
title = it.title,
description = it.description,
updated = it.updated,
thumbnail = it.thumbnail)
}
}
DataTransferObjects.kt
/**
* Convert Network results to database objects
*/
fun NetworkVideoContainer.asDatabaseModel(): List<DatabaseVideo> {
return videos.map {
DatabaseVideo(
title = it.title,
description = it.description,
url = it.url,
updated = it.updated,
thumbnail = it.thumbnail)
}
}
Step 3: VideoDao
Room.kt
@Dao
interface VideoDao {
@Query("select * from databasevideo")
fun getVideos(): LiveData<List<DatabaseVideo>>
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll( videos: List<DatabaseVideo>)
}
간단함을 위해, 비디오 엔트리가 이미 데이터베이스에 존재하면 데이터베이스 엔트리를 덮어쓰기 한다. 이를 위해 conflict strategy를 REPLACE로 설정하는 onConflict 인자를 사용한다.
Step 4: RoomDatabase 구현하기
Room.kt
@Database(entities = [DatabaseVideo::class], version = 1)
abstract class VideosDatabase: RoomDatabase() {
abstract val videoDao: VideoDao
}
private lateinit var INSTANCE: VideosDatabase
fun getDatabase(context: Context): VideosDatabase {
synchronized(VideosDatabase::class.java) {
if (!::INSTANCE.isInitialized) {
INSTANCE = Room.databaseBuilder(context.applicationContext,
VideosDatabase::class.java,
"videos").build()
}
}
return INSTANCE
}
VideoDatabase 클래스를 Room 데이터베이스로 표시하기 위해 @Database 어노테이션을 사용한다. 이 데이터베이스에 소속되는 DatabaseVideo 엔터티를 선언한다.
singleton 오브젝트를 저장하기 위해 INSTANCE라는 변수를 생성한다. 동시에 열린 데이터베이스의 다중 인스턴스를 갖는 것을 막기 위해 VideoDatabase는 singleton이어야 한다.
Tip: .isInitialized Kotlin 값은 lateint property에 값이 할당되어 있으면 true, 아니면 false를 리턴한다.
Concept: Repositories
Create a repository
Step 1: 리파지토리 추가하기
VideosRepository.kt
/**
* Repository for fetching devbyte videos from the network and storing them on disk
*/
class VideosRepository(private val database: VideosDatabase) {
/**
* Refresh the videos stored in the offline cache.
*
* This function uses the IO dispatcher to ensure the database insert database operation
* happens on the IO dispatcher. By switching to the IO dispatcher using `withContext` this
* function is now safe to call from any thread including the Main thread.
*
*/
suspend fun refreshVideos() {
withContext(Dispatchers.IO) {
Timber.d("refresh videos is called");
val playlist = DevByteNetwork.devbytes.getPlaylist()
database.videoDao.insertAll(playlist.asDatabaseModel())
}
}
}
Note: Android 상의 데이터베이스는 파일 시스템이나 디스크에 저장되고 저장하기 위해서는 디스크 I/O를 반드시 수행해야 한다. 디스크 I/O나 디스크를 읽고 쓰는 것은 느리고 항상 수행이 완료될 때까지 현재 스레드를 블락한다. 이 때문에 I/O dispatcher에서 disk I/O를 실행해야 한다. 이 디스패처는 withContext(Dispatchers.IO) { ... }를 사용하여 블락하는 I/O 태스크를 공유된 풀(shared pool)로 옮기기 위해 설계되었다.
Step 2: 데이터베이스로부터 데이터 가져오기
VideosRepository.kt
class VideosRepository(private val database: VideosDatabase) {
val videos: LiveData<List<DevByteVideo>> = Transformations.map(database.videoDao.getVideos()) {
it.asDomainModel()
}
...
}
Integrate the repository using a refresh strategy
DevByteViewModel.kt
class DevByteViewModel(application: Application) : AndroidViewModel(application) {
...
/**
* The data source this ViewModel will fetch results from.
*/
private val videosRepository = VideosRepository(getDatabase(application))
/**
* A playlist of videos displayed on the screen.
*/
val playlist = videosRepository.videos
/**
* init{} is called immediately when this ViewModel is created.
*/
init {
refreshDataFromRepository()
}
...
/**
* Refresh data from the repository. Use a coroutine launch to run in a
* background thread.
*/
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
}
}
}
...
}
singleton VideoDatabase 오브젝트를 넘겨 인스턴스를 변수 videoRepository에 생성한다.
Homework
Answer these questions
Q1. 3 / Q2. 1 / Q3. 2 / Q4. 1
'안드로이드 Android > Android Study Jams' 카테고리의 다른 글
9. Repository and workManager - WorkManager (0) | 2021.01.29 |
---|---|
8. Connect to the internet - Filter and create views with internet data (0) | 2021.01.28 |
8. Connect to the internet - Load and display images from the internet (0) | 2021.01.28 |
8. Connect to the internet - Get data from the internet (0) | 2021.01.28 |
7. Databases and RecyclerView - RecyclerView Fundamentals (0) | 2021.01.21 |