본문 바로가기

안드로이드 Android/Android Study Jams

9. Repository and workManager - Add a Repository

github.com/google-developer-training/android-kotlin-fundamentals-starter-apps/tree/master/DevBytes-starter

 

google-developer-training/android-kotlin-fundamentals-starter-apps

android-kotlin-fundamentals-starter-apps. Contribute to google-developer-training/android-kotlin-fundamentals-starter-apps development by creating an account on GitHub.

github.com

 

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

developer.android.com/codelabs/kotlin-android-training-repository?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fkotlin-fundamentals-nine%23codelab-%2Fcodelabs%2Fkotlin-android-training-repository#3

 

Android Kotlin Fundamentals: Repository  |  Android 개발자

Learn how to add a repository to abstract the data layer and provide a clean API to the rest of the app in your Android Kotlin app.

developer.android.com

 

 

 

Add an offline cache

Key concept: 앱이 시작될 때마다 네트워크로 데이터를 가져오지 않는다. 대신 데이터베이스로부터 가져온 데이터를 띄운다. 이 기술은 앱 로딩 시간을 줄여준다.

출처: Android Study Jams

 

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

developer.android.com/codelabs/kotlin-android-training-repository?continue=https%3A%2F%2Fdeveloper.android.com%2Fcourses%2Fpathways%2Fkotlin-fundamentals-nine%23codelab-%2Fcodelabs%2Fkotlin-android-training-repository#5

 

Android Kotlin Fundamentals: Repository  |  Android 개발자

Learn how to add a repository to abstract the data layer and provide a clean API to the rest of the app in your Android Kotlin app.

developer.android.com

 

 

 

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