Android Kotlin基礎講座 09.1:レポジトリー

 Task: Add an offline cache

In this task, you add a Room database to your app to use as an offline cache.

Key concept: Don’t retrieve data from the network every time the app is launched. Instead, display data that you fetch from the database. This technique decreases app-loading time.

アプリがネットワークからデータを読み取る際に、データを即座に画面に表示するのではなく、データベースに保存します。

新しいネットワークリザルトを受け取ったときに、ローカルデータベースを更新し、新規コンテンツをローカルデータベースから画面に表示します。この手法によってオフラインキャッシュが常に最新であることが確認できます。また、端末がオフラインの場合でもキャッシュされたデータをローカルで読み込むことができます。

ステップ1:Roomの依存関係を追加する

  1. build.gradle (Module:app)を開き、プロジェクトにRoomの依存関係を追加してください。
// Room dependency
def room_version = "2.1.0-alpha06"
implementation "androidx.room:room-runtime:$room_version"
kapt "androidx.room:room-compiler:$room_version"

ステップ2:データベースオブジェクトを追加する

このステップでは、databaseオブジェクトを表すためのDatabaseVideoというデータベースの実体を作成します。またDatabaseVideoオブジェクトをdomainオブジェクトに変換するためのメソッドと、networkオブジェクトをDatabaseVideoオブジェクトに変換するためのメソッドも実装します。

  1. database/DatabaseEntities.ktを開き、DatabaseVideoという名前のRoom実体を作成してください。プライマリーキーにはurlをセットします。DevBytesサーバーの設計は動画のURLが常にユニークなものであることを保証しています。
/**
* 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)
  1. database/DatabaseEntities.kt内で、asDomainModel()という拡張関数を作成してください。この関数はDatabaseVideoオブジェクトをdomainオブジェクトに変換するために使います。
/**
* 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)
   }
}

このサンプルアプリでは、変換はシンプルなものなので、このコードのいくつかは必要ではありません。しかし実際の市場にあるアプリでは、domain、database、networkオブジェクトの構造は異なります。より複雑な変換ロジックは必要になるでしょう。

  1. network/DataTransferObjects.ktを開き、asDatabaseModel()という拡張関数を作成してください。この関数はnetoworkオブジェクトをDatabaseVideoオブジェクトに変換するために使います。
/**
* 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)
   }
}

ステップ3:VideoDaoを追加する

このステップでは、VideoDaoを実装し、データベースにアクセスするための二つのメソッドを定義します。一つはデータベースから動画を取得するためのもので、もう一つはデータベースに動画を挿入するためのメソッドです。

  1. database/Room.kt内で、VideoDaoインターフェースを定義し、@Daoでアノテーションしてください。
@Dao
interface VideoDao { 
}
  1. VideoDaoインターフェース内にデータベースから動画を読み取るためのgetVideos()というメソッドを作成してください。このメソッドの戻り値の型をLiveDataにし、データベースのデータが変化しても、UIに表示されるデータが常に更新されるようにします。
   @Query("select * from databasevideo")
   fun getVideos(): LiveData<List<DatabaseVideo>>

Android StudioでUnresolved referenceエラーが表示されたら、androidx.room.Queryをインポートしてください。

  1. VideoDaoインターフェース内で、ネットワークから読み取った動画のリストをデータベースに挿入するためのinsertAll()というメソッドを定義してください。簡素化するために、既にデータベース内に動画が登録されていたら、データベースを上書きします。そのためにはonConflict引数を使ってConflictStrategyをREPLACEに設定してください。
@Insert(onConflict = OnConflictStrategy.REPLACE)
fun insertAll( videos: List<DatabaseVideo>)

ステップ4:RoomDatabaseを実装する

このステップでは、RoomDatabaseを実装することでオフラインキャッシュをデータベースに追加します。

  1. database/Room.kt内、VideoDaoインターフェースの後にVideosDatabaseという抽象クラスを作成してください。RoomDatabaseを継承させてください。
  2. VideosDatabaseクラスをRoomデータベースとしてマークするための@Databaseアノテーションを使用してください。このデータベースに含まれるDatabaseVideo実体を宣言し、バージョン番号を1にします。
  3. VideosDatabase内でDaoメソッドにアクセスするためのVideoDao型の変数を定義してください。
@Database(entities = [DatabaseVideo::class], version = 1)
abstract class VideosDatabase: RoomDatabase() {
   abstract val videoDao: VideoDao
}
  1. private lateinitのINSTANCEという変数をクラスの外側に作成してください。これはシングルトンオブジェクトを保持するためのものです。VideosDatabaseは同時に複数のデータベースのインスタンスが作成されるのを防ぐためにシングルトンであるべきです。
  2. getDatabase()メソッドをクラスの外側に作成してください。getDatabase()内のsynchronizedブロック内でINSTANCE変数を初期化し返すようにしてください。
@Dao
interface VideoDao {
...
}
abstract class VideosDatabase: RoomDatabase() {
...
}

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
}

Tip: .isInitializedというKotlinプロパティはlateinitプロパティ(ここではINSTANCEのこと)に値が代入されている場合にはtrueを、されていなければfalseを返します。

これでRoomを使ったデータベースを実装できました。次のタスクでは、レポジトリーパターンを使ってこのデータベースを扱う方法を学習します。