見出し画像

【初心者向けチュートリアル3】JetpackComposeでTodoリストアプリを作ろう

こんにちはまっこりです。今回の記事はJetpackComposeの初心者向けチュートリアルの第三弾となっております。第一弾、第二弾を通じてJetpackComposeでのUIの作成方法、ViewModelを使ったデータの管理方法を学んできました。第三弾では、これまで学んできたUIの作成方法やViewModelを使用しつつ、リスト、ダイアログ表示の作成や、より難しいDB操作をするTodoリストアプリを作っていきます。

初心者向けチュートリアルの第一弾、第二弾をやっていない方はぜひ、チェックしてみてください。

学べる内容
・JetpackComposeでのリスト表示の作り方
・JetpackComposeでのダイアログの作り方
・Roomを用いたDBへのCRUDの実装方法
・MVVMアーキテクチャ

前提条件
・Android Studioがインストール済みであること
・エミュレータや実機での動作確認方法がわかる
・ViewModelを使ったことがある
推奨条件
・初心者向けチュートリアル第一弾 & 第二弾

ハンズオン形式で学んでいただける内容となってますので、手元で実際に作りながら本チュートリアルを読み進めていってください。

もし、途中で詰まってしまった方は、Githubに今回作るアプリのソースコードをアップしていますので、こちらを参照ください。

作るアプリについて

今回作るTodoリストアプリの完成図は以下のようになります。

データベースに保存されている、タスク情報を画面に一覧表示しています。一つ一つのリストの右端についているゴミ箱ボタンを押すことで、タスク情報を消すことができます。

右下の「+」ボタンを押すと下のようなタスクの新規作成ダイアログが開かれます。(タスクの更新時も下のダイアログを使用します。)

このダイアログでOKを押すと、テキストフィール度に入力している内容でタスクが新規作成(or更新)されます。以上が今回作っていくアプリの概要です。

それではアプリの方を作っていきましょう!

1.新規プロジェクトの作成

プロジェクトの作成と、不要なテンプレートコードの削除を行います。

1-1. テンプレート選択

Android Studioのツールバーより、File -> New -> New Projectを選択して、プロジェクトの新規作成をします。
テンプレートは「Empty Compose Activity」にして「Next」

テンプレート選択画面
アプリ名入力

アプリ名は「JetTodoApp」としましょう。その他の項目は変更せず「Finish」を押してください。選んだテンプレートをもとにプロジェクトが新規作成されます。

1-2. 余計なテンプレートコードの削除

ここでアプリを起動すると、画面の左上に「Hello Android!」と表示されます。この、「Hellow Android!」という表示は不要なので、この表示をしているテンプレートコードを削除してしまいます。

MainActivity.ktを開いて、以下の二つの関数を削除してください。

@Composable
fun Greeting(name: String) {
    Text(text = "Hello $name!")
}

@Preview(showBackground = true)
@Composable
fun DefaultPreview() {
    JetTodoAppTheme {
        Greeting("Android")
    }
}

また、MainActivityの中で今削除した、Greeting関数を呼び出している箇所も不要なので消してしまいましょう。

不要なテンプレートコードを削除した後のMainActivityは以下のようになります。

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            JetTodoAppTheme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colors.background
                ) {
                }
            }
        }
    }
}

これで不要なテンプレートコードの削除は完了です。アプリを起動すると、真っ白な画面(ダークモードはグレー)が表示されるかと思います。

2. エンティティの作成

このチュートリアルでは「データベース周りの作成」 -> 「Hiltのセットアップ」->「UI & ViewModelの作成」といった流れでアプリを作っていきます。

UIの作成をするときに、見た目を確認しながら進めていきたいので、先にデータベース周りを作ってしまおうという考えです。

前半は理解が難しく、見た目の動作確認などもできない箇所になるので、少し苦しくなるかもしれませんが、完全に理解するといったスタンスではなく、使い方に慣れるという軽いスタンスで進めていただければ良いかと思っています。

この章では、Todoリストで管理するタスクのデータ構造を表すエンティティを作成します。

2-1. Roomの依存関係を追加

データベース周りの作成を始める前に、今回データベースを扱うために使用するRoomライブラリの依存関係を追加します。

Moduleレベルのbuild.gradleファイルを開いて、pluginsブロックに以下のように一行を追加してください。

plugins {
    ...
    id 'kotlin-kapt' // new
}

また、dependenciesブロックの中に以下のように4行追加してください。

dependencies {
    // new
    def room_version = "2.4.2"
    implementation "androidx.room:room-runtime:$room_version"
    kapt "androidx.room:room-compiler:$room_version"
    implementation "androidx.room:room-ktx:$room_version"
}

これでRoomの依存関係を追加することができました。

Roomでのデータベース作成手順

Roomでデータベースを扱えるようにするには、三つのコンポーネントを作成する必要があります。それぞれのざっくりとした役割としては下のようになります。

・データベースクラス
SQLiteと直接接続する部分

・データエンティティ
DBのテーブル構造を定義する。kotlinのオブジェクトとして扱える。

・データアクセスオブジェクト(DAO)
SQLiteに対するCRUD処理を提供する

より詳しい説明はdevelopperのサイトにありますので、詳しく知りたい方は見てみてください。

読んで理解するのは難しいので、実際に作りながら理解する程度で見てみましょう。
では、データエンティティの作成から始めていきましょう。

2-1. Taskエンティティの作成

今回作成するアプリで、データベースに保存したいデータはタスクの情報です。なので、タスクを表すテーブルを作成する必要があります。

タスクひとつひとつが持つべき情報としては、以下項目があります。

・ID - Int型 - データベースで管理するための項目
・タイトル - String - タスクのタイトル
・説明 - String - タスクの説明

この三つの情報を持ったエンティティ(クラス)を作成します。
MainActivity.ktと同じパッケージに、Taskという名前のファイルを新規作成して、以下のようにコードを追加してください。

data class Task(
    val id: Int,
    var title: String,
    var description: String,
)

これで、タスク一つ一つがもつデータを表すためのクラスを作成できました。titleとdescriptionに関しては、後から変更できるようにするためにvalではなくvarで定義しています。idはユーザーが設定することはないのでvalです。

次は、このTaskエンティティをRoomにデータエンティティであるということを教えてあげます。そのためにはアノテーションを振っています。

@Entity
data class Task(
    @PrimaryKey(autoGenerate = true) val id: Int = 0,
    var title: String,
    var description: String,
)

説明

@Entityアノテーションをクラスにつけることで、Roomエンティティを宣言できます。
@PrimaryKey(autoGenerate = true)をidプロパティにつけることで、idプロパティが、プライマリーキーであること、データベース側でidを自動生成して良いことをRoomに伝えられます。idのデフォルト値に0を入れているのは、Taskインスタンスを生成するときに値を入れるのを省くためです。0が設定されていても、データベースに保存されるときには自動生成されたidが入れられます。

以上でTaskエンティティの追加は完了です。Roomでは、このTaskエンティティがデータベーステーブルの構造を定義します。

3. DAOの定義

それでは、データベースアクセスオブジェクト(DAO)を定義していきます。DAOのRoomでの役割は、先ほど定義したTaskテーブルを操作するためのメソッドを提供することです。

3-1. CRUDメソッドの作成

今回作るTodoアプリで必要なメソッドは以下の四つです。

・insertTask(task: Task)
タスクひとつ分のデータをDBに保存する
・loadAllTasks()
データベースに保存されている全てのタスクデータを取得する
・updateTask(task: Task)
DBに保存されているタスクを更新する
・deleteTask(task: Task)
DBに保存されているタスク情報を削除する

これらのメソッドを提供するDAOを作っていきましょう。
MainActivityと同じパッケージにTaskDao.ktという名前でkotlinファイルを作成し、以下のようにインターフェースを追加してください。

@Dao
interface TaskDao {
}

@Daoとアノテーションをつけることで、intafaceがDAOの定義であることを宣言できます。

このTaskDaoの中にデータベースを扱うための四つのメソッドを追加します。以下のように四つの関数を追加してください。

@Dao
interface TaskDao {
    @Insert
    fun insertTask(task: Task)

    @Query("SELECT * FROM Task")
    fun loadAllTasks(): List<Task>

    @Update
    fun updateTask(task: Task)

    @Delete
    fun deleteTask(task: Task)
}

それぞれのメソッドの説明

@Insert
fun insertTask(task: Task)

@Insertアノテーションをつけることで、引数で受け取ったTaskをテーブルにインサートするメソッドを定義できます。

@Query("SELECT * FROM Task")
fun loadAllTasks(): List<Task>

@Queryアノテーション使うと、SQL分を使ってクエリを実行できます。今回は、Taskデータベースから全てのレコードを取得してくるように記述しています。このメソッドは返り値として、Taskデータベースに保存されている全レコードデータを返します。

@Update
fun updateTask(task: Task)

@Updateアノテーションを使うと、引数で受け取ったTaskインスタンスに対応するレコードの更新を行えます。

@Delete
fun deleteTask(task: Task)

@Deleteアノテーションを使うと、引数で受け取ったTaskインスタンスに対応するレコードをDBから削除できます。

これで、Taskテーブルに対するCRUD操作を行うためのDAOを定義できました。

3-2. 非同期化

ここまでで、CRUDメソッドをDAOに追加してきました。しかし、これらのメソッドをMainActivityやViewmodelから使用するには、非同期で別スレッドでこれらのクエリが実行されるようにする必要があります。

理由としては、Room自体がメインスレッドでのデータベースアクセスを許可しないこと、そもそも時間のかかる処理をメインスレッドで実行するのはユーザーがアプリを重く感じたりANRを発生させてしまうことがあります。

DAOのメソッドの非同期化には、二つの方法があります。

・非同期ワンショットクエリ
一回だけクエリが実行され、データベースのスナップショットを返す
<- 普通の関数を別スレッドで行なっているようなものです。
・オブザーバブルクエリ
テーブルに変更があるたびに、新しいデータを取得しにいくクエリ
<- Flowを返り値とすることで、メソッドの呼び出しもとに変更を通知することができる。データベースからのデータの読み出しに使用。

DAOの非同期化についてもっと知りたい方は下のリンクを確認してみてください。

ここまでで作ってきた四つのメソッドの非同期化は以下のようにしていきます。

insertTask -> 非同期ワンショットクエリ
loadAllTasks -> オブザーバブルクエリ
updateTask -> 非同期ワンショットクエリ
deleteTask -> 非同期ワンショットクエリ

loadAllTasksは、画面にリスト表示するタスクデータを取得するのに使います。他のメソッドによって、データベースが変更されたら、その変更を反映できるように、オブザーバブルクエリを使用します。

ではDAOを非同期対応させましょう。以下のようにTaskDaoを変更してください。

@Dao
interface TaskDao {
    @Insert
    suspend fun insertTask(task: Task) // change

    @Query("SELECT * FROM Task")
    fun loadAllTasks(): Flow<List<Task>> // change

    @Update
    suspend fun updateTask(task: Task) // change

    @Delete
    suspend fun deleteTask(task: Task) // change
}

insertTask, updateTask, deleteTaskにはsuspend修飾子をつけて、非同期ワンショットクエリにしています。
loadAllTasksは返り値をFlow(coroutine)にすることで、オブザーバブルクエリにしています。

以上でDAOの定義は完了です!

4. データベースクラスの作成

Roomのセットアップの最後の手順として、データベースクラスを作成します。

データベースクラスは、SQLへのアプリのアクセスポイントであり、先ほど作成したDAOインターフェースのオブジェクトを返すことで、アプリにデータベースを操作する手段を提供することです。

MainActivity.ktと同じパッケージに、AppDatabaseという名前のkotlinファイルを追加して、以下のようにコードを記述してください。

@Database(entities = [Task::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
    abstract fun taskDao(): TaskDao
}

これでRoomデータベースの定義は完了です。

・@Databaseアノテーションをつけ、その引数にテーブル(エンティティ)とデータベースのバージョンを渡す
・RoomDatabaseを継承した抽象クラスとする
・Daoインスタンスを返す抽象関数を定義する

この三つを満たすように抽象クラスを作成すればOKです。

ここまでで、Roomでデータベースを操作するのに必要な、データベースクラス、データエンティティ、データアクセスオブジェクトといった三つのコンポーネントの作成が完了しました。

5. Hiltによる依存関係注入の設定

次は、TaskDaoのメソッドをViewModelで使用できるようにするために、Hiltを使った依存関係注入のセットアップを行なっていきあます。

もし、依存関係注入がわからない場合は、下のリンクを確認してみてください。依存関係注入自体、結構理解しずらいトピックなので文章で全体像をざっくりと理解して、実際に作りながら理解を深めていくというスタンスで見てみてください。

今回使っていくHiltというライブラリはjetpack推奨で、Android開発ではごく一般的に使われているので、学ぶメリットは大きいかと思います。

5-1. Hiltの依存関係を追加

Hiltを使用するために、プロジェクトにHiltの依存関係を追加していきましょう。プロジェクトレベルのbuild.gradleファイルを開いて以下のようにbuildscriptブロックの中に、dependenciesブロックを追加してください。

buildscript {
    ext {
        compose_version = '1.1.0-beta01'
        hilt_version = '2.43.2' // new
    }
    // new
    dependencies {
        classpath "com.google.dagger:hilt-android-gradle-plugin:$hilt_version"
    }
}// Top-level build file where you can add configuration options common to all sub-projects/modules.

また、モジュールレベルのbuild.gradleファイルを開いて、以下のようにコードを3行追加してください。

plugins {
    ...
    id 'dagger.hilt.android.plugin' // new
}

android {
    ...
}

dependencies {
    ...
    // new
    implementation "com.google.dagger:hilt-android:$hilt_version"
    kapt "com.google.dagger:hilt-compiler:$hilt_version"
}

以上でHiltが使えるようになりました。

5-2. Hiltモジュールの作成

TaskDaoをViewModelに注入できるようにするために、HiltにTaskDaoオブジェクトの生成方法を教える必要があります。Hiltに注入するインスタンスの生成方法を伝えるためにHiltモジュールというものを作成します。

MainActivityと同じパッケージ内に、Moduleという名前のkotlinファイルを追加して、以下のようにコードを追加してください。

@Module
@InstallIn(SingletonComponent::class)
class Module {
    @Singleton
    @Provides
    fun provideDatabase(
        @ApplicationContext context: Context
    ) = Room.databaseBuilder(context, AppDatabase::class.java, "task_database").build()

    @Singleton
    @Provides
    fun provideDao(db: AppDatabase) = db.taskDao()
}

説明

@Moduleというアノテーションをつけることで、Hiltモジュールを定義することができます。また、@InstallInアノテーションでは、このモジュールを提供するコンテナー(つまり、範囲)を指定することができます。今回はSingletonComponentとすることで、application全体に提供されうるように設定しました。

@Singleton
@Provides
fun provideDatabase(
    @ApplicationContext context: Context
) = Room.databaseBuilder(context, AppDatabase::class.java, "task_database").build()

この箇所では、Roomデータベースクラスインスタンスの生成方法をHiltに伝えています。TaskDaoインスタンスの生成時にAppDatabaseが必要となってくるので、AppDatabaseの提供方法も記述しています。
@Singletonとつけることで、インスタンスが毎回生成されるのではなく一度だけ生成し、そのインスタンスを使い回すように設定しています。
@Providesをつけると、HiltにDIデフィニションであることを伝えられます。

@Singleton
@Provides
fun provideDao(db: AppDatabase) = db.taskDao()

TaskDaoを提供するためのDIデフィニションです。引数には既に依存関係注入方法を設定ずみのAppDatabaseを設定しているので、Hilt側でAppDatabaseのインスタンスを取得してきて、TaskDaoを返してくれます。

以上でHiltモジュールの定義は完了です。

5-3. Hiltアプリケーション & エントリーポイント設定

Hiltによる依存関係注入を使うためには、Hiltアプリケーションの設定と、エントリーポイントの設定が必要なので、ここではそれを実施していきます。

まずはHiltアプリケーションの作成をします。MainActivityと同じパッケージにAppという名前でkotlinファイルを追加し、以下のようにコードを記述してください。

@HiltAndroidApp
class App : Application()

これで、Hiltアプリケーションを定義できましたので、このAppクラスを使うようにMainfestファイルを変更します。

AndroidManifest.xmlファイルを開いて、applicationタグに以下のようにname属性を追加してください。

<application
    android:name=".App" // new
    android:allowBackup="true"
    android:dataExtractionRules="@xml/data_extraction_rules"
    android:fullBackupContent="@xml/backup_rules"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/Theme.JetTodoApp"
    tools:targetApi="31">

以上でHiltアプリケーションの設置は完了です。

次は、エントリーポイントを設定します。Hiltによる依存関係注入を使用する際には、使用する箇所のライフサイクルを持っているクラス(ActivityやFragmentなど)に@AndroidEntryPointというアノテーションを付与する必要があります。今回の場合はMainActivityをエントリーポイントに設定します。

MainActivityを開いて、以下のように@AndroidEntryPointというアノテーションを付与してください。

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

これでMainActivityをエントリーポイントとして設定することができました。これによって、このあと追加していくViewModelに対して、HiltによるTaskDaoインスタンスの注入が行えるようになりました。

6. タスク新規作成機能の追加

ここから先は

17,274字 / 7画像
この記事のみ ¥ 250

この記事が気に入ったらチップで応援してみませんか?