アーキテクチャ コンポーネントの目的は、ライフサイクル管理やデータ永続化などの一般的タスク用のライブラリを提供するとともに、アプリ アーキテクチャに関する指針を示すことです。アーキテクチャ コンポーネントを使用すると、ボイラープレート コードを減らし、堅牢でテストとメンテナンスがしやすいアプリを構築できます。アーキテクチャ コンポーネント ライブラリは Android Jetpack に含まれています。
なお、この Codelab は Kotlin 版です。Java プログラミング言語版については、こちらをご覧ください。
この Codelab で問題(コードのバグ、文法的な誤り、不明確な表現など)が見つかった場合は、Codelab の左下隅にある [誤りを報告] から問題を報告してください。
前提条件
Kotlin、オブジェクト指向設計のコンセプト、Android アプリ開発の基礎、特に以下に精通している必要があります。
RecyclerViewとアダプター- SQLite データベースと SQLite クエリ言語
- コルーチンの基本的な使い方(コルーチンに慣れていない場合は、Android アプリで Kotlin コルーチンを使用するをご覧ください)
また、MVP や MVC など、ユーザー インターフェースからデータを分離するソフトウェア アーキテクチャ パターンに精通していれば、より理解しやすいでしょう。この Codelab は、アプリ アーキテクチャ ガイドに定義されているアーキテクチャを実装しています。
取り扱う範囲は、Android アーキテクチャ コンポーネントです。それ以外のコンセプトやコードについては、コピーして貼り付ければ済む形で提供します。
Kotlin に慣れていない場合は、この Codelab の Java プログラミング言語版(こちら)をご覧ください。
演習内容
この Codelab では、アーキテクチャ コンポーネントである Room、ViewModel、LiveData を使用したアプリの設計・構築方法を学習し、次のようなアプリを作成します。
- Android アーキテクチャ コンポーネントを使用した推奨アーキテクチャを実装している。
- データベースと連携してデータの取得・保存を行う。また、そのデータベースに単語を事前入力する。
MainActivityのRecyclerViewにすべての単語を表示する。- ユーザーが [+] ボタンをタップすると 2 つ目のアクティビティが開く。ユーザーが単語を入力すると、その単語がデータベースとリストに追加されます。
このアプリは、見た目はシンプルですが、開発用のテンプレートとして使用できるほど複雑な内容を持っています。プレビューを以下に示します。
|
|
|
必要なもの
- Android Studio 3.0 以降とその使用方法に関する知識。Android Studio に加え、SDK と Gradle も必ず最新版にしてください。
- Android デバイスまたはエミュレータ。
この Codelab には、アプリを完成させるために必要なコードがすべて用意されています。
アーキテクチャ コンポーネントを使用して推奨アーキテクチャを実装するには、多くのステップを踏むことになります。重要なのは、各ステップがどのように関連し合っているか、データがどのように流れているかを把握し、今行っていることを頭の中で明確にイメージすることです。この Codelab の作業では、単にコピーと貼り付けを行うだけでなく、そのようなメンタルモデルを構築するよう努めてください。
推奨アーキテクチャ コンポーネントとは
用語を紹介するために、アーキテクチャ コンポーネントと、それらがどのように連携して機能するかを簡単に紹介します。なお、この Codelab で取り上げるのは、アーキテクチャ コンポーネントの一部(具体的には LiveData、ViewModel、Room)です。各コンポーネントについて詳しくは、使用する際に説明します。
下図に、アーキテクチャの基本形を示します。

エンティティ: Room を使用する際にデータベース テーブルを記述するアノテーション付きクラス。
SQLite データベース: デバイス上のストレージ。Room 永続ライブラリが作成して管理します。
DAO: データ アクセス オブジェクト。関数に対する SQL クエリのマッピング。DAO を使用する際は、メソッドを呼び出せばあとは Room が処理します。
Room データベース: 基盤にある SQLite データベースへのアクセス ポイントとして機能(SQLiteOpenHelper) を隠蔽)し、データベースの処理をシンプルにします。Room データベースは、DAO を使用して SQLite データベースにクエリを発行します。
リポジトリ: デベロッパーが作成するクラスで、主に複数のデータソースを管理するために使用します。
ViewModel: リポジトリ(データ)と UI 間のコミュニケーション センターとして機能します。これにより、UI はデータの生成元を意識する必要がなくなります。ViewModel インスタンスは、アクティビティやフラグメントを再作成しても存続します。
LiveData: 監視可能なデータホルダー クラス。常に最新バージョンのデータを保持またはキャッシュに保存し、データが変更されたときにオブザーバーに通知します。LiveData はライフサイクルを認識します。UI コンポーネントは関連データを監視するだけで、監視の停止や再開は行いませんが、LiveData はそのすべてを自動的に管理します。これは、LiveData が監視を行いながら、関連するライフサイクルの状態の変化を認識するためです。
RoomWordSample のアーキテクチャ概要
下図に、このアプリのすべての要素を示します。角丸の四角(SQLite データベースは含まない)はすべて、これから作成するクラスを表しています。

- Android Studio を開き、[Start a new Android Studio project] をクリックします。
- [Create New Project] ウィンドウで [Empty Activity] を選択し、[Next] をクリックします。
- 次の画面で、アプリに「RoomWordSample」という名前を付けて、[Finish] をクリックします。

次に、コンポーネント ライブラリを Gradle ファイルに追加する必要があります。
- Android Studio で [Project] タブをクリックし、Gradle Scripts フォルダを展開します。
build.gradle(Module: app)を開きます。
build.gradle(Module: app)ファイルの先頭に定義されている他のプラグインの下に次の行を追加して、kaptアノテーション プロセッサ Kotlin プラグインを適用します。
apply plugin: 'kotlin-kapt'androidブロック内にpackagingOptionsブロックを追加して、パッケージからアトミック関数モジュールを除外し、警告を回避します。
android {
// other configuration (buildTypes, defaultConfig, etc.)
packagingOptions {
exclude 'META-INF/atomicfu.kotlin_module'
}
}dependenciesブロックの末尾に次のコードを追加します。
// Room components
implementation "androidx.room:room-runtime:$rootProject.roomVersion"
kapt "androidx.room:room-compiler:$rootProject.roomVersion"
androidTestImplementation "androidx.room:room-testing:$rootProject.roomVersion"
// Lifecycle components
implementation "androidx.lifecycle:lifecycle-extensions:$rootProject.archLifecycleVersion"
kapt "androidx.lifecycle:lifecycle-compiler:$rootProject.archLifecycleVersion"
implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:$rootProject.archLifecycleVersion"
// Kotlin components
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
api "org.jetbrains.kotlinx:kotlinx-coroutines-core:$rootProject.coroutines"
api "org.jetbrains.kotlinx:kotlinx-coroutines-android:$rootProject.coroutines"
// Material design
implementation "com.google.android.material:material:$rootProject.materialVersion"
// Testing
testImplementation 'junit:junit:4.12'
androidTestImplementation "androidx.arch.core:core-testing:$rootProject.coreTestingVersion"build.gradle(Project: RoomWordsSample)ファイルを開き、最後尾にバージョン番号を次のコードのように追加します。
ext {
roomVersion = '2.2.5'
archLifecycleVersion = '2.2.0'
coreTestingVersion = '2.1.0'
materialVersion = '1.1.0'
coroutines = '1.3.4'
}このアプリのデータは単語であり、その値を保持するために下図のようなシンプルなテーブルが必要になります。

Room のテーブルは、エンティティを使用して作成できます。それでは作成しましょう。
Wordデータクラスを含むWordという名前の新しい Kotlin クラスファイルを作成します。
このクラスにより、単語用のエンティティ(SQLite テーブルを表す)が記述されます。クラス内の各プロパティは、テーブル内の列を表します。Room は最終的にこのプロパティを使用して、テーブルの作成と、データベース行からのオブジェクトのインスタンス化を行います。
コードは次のとおりです。
data class Word(val word: String)Room データベースで Word クラスを使用できるようにするには、アノテーションを付ける必要があります。アノテーションは、クラスの各部分がデータベース内のエントリにどう関連しているかを示します。Room はこの情報を使用してコードを生成します。
アノテーションを貼り付けずに手動で入力すると、Android Studio によってアノテーション クラスが自動的にインポートされます。
- 次のコードのように、
Wordクラスにアノテーションを追加します。
@Entity(tableName = "word_table")
class Word(@PrimaryKey @ColumnInfo(name = "word") val word: String)それぞれのアノテーションの機能は以下のとおりです。
@Entity(tableName ="word_table")
: 各@Entityクラスは SQLite テーブルを表します。エンティティであることを示すために、クラス宣言にアノテーションを付けます。テーブル名を指定することで、クラス名と異なる名前にすることができます。ここでは、テーブル名を「word_table」としています。@PrimaryKey
: すべてのエンティティには主キーが必要です。ここでは、シンプルに各単語自体を主キーにしています。@ColumnInfo(name ="word")
: テーブル内の列名をメンバー変数と異なる名前にする場合に指定します。ここでは、列に「word」という名前を付けています。- データベースに格納されるすべてのプロパティは公開設定を一般公開(Kotlin のデフォルト)にする必要があります。
すべてのアノテーションの一覧については、Room パッケージ概要リファレンスをご覧ください。
DAO とは
DAO(データ アクセス オブジェクト)とは、SQL クエリを指定してメソッド呼び出しに関連付けるためのものです。一般的なクエリを利便性の良いアノテーション(@Insert など)に関連付けておくと、コンパイラにより SQL のチェックとクエリの生成が行われます。Room はこの DAO を使用して、コード用のクリーンな API を作成します。
DAO はインターフェースまたは抽象クラスである必要があります。
デフォルトでは、すべてのクエリは個別のスレッドで実行する必要があります。
Room はコルーチンをサポートしているため、クエリに suspend 修飾子付きのアノテーションを指定して、コルーチンや他の suspend 関数から呼び出すことができます。
DAO を実装する
次のクエリを行う DAO を作成しましょう。
- すべての単語をアルファベット順に並べる
- 単語を挿入する
- すべての単語を削除する
WordDaoという名前の新しい Kotlin クラスファイルを作成します。- 次のコードをコピーして
WordDaoに貼り付け、必要に応じてコンパイル用にインポートを調整します。
@Dao
interface WordDao {
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): List<Word>
@Insert(onConflict = OnConflictStrategy.IGNORE)
suspend fun insert(word: Word)
@Query("DELETE FROM word_table")
suspend fun deleteAll()
}
詳しく見ていきましょう。
WordDaoはインターフェースです。DAO は、インターフェースまたは抽象クラスである必要があります。@Daoアノテーションは、Room の DAO クラスであることを示します。suspend fun insert(word: Word): 1 つの単語を挿入する suspend 関数を宣言しています。@Insertアノテーションは特殊な DAO メソッド アノテーションであり、ここには SQL を指定する必要はありません。なお、行の削除・更新用に@Deleteと@Updateというアノテーションもありますが、このアプリでは使用しません。onConflict = OnConflictStrategy.IGNORE: ここで選択した onConflict 戦略により、すでにリストにある単語と完全に一致するものを挿入しようとした場合は無視されます。利用可能な競合戦略について詳しくは、ドキュメントをご覧ください。suspend fun deleteAll(): すべての単語を削除する suspend 関数を宣言しています。- 複数のエンティティを削除するための利便性の良いアノテーションがないため、汎用の
@Queryアノテーションを使用しています。 @Query("DELETE FROM word_table"):@Queryでは、アノテーションの文字列パラメータに SQL クエリを指定する必要があります。これにより、複雑な読み取りクエリなどの操作が可能になります。fun getAlphabetizedWords(): List<Word>: すべての単語を取得して、WordsのListを返すメソッド。@Query("SELECT * from word_table ORDER BY word ASC"): 単語のリストを昇順に並べ替えて返すクエリ。
通常、データが変更された際には、更新されたデータを UI に表示するなど、なんらかのアクションが必要になります。そのため、データの変更に対応できるように監視する必要があります。
データの保存方法によっては、この操作が難しい場合があります。アプリの複数のコンポーネント間でデータの変更を監視すると、コンポーネント間に明示的で厳密な依存関係のパスが作成される可能性があります。これにより、テストやデバッグが難しくなります。
LiveData は、データ オブザベーション用のライフサイクル ライブラリ クラスで、この問題を解決します。メソッドの記述で LiveData 型の戻り値を使用すると、データベースが更新された際の LiveData の更新に必要なすべてのコードが Room により生成されます。
WordDao で、返される List<Word> が LiveData でラップされるように getAlphabetizedWords() メソッドのシグネチャを変更します。
@Query("SELECT * from word_table ORDER BY word ASC")
fun getAlphabetizedWords(): LiveData<List<Word>>この Codelab の後半では、MainActivity の Observer を介してデータの変更を追跡します。
Room データベースとは?
- Room は、SQLite データベースを基盤とするデータベース レイヤです。
SQLiteOpenHelperで処理していたような一般的なタスクを担います。- DAO を使用してデータベースにクエリを発行します。
- UI のパフォーマンス低下を避けるため、デフォルトではメインスレッドからクエリを発行できません。Room のクエリが
LiveDataを返す場合、クエリは自動的にバックグラウンド スレッドで非同期に実行されます。 - Room は、SQLite ステートメントのコンパイル時チェックを行います。
Room データベースを実装する
Room データベースは、RoomDatabase を拡張した抽象クラスにする必要があります。通常、アプリ全体で必要な Room データベースのインスタンスは 1 つのみです。
それでは作成してみましょう。
WordRoomDatabaseという名前の Kotlin クラスファイルを作成し、次のコードを追加します。
// Annotates class to be a Room Database with a table (entity) of the Word class
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
public abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
companion object {
// Singleton prevents multiple instances of database opening at the
// same time.
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(context: Context): WordRoomDatabase {
val tempInstance = INSTANCE
if (tempInstance != null) {
return tempInstance
}
synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
).build()
INSTANCE = instance
return instance
}
}
}
}コードを詳しく見ていきましょう。
- Room データベースのクラスは
abstractで、RoomDatabaseを拡張する必要があります。 - クラスに
@Databaseアノテーションを付けて Room データベースであることを示し、アノテーション パラメータを使用してデータベースに属するエンティティの宣言とバージョンの指定を行います。各エンティティは、データベース内に作成されるテーブルに対応します。この Codelab では、データベースの移行は範囲外のため、exportSchemaを false に設定してビルド警告を回避します。実際のアプリでは、現在のスキーマをバージョン管理システムにチェックインできるように、Room によるスキーマのエクスポートに使用されるディレクトリを設定することを検討してください。 - データベースは、@Dao ごとに抽象「ゲッター」メソッドを介して DAO を公開します。
- データベースのインスタンスが複数同時に開かれるのを防ぐために、シングルトンの
WordRoomDatabase,を定義しています。 getDatabaseはこのシングルトンを返します。そして、最初にアクセスされた時点でデータベースを作成します。その際には、Room のデータベース ビルダーを使用してWordRoomDatabaseクラスからアプリのコンテキスト内にRoomDatabaseオブジェクトを作成し、"word_database"という名前を付けます。
リポジトリとは
リポジトリ クラスは、複数のデータソースへのアクセスを抽象化します。リポジトリは、アーキテクチャ コンポーネント ライブラリには含まれていませんが、コードの分離とアーキテクチャの観点から推奨されるベスト プラクティスです。リポジトリ クラスは、アプリの他の部分に対するデータアクセスのためのクリーンな API を提供します。

リポジトリを使用する理由
リポジトリはクエリを管理し、複数のバックエンドの使用を可能にします。よくあるのは、データをネットワークから取得するか、ローカル データベース内のキャッシュに保存された結果を使用するかの決定ロジックを、リポジトリで実装する例です。
リポジトリの実装
WordRepository という名前の Kotlin クラスファイルを作成し、次のコードを貼り付けます。
// Declares the DAO as a private property in the constructor. Pass in the DAO
// instead of the whole database, because you only need access to the DAO
class WordRepository(private val wordDao: WordDao) {
// Room executes all queries on a separate thread.
// Observed LiveData will notify the observer when the data has changed.
val allWords: LiveData<List<Word>> = wordDao.getAlphabetizedWords()
suspend fun insert(word: Word) {
wordDao.insert(word)
}
}要点は以下のとおりです。
- リポジトリ コンストラクタには、データベース全体ではなく DAO が渡されます。DAO には、データベースに対するすべての読み取り・書き込みメソッドが含まれているため、これにアクセスするだけでよいのです。データベース全体をリポジトリに公開する必要はありません。
- 単語のリストは公開プロパティです。Room から単語の
LiveDataリストを取得することで初期化されます。これができるのは、「LiveData クラス」ステップでLiveDataを返すようにgetAlphabetizedWordsメソッドを定義したためです。Room は、すべてのクエリを個別のスレッドで実行します。データが変更されると、監視対象のLiveDataはメインスレッドでオブザーバーに通知します。 suspend修飾子により、この関数はコルーチンまたは他の suspend 関数から呼び出される必要があることをコンパイラに伝えます。
ViewModel とは
ViewModel の役割は、UI にデータを提供し、構成変更に耐えられるようにすることです。ViewModel は、リポジトリと UI 間のコミュニケーション センターとして機能します。また、ViewModel を使用すれば、フラグメント間でデータを共有することもできます。ViewModel はライフサイクル ライブラリに含まれています。

このトピックの入門ガイドとしては、ViewModel の概要(ViewModel Overview)または ViewModel: 簡単な例のブログ投稿をご覧ください。
ViewModel を使用する理由
ViewModel は、構成変更に耐えられるようライフサイクルを意識した方法で、アプリの UI データを保持します。アプリの UI データを Activity クラスや Fragment クラスから分離することで、単一責任の原則への遵守性を高めることができます。つまり、アクティビティやフラグメントは画面へのデータの描画を担い、ViewModel は UI に必要なすべてのデータの保持と処理を担います。
ViewModel では、UI が使用または表示するデータで変更される可能性があるものに LiveData を使用します。LiveData を使用すると、次のようなメリットがあります。
- データをポーリングして変更を検出するのではなく、データにオブザーバーを設定し、データが実際に変更された場合にのみ UI を更新できます。
- リポジトリと UI は
ViewModelによって完全に分離されています。 ViewModelからのデータベース呼び出しはありません(これはすべて Repository で処理されるため、コードのテストが容易になります)。
viewModelScope
Kotlin では、すべてのコルーチンが CoroutineScope 内で実行されます。スコープは、そのジョブを通じてコルーチンの存続期間を制御します。スコープのジョブをキャンセルすると、そのスコープで開始されたコルーチンがすべてキャンセルされます。
AndroidX lifecycle-viewmodel-ktx ライブラリにより、ViewModel クラスの拡張関数として viewModelScope が追加され、スコープを操作できるようになります。
ViewModel でのコルーチンの操作について詳しくは、Android アプリで Kotlin コルーチンを使用する Codelab のステップ 5、または Android の簡単なコルーチン: viewModelScope のブログ投稿をご覧ください。
ViewModel を実装する
WordViewModel 用の Kotlin クラスファイルを作成し、次のコードを追加します。
class WordViewModel(application: Application) : AndroidViewModel(application) {
private val repository: WordRepository
// Using LiveData and caching what getAlphabetizedWords returns has several benefits:
// - We can put an observer on the data (instead of polling for changes) and only update the
// the UI when the data actually changes.
// - Repository is completely separated from the UI through the ViewModel.
val allWords: LiveData<List<Word>>
init {
val wordsDao = WordRoomDatabase.getDatabase(application).wordDao()
repository = WordRepository(wordsDao)
allWords = repository.allWords
}
/**
* Launching a new coroutine to insert the data in a non-blocking way
*/
fun insert(word: Word) = viewModelScope.launch(Dispatchers.IO) {
repository.insert(word)
}
}ここでは、以下を行っています。
AndroidViewModelを拡張して、Applicationをパラメータとして取得するWordViewModelというクラスを作成しています。- リポジトリへの参照を保持するためのプライベート メンバー変数を追加しました。
- 単語のリストをキャッシュに保存するためのパブリック
LiveDataメンバー変数を追加しています。 WordRoomDatabaseからWordDaoへの参照を取得するinitブロックを作成しました。initブロックで、WordRoomDatabaseに基づいてWordRepositoryを構築しました。initブロックで、リポジトリを使用してallWordsLiveData を初期化しました。- リポジトリの
insert()メソッドを呼び出すラッパーinsert()メソッドを作成しています。これにより、insert()の実装が UI からカプセル化されます。insert がメインスレッドをブロックしないように、新しいコルーチンを開始し、suspend 関数であるリポジトリの insert を呼び出します。前述のとおり、ViewModel にはライフサイクルに基づくコルーチン スコープviewModelScopeがあり、ここではそれを使用しています。
次に、リストとアイテム用の XML レイアウトを追加する必要があります。
この Codelab は、XML でのレイアウトの作成に精通していることを前提としているため、コードを提供するのみとします。
AppTheme の親を Theme.MaterialComponents.Light.DarkActionBar に設定して、アプリテーマ マテリアルを作成してください。values/styles.xml にリストアイテムのスタイルを追加します。
<resources>
<!-- Base application theme. -->
<style name="AppTheme" parent="Theme.MaterialComponents.Light.DarkActionBar">
<!-- Customize your theme here. -->
<item name="colorPrimary">@color/colorPrimary</item>
<item name="colorPrimaryDark">@color/colorPrimaryDark</item>
<item name="colorAccent">@color/colorAccent</item>
</style>
<!-- The default font for RecyclerView items is too small.
The margin is a simple delimiter between the words. -->
<style name="word_title">
<item name="android:layout_width">match_parent</item>
<item name="android:layout_marginBottom">8dp</item>
<item name="android:paddingLeft">8dp</item>
<item name="android:background">@android:color/holo_orange_light</item>
<item name="android:textAppearance">@android:style/TextAppearance.Large</item>
</style>
</resources>layout/recyclerview_item.xml レイアウトを追加します。
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/textView"
style="@style/word_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@android:color/holo_orange_light" />
</LinearLayout>layout/activity_main.xml で、TextView を RecyclerView に置き換え、フローティング操作ボタン(FAB)を追加します。レイアウトは次のようになるはずです。
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recyclerview"
android:layout_width="0dp"
android:layout_height="0dp"
tools:listitem="@layout/recyclerview_item"
android:padding="@dimen/big_padding"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_word"/>
</androidx.constraintlayout.widget.ConstraintLayout>FAB の外観は実際の動作に対応しているほうがよいため、アイコンを「+」記号に置き換えます。
まず、次のようにして新しいベクター アセットを追加する必要があります。
- [File] > [New] > [Vector Asset] を選択します。
- [Clip Art ] で Android ロボットのアイコンをクリックします。

- 「add」を検索して「+」アセットを選択します。[OK
] をクリックします。 - [次へ] をクリックします。

- アイコンのパスが
main > drawableであることを確認し、[Finish] をクリックしてこのアセットを追加します。
- 引き続き
layout/activity_main.xmlで、新しいドローアブルが含まれるよう FAB を更新します。
<com.google.android.material.floatingactionbutton.FloatingActionButton
android:id="@+id/fab"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_margin="16dp"
android:contentDescription="@string/add_word"
android:src="@drawable/ic_add_black_24dp"/>これからデータを RecyclerView に表示するようにします。そのほうが、単に TextView にスローするよりも少し都合がよいためです。この Codelab は、RecyclerView、RecyclerView.LayoutManager、RecyclerView.ViewHolder、RecyclerView.Adapter の仕組みを理解していることを前提としています。
アダプタの words 変数にデータがキャッシュ保存されていることに注意してください。次のタスクでは、データを自動的に更新するコードを追加します。
RecyclerView.Adapter を拡張する WordListAdapter 用の Kotlin クラスファイルを作成します。コードは次のとおりです。
class WordListAdapter internal constructor(
context: Context
) : RecyclerView.Adapter<WordListAdapter.WordViewHolder>() {
private val inflater: LayoutInflater = LayoutInflater.from(context)
private var words = emptyList<Word>() // Cached copy of words
inner class WordViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) {
val wordItemView: TextView = itemView.findViewById(R.id.textView)
}
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): WordViewHolder {
val itemView = inflater.inflate(R.layout.recyclerview_item, parent, false)
return WordViewHolder(itemView)
}
override fun onBindViewHolder(holder: WordViewHolder, position: Int) {
val current = words[position]
holder.wordItemView.text = current.word
}
internal fun setWords(words: List<Word>) {
this.words = words
notifyDataSetChanged()
}
override fun getItemCount() = words.size
}RecyclerView を MainActivity の onCreate() メソッドに追加します。
onCreate() メソッドの setContentView より下は次のようになります。
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)アプリを実行して、すべての動作に問題がないことを確認します。まだデータに接続していないため、アイテムはありません。

データベースにはデータが存在しません。データを登録するには、2 通りの方法があります。データベースを開いたときにデータをいくつか登録する方法と、単語を登録する Activity を追加する方法です。
アプリが起動されるたびにデータベースのコンテンツをすべて削除して再入力するには、RoomDatabase.Callback を作成して onOpen() をオーバーライドします。UI スレッドからは Room データベース操作を行えないため、onOpen() は IO ディスパッチャ上でコルーチンを起動します。
コルーチンを起動するには、CoroutineScope が必要です。WordRoomDatabase クラスの getDatabase メソッドを更新し、CoroutineScope もパラメータとして取得します。
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
...
}WordViewModel の init ブロックで、データベース取得イニシャライザを更新して、スコープも渡すようにします。
val wordsDao = WordRoomDatabase.getDatabase(application, viewModelScope).wordDao()WordRoomDatabase で、RoomDatabase.Callback() のカスタム実装を作成します。これも、CoroutineScope をコンストラクタ パラメータとして取得します。次に、データベースにデータを入力するため onOpen メソッドをオーバーライドします。
WordRoomDatabase クラスの中でコールバックを作成するコードを以下に示します。
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
populateDatabase(database.wordDao())
}
}
}
suspend fun populateDatabase(wordDao: WordDao) {
// Delete all content here.
wordDao.deleteAll()
// Add sample words.
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
// TODO: Add your own words!
}
}最後に、データベース作成シーケンスの Room.databaseBuilder() で .build() を呼び出す直前に、次のコールバックを追加します。
.addCallback(WordDatabaseCallback(scope))最終的なコードは次のようになります。
@Database(entities = arrayOf(Word::class), version = 1, exportSchema = false)
abstract class WordRoomDatabase : RoomDatabase() {
abstract fun wordDao(): WordDao
private class WordDatabaseCallback(
private val scope: CoroutineScope
) : RoomDatabase.Callback() {
override fun onOpen(db: SupportSQLiteDatabase) {
super.onOpen(db)
INSTANCE?.let { database ->
scope.launch {
var wordDao = database.wordDao()
// Delete all content here.
wordDao.deleteAll()
// Add sample words.
var word = Word("Hello")
wordDao.insert(word)
word = Word("World!")
wordDao.insert(word)
// TODO: Add your own words!
word = Word("TODO!")
wordDao.insert(word)
}
}
}
}
companion object {
@Volatile
private var INSTANCE: WordRoomDatabase? = null
fun getDatabase(
context: Context,
scope: CoroutineScope
): WordRoomDatabase {
// if the INSTANCE is not null, then return it,
// if it is, then create the database
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
WordRoomDatabase::class.java,
"word_database"
)
.addCallback(WordDatabaseCallback(scope))
.build()
INSTANCE = instance
// return instance
instance
}
}
}
}
次の文字列リソースを values/strings.xml に追加します。
<string name="hint_word">Word...</string>
<string name="button_save">Save</string>
<string name="empty_not_saved">Word not saved because it is empty.</string>次のカラーリソースを value/colors.xml に追加します。
<color name="buttonLabel">#FFFFFF</color>次のようにして、サイズリソース ファイルを新たに作成します。
- [Project] ウィンドウでアプリ モジュールをクリックします。
- [File] > [New] > [Android Resource File] を選択します。
- [Available qualifiers] から [Dimension ] を選択します。
- ファイル名を dimens に設定します。

次のサイズリソースを values/dimens.xml に追加します。
<dimen name="small_padding">8dp</dimen>
<dimen name="big_padding">16dp</dimen>次の手順で、空のアクティビティ テンプレートを使って空の Android Activity を新たに作成します。
- [File] > [New] > [Activity] > [Empty Activity] を選択します。
- アクティビティ名として「
NewWordActivity」と入力します。 - Android マニフェストに新しいアクティビティが追加されたことを確認します。
<activity android:name=".NewWordActivity"></activity>レイアウト フォルダにある activity_new_word.xml ファイルを次のコードのように更新します。
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<EditText
android:id="@+id/edit_word"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:minHeight="@dimen/min_height"
android:fontFamily="sans-serif-light"
android:hint="@string/hint_word"
android:inputType="textAutoComplete"
android:layout_margin="@dimen/big_padding"
android:textSize="18sp" />
<Button
android:id="@+id/button_save"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/colorPrimary"
android:text="@string/button_save"
android:layout_margin="@dimen/big_padding"
android:textColor="@color/buttonLabel" />
</LinearLayout>アクティビティのコードを次のように更新します。
class NewWordActivity : AppCompatActivity() {
private lateinit var editWordView: EditText
public override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_new_word)
editWordView = findViewById(R.id.edit_word)
val button = findViewById<Button>(R.id.button_save)
button.setOnClickListener {
val replyIntent = Intent()
if (TextUtils.isEmpty(editWordView.text)) {
setResult(Activity.RESULT_CANCELED, replyIntent)
} else {
val word = editWordView.text.toString()
replyIntent.putExtra(EXTRA_REPLY, word)
setResult(Activity.RESULT_OK, replyIntent)
}
finish()
}
}
companion object {
const val EXTRA_REPLY = "com.example.android.wordlistsql.REPLY"
}
}
最後のステップでは、ユーザーが入力した新しい単語を保存し、単語データベースの現在の内容を RecyclerView に表示することにより、UI をデータベースに接続します。
データベースの現在の内容を表示するには、ViewModel に LiveData を監視するオブザーバーを追加します。
データが変更されるたびに、onChanged() コールバックが起動されます。このコールバックは、アダプターの setWords() メソッドを呼び出して、アダプターのキャッシュに保存されたデータを更新するとともに表示中のリストを更新します。
MainActivity で、ViewModel のメンバー変数を作成します。
private lateinit var wordViewModel: WordViewModelViewModelProvider を使用して、ViewModel を Activity に関連付けます。
Activity が最初に起動すると、ViewModelProviders が ViewModel を作成します。アクティビティが破棄されても(構成の変更など)、ViewModel は保持されます。アクティビティが再作成されると、ViewModelProviders は既存の ViewModel を返します。詳細については、ViewModel をご覧ください。
RecyclerView コードブロックの下の onCreate() で、ViewModelProvider から ViewModel を取得します。
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)また、onCreate() に、WordViewModel の allWords LiveData プロパティのオブザーバーを追加します。
onChanged() メソッド(ラムダのデフォルト メソッド)は、監視対象データが変更され、かつアクティビティがフォアグラウンドにある場合に呼び出されます。
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})FAB をタップすると NewWordActivity が開き、MainActivity に戻ったらデータベースに新しい単語が挿入されるか、Toast が表示されるようにします。これを実現するには、まずリクエスト コードを次のように定義します。
private val newWordActivityRequestCode = 1MainActivity に、NewWordActivity の onActivityResult() コードを追加します。
アクティビティが RESULT_OK で返された場合は、WordViewModel の insert() メソッドを呼び出して、返された単語をデータベースに挿入します。
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}MainActivity, で、ユーザーが FAB をタップしたら NewWordActivity を開始します。MainActivity onCreate で FAB を見つけ、次のコードで onClickListener を追加します。
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}完成したコードは次のようになります。
class MainActivity : AppCompatActivity() {
private const val newWordActivityRequestCode = 1
private lateinit var wordViewModel: WordViewModel
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setSupportActionBar(toolbar)
val recyclerView = findViewById<RecyclerView>(R.id.recyclerview)
val adapter = WordListAdapter(this)
recyclerView.adapter = adapter
recyclerView.layoutManager = LinearLayoutManager(this)
wordViewModel = ViewModelProvider(this).get(WordViewModel::class.java)
wordViewModel.allWords.observe(this, Observer { words ->
// Update the cached copy of the words in the adapter.
words?.let { adapter.setWords(it) }
})
val fab = findViewById<FloatingActionButton>(R.id.fab)
fab.setOnClickListener {
val intent = Intent(this@MainActivity, NewWordActivity::class.java)
startActivityForResult(intent, newWordActivityRequestCode)
}
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == newWordActivityRequestCode && resultCode == Activity.RESULT_OK) {
data?.getStringExtra(NewWordActivity.EXTRA_REPLY)?.let {
val word = Word(it)
wordViewModel.insert(word)
}
} else {
Toast.makeText(
applicationContext,
R.string.empty_not_saved,
Toast.LENGTH_LONG).show()
}
}
}それではアプリを実行しましょう。NewWordActivity でデータベースに単語を追加すると、UI が自動的に更新されます。
正常に動作するアプリが出来上がったので、これを振り返ってみましょう。アプリの構造を再度以下に示します。

アプリのコンポーネントは次のとおりです。
MainActivity:RecyclerViewとWordListAdapterを使用して単語をリストに表示します。MainActivityには、データベースからの単語の LiveData を監視し、変更があった場合には通知を受け取るObserverが存在します。NewWordActivity:リストに新しい単語を追加します。WordViewModel: データレイヤにアクセスするためのメソッドを提供します。また、MainActivity がオブザーバーの関連付けをセットアップできるように LiveData を返します。*LiveData<List<Word>>: UI コンポーネントの自動更新を可能にします。MainActivityには、データベースからの単語の LiveData を監視し、変更があった場合には通知を受け取るObserverが存在します。Repository:1 つ以上のデータソースを管理します。Repositoryは、基盤にあるデータ プロバイダと対話するためのメソッドを ViewModel に公開します。このアプリでは、バックエンドのデータ プロバイダは Room データベースです。Room: SQLite データベースを実装し、そのラッパーとなります。これまでデベロッパーが行っていたさまざまな処理を代行します。- DAO: メソッド呼び出しをデータベース クエリにマッピングします。たとえば、リポジトリが
getAlphabetizedWords()を呼び出したら Room がSELECT * from word_table ORDER BY word ASCを実行するようにできます。 Word: 1 つの単語を含むエンティティ クラス。
* Views と Activities(および Fragments)は、ViewModel を介してのみデータを操作します。そのため、データの取得元は関係ありません。
UI 自動更新のデータフロー(リアクティブ UI)
LiveData を使用しているため、自動更新が可能です。MainActivity には、データベースからの単語の LiveData を監視し、変更があった場合には通知を受け取る Observer が存在します。変更があると、オブザーバーの onChange() メソッドが実行され、WordListAdapter の mWords が更新されます。
データは LiveData のため監視可能です。監視対象となるのは、WordViewModel allWords プロパティから返される LiveData<List<Word>> です。
WordViewModel は、UI レイヤに対してバックエンドに関するすべての情報を隠蔽します。そして、データレイヤにアクセスするためのメソッドを提供し、MainActivity がオブザーバーの関連付けをセットアップできるように LiveData を返します。Views と Activities(および Fragments)は、ViewModel を介してのみデータを操作します。そのため、データの取得元は関係ありません。
今回の場合、データは Repository から取得されます。ViewModel は、そのリポジトリが何とやり取りするかを知る必要はありません。知る必要があるのは Repository とどうやり取りするかです。ここでは Repository によって公開されたメソッドを使用します。
リポジトリは 1 つ以上のデータソースを管理します。WordListSample アプリの場合、データソースのバックエンドは Room データベースです。Room は SQLite データベースを実装し、そのラッパーとなります。これまでデベロッパーが行っていたさまざまな処理を代行します。たとえば、Room は、SQLiteOpenHelper クラスで行っていたことをすべて行えます。
DAO は、メソッド呼び出しをデータベース クエリにマッピングします。たとえば、リポジトリが getAllWords() を呼び出したら Room が SELECT * from word_table ORDER BY word ASC. を実行するようにできます。
クエリから返される結果は監視対象の LiveData であるため、Room でデータが変更されるたびに Observer インターフェースの onChanged() メソッドが実行され、UI が更新されます。
(省略可)解答コードをダウンロードする
この Codelab の解答コードをまだ確認していない場合は確認します。GitHub リポジトリを参照するか、以下からコードをダウンロードしてください。
ダウンロードした zip ファイルを解凍すると、アプリ全体を含んだルートフォルダ android-room-with-a-view-kotlin が展開されます。


