Jetpack Compose アプリに Cloud Firestore を追加する
これは、ユーザー インターフェースにJetpack Composeを使用し、 Firebase Authentication、Crashlytics、Cloud Firestore、Performance Monitoring、Remote Config、Firebase Extensions、Firebase Cloud Messaging、Hiltなど、Google が提供するその他のツールを使用して、新しい Android アプリケーションをゼロから作成する方法を紹介する一連の記事の第 4 部です。
このシリーズのパート 1では、このアプリで何ができるのか、またその構造について概要を説明します。パート 2 では、Firebase Authentication を使用してログインおよびサインアップ フローを実装する方法を説明します。パート 3 では、クラッシュを処理してユーザーにメッセージを表示するために Firebase Crashlytics を追加する方法について説明します。
この第 4 部では、Cloud Firestore について学び、アプリケーションに追加して、クラウドに ToDo アイテムを保存するために使用し始める方法を確認します。また、保存されたアイテムの変更を監視し、これらのイベントに基づいて UI を更新する方法についても説明します。データをリアルタイムで同期できることは、Firestore の最も魅力的な機能の 1 つです。
タスクの作成と編集
タスク画面のフロー
コードに進む前に、タスク画面 (下の最初のスクリーンショットに表示) のフローを確認しましょう。この画面から、次のアクションを実行できます。
- 新しいタスクを作成する: 右下隅にある追加ボタンをタップすると、タスクを作成できます。ユーザーはタスク編集画面にリダイレクトされ、新しいタスクを作成できます。すべてのフィールドはデフォルト値で初期化されます。
- タスク メニューを展開します。各タスクには 3 つのドットのメニューがあり、ユーザーがタップすると 3 つのオプションが表示されます。
- タスクの編集: 上記と同じタスク編集画面が開きますが、空のタスクのデフォルト値ではなく、選択した項目の値が表示されます。
- フラグの切り替え: フラグの状態をオンからオフに、またはその逆に変更します。
- タスクの削除: タスクを削除します。
- 設定画面を開きます。右上隅の歯車アイコンをタップするとアクセスできます。設定画面では、ユーザーは既存のアカウントにログインしたり、新しいアカウントを作成したりできます。
タスク編集画面の流れ
タスク編集画面は、下の 2 番目のスクリーンショットでわかるように、非常に分かりやすいです。各タスクには、TextFields
画面上部に表示される 3 つの場所に入力できるタイトル、説明、URL があります。また、スクリーンショット 3 と 4 に示すように、日付と時刻のオプションをタップし、カレンダーと時計を使用して値を選択することで、各タスクの期限と時刻を追加することもできます。
最後に、優先度とフラグのステータスを変更することができます。各タスクはデフォルトの優先度 で初期化されNone
ますが、Low
、Medium
またはに変更することができますHigh
。フラグについては、初期値は ですOff
が、 に変更することができますOn
。ユーザーはタスクの設定が完了したら、画面の右上隅にある確認アイコンをタップできます。

クラウド ファイアストア
Cloud Firestore とは何ですか?
Cloud FirestoreはNoSQLクラウド データベースです。つまり、情報の保存方法は SQL データベースとは異なります。テーブルと列を使用する代わりに、情報はコレクションとドキュメントに保存されます。コレクションとドキュメントはJSONに非常に似た構造になっています (各ドキュメントには値にマッピングされたフィールドが含まれています)。
ドキュメント内のこれらのフィールドはさまざまなタイプ ( String
、Integer
、Array
、Date
および他のドキュメントへの参照) にすることができ、アプリケーションに最適なデータ構造を選択できます。ドキュメントはコレクションに保存されます。データを取得するには、個々のドキュメントを取得するか、クエリ条件に一致する複数のドキュメントをコレクションでクエリします。
Cloud Firestore を使用すると、リアルタイム リスナーを通じて、さまざまなプラットフォームやデバイス間でアプリケーション データを同期させることもできます。これについては、この記事でさらに詳しく説明します。これらの利点やその他の多くの利点の詳細については、公式ドキュメントをご覧ください。
プロジェクトに Cloud Firestore を追加する
Cloud Firestore を Android プロジェクトに追加するには、app/build.gradle
ファイルに次の依存関係を追加します。
dependencies { // Import the BoM for the Firebase platform implementation platform('com.google.firebase:firebase-bom:31.0.0') // Declare the dependency for the Firestore library implementation 'com.google.firebase:firebase-firestore-ktx' }
Firebase コンソールでプロジェクト用の Cloud Firestore データベースを作成することを忘れないでください。これにより、コードでこのデータベースに接続できるようになります。コンソールの Firestore データベース セクションに移動し、[Firestore データベースを作成] をクリックします。セットアップ フローの詳細については、公式のGet Started ドキュメントをご覧ください。
ストレージサービスの作成
次のステップは、このシリーズのパート 1StorageService
で説明したサービス インターフェイスと実装パターンを使用して、を作成することです。インターフェイスは次のようになります。
interface StorageService { fun addListener( userId: String, onDocumentEvent: (Boolean, Task) -> Unit, onError: (Throwable) -> Unit ) fun removeListener() fun getTask(taskId: String, onError: (Throwable) -> Unit, onSuccess: (Task) -> Unit) fun saveTask(task: Task, onResult: (Throwable?) -> Unit) fun updateTask(task: Task, onResult: (Throwable?) -> Unit) fun deleteTask(taskId: String, onResult: (Throwable?) -> Unit) }
監視したいドキュメントのコレクション内のイベントをリッスンするリスナーを登録および登録解除するメソッドがあります。その他のメソッドは、アイテムを取得、保存、更新、および削除するためのものです。上記のように、ドキュメントは と呼ばれるデータ構造で表されますTask
。これは次のdata class
ようになります。
data class Task( val id: String = "", val title: String = "", val priority: String = "", val dueDate: String = "", val dueTime: String = "", val description: String = "", val url: String = "", val flag: Boolean = false, val completed: Boolean = false, val userId: String = "" )
ユーザーに表示されるすべての情報に加えて、各アイテムにはuserId
(アプリケーションにログインしたユーザーに基づいてタスクを取得できるようにするため) と、id
ドキュメントの作成時に Firestore によって自動生成される もあります。後者は、LazyColumn
(Compose でリストを表示するために使用される UI 要素) では各アイテムに一意のキーが必要であるため必要です。
タスク画面フローの実装
リスナーの追加
ライブ データを表示できるようにしたいと考えています。そのため、ユーザーが別のデバイスでタスク リストを変更するたびに、他のすべてのデバイスに同じデータが表示されるようにする必要があります。Firestore にはスナップショット リスナーと呼ばれるメカニズムが用意されており、数行のコードでこれを実装できます。
ただし、このスナップショット リスナーが画面のライフサイクルに準拠していることを確認する必要があります。画面が表示されていない場合、イベントを受信しても意味がありません。そのため、ビューが表示されたときにリスナーを登録し、ビューが消えたときに登録を解除する必要があります。これを実現するには、最上位の composable 関数にDisposableEffectを追加します。これは、クリーンアップする必要がある副作用 (リスナーの登録と登録解除など、まさにその通りです) に使用する必要があります。
DisposableEffect(viewModel) { viewModel.addListener() onDispose { viewModel.removeListener() } }
add
上記のスニペットでは、 メソッドとremove
メソッドが互いに下にありますが、これらは順番に実行されるわけではありません。addListener
メソッドは、コンポーズ可能な関数が開始されるとすぐに呼び出され、removeListener
メソッドは、関数が破棄されるとすぐに呼び出されます。
内のこれらのメソッドaddListener
とメソッドは、ドキュメント イベントを受信するたびに実行されるコールバックを渡して を呼び出します。removeListener
TasksViewModel
StorageService
fun addListener() { viewModelScope.launch(showErrorExceptionHandler) { storageService.addListener(accountService.getUserId(), ::onDocumentEvent, ::onError) } } fun removeListener() { viewModelScope.launch(showErrorExceptionHandler) { storageService.removeListener() } }
クラスにサービス インターフェイスのメソッドを実装しますStorageServiceImpl
。
override fun addListener( userId: String, onDocumentEvent: (Boolean, Task) -> Unit, onError: (Throwable) -> Unit ) { val query = Firebase.firestore.collection(TASK_COLLECTION).whereEqualTo(USER_ID, userId) listenerRegistration = query.addSnapshotListener { value, error -> if (error != null) { onError(error) return@addSnapshotListener } value?.documentChanges?.forEach { val wasDocumentDeleted = it.type == REMOVED val task = it.document.toObject<Task>().copy(id = it.document.id) onDocumentEvent(wasDocumentDeleted, task) } } } override fun removeListener() { listenerRegistration?.remove() }
はlistenerRegistration
、 への参照を保持するサービス実装のオブジェクトですsnapshotListener
。この参照は保持して、必要に応じて登録解除できるようにします。forEach
ブロック内では、各ドキュメント イベントをチェックして、変更が 型であるかどうかを確認します。そのため、これをパラメーターとして渡してコールバックREMOVED
を呼び出すことができます。onDocumentEvent
boolean
パラメータに加えて、送信する必要のあるパラメータboolean
もあります。これは、タスク リストに必要な変更を適用するために、どのタスクが変更されたかを が正確に知る必要があるためです。APIによって提供される メソッドを使用して、ドキュメントをコードベースが認識するオブジェクト (この場合は ) にマッピングできます。マッピングについては、この記事の後半で説明します。Task
TasksViewModel
toObject
Task
ViewModel からサービス メソッドを呼び出す
TasksViewModel
ユーザーが完了としてマークしたりマークを解除したりしたとき、またはフラグのステータスが変更されたときに、Firestore のを更新するのも の役割ですTask
。ユーザーがコンテキスト メニューで項目をタップして削除したときも同じことが起こります。 は、サービスから メソッドTasksViewModel
を呼び出す役割を担っています。deleteTask
Exception
サービス メソッドを呼び出した後、次の 2 つのことが発生する可能性があります。エラーが発生した場合は、コールバックでを受け取り、onError
以下のコード スニペットに示すように、メソッドを呼び出します。このメソッドは、このシリーズのパート 3onError
で説明したように、Crashlytics を使用してエラーをログに記録します。成功すると、Firestore コレクションでドキュメントが更新され、登録されたリスナーを通じてイベントが受信され、ローカル リストが更新されます。
private fun onDeleteTaskClick(task: Task) { viewModelScope.launch(showErrorExceptionHandler) { storageService.deleteTask(task.id) { error -> if (error != null) onError(error) } } }
構成可能な画面の更新
ToDo リスト画面では、 のリストだけを確認すればよいため、と のUiState
間で情報を交換するためにを作成する必要はありません。他の画面で見てきたように を使用する代わりに、 を使用します。TasksViewModel
TasksScreen
Tasks
MutableState
MutableStateMap
var tasks = mutableStateMapOf<String, Task>() private set
タスクの ID をマップのキーとして使用し、タスクをそのキーの値として保存します。これにより、一意の ID に基づいてアイテムに非常に迅速にアクセスして変更を加えることができます。たとえば、TasksViewModel
ドキュメント イベントをリッスンするときに が行うことはこれだけです。
private fun onDocumentEvent(wasDocumentDeleted: Boolean, task: Task) { if (wasDocumentDeleted) tasks.remove(task.id) else tasks[task.id] = task }
次に、このマップを監視するコンポーザブル関数が再構成され、更新された項目が表示されます。
LazyColumn { items(viewModel.tasks.values.toList(), key = { it.id }) { item -> [...] } }
タスク編集画面フローの実装
ViewModel からサービス メソッドを呼び出す
StorageService
では、呼び出すことができるメソッドが3 つありますEditTaskViewModel
。最初のメソッドは と呼ばれgetTask
、 をパラメータとして受け取りますtaskId
。この ID を使用して、この画面で編集する必要がある を取得できますTask
。パラメータとして ID が送信されない場合は、空のフィールドを含む画面が表示され、ユーザーはTask
最初から新しい を作成できます。
使用する 2 番目と 3 番目のメソッドは、saveTask
(新しいタスクを作成するために使用) とupdateTask
(既存のタスクを更新するために使用) と呼ばれます。
fun onDoneClick(popUpScreen: () -> Unit) { viewModelScope.launch(showErrorExceptionHandler) { val editedTask = task.value.copy(userId = accountService.getUserId()) if (editedTask.id.isBlank()) saveTask(editedTask, popUpScreen) else updateTask(editedTask, popUpScreen) } } private fun saveTask(task: Task, popUpScreen: () -> Unit) { storageService.saveTask(task) { error -> if (error == null) popUpScreen() else onError(error) } } private fun updateTask(task: Task, popUpScreen: () -> Unit) { storageService.updateTask(task) { error -> if (error == null) popUpScreen() else onError(error) } }
onDoneClick
ユーザーが を保存するためにボタンをタップすると、 メソッドが呼び出されます。Task
このメソッド内では、 がTask
すでに存在するかどうか (ID が空白かどうかをチェックすることによって) を確認し、 メソッドを呼び出して を保存または更新するかどうかを決定しますTask
。繰り返しますが、これらのサービス メソッドのいずれかが呼び出されると、2 つのことが発生する可能性があります。エラーの場合は、Exception
コールバックで を受け取り、 メソッドを呼び出しますonError
。成功した場合は、 を閉じてEditTaskScreen
ユーザーを にリダイレクトしTasksScreen
、更新されたリストを表示できるようにします。
はコレクション内の変更をリッスンするように登録されているので、を にTasksViewModel
保存するとすぐに、この更新を含むイベントが Firestore から送信され、 は自身のタスク リストを自動的に更新することに注意してください。Task
EditTaskViewModel
TasksViewModel
タスクの保存
上で見たように、ユーザーが保存ボタンをクリックするとすぐに、コンポーザブル関数 onDoneClick
が から メソッドを呼び出し、このメソッド内でからまたはをEditTaskViewModel
呼び出すことができます。メソッドは をコレクションに追加するだけです(その後、Firestore はこのドキュメントの ID を自動的に生成します)。メソッドは ID を使用してFirestore 内でこのドキュメントを見つけ、新しい値で更新します。saveTask
updateTask
StorageService
saveTask
Task
updateTask
Task
override fun saveTask(task: Task, onResult: (Throwable?) -> Unit) { Firebase.firestore .collection(TASK_COLLECTION) .add(task) .addOnCompleteListener { onResult(it.exception) } }
override fun updateTask(task: Task, onResult: (Throwable?) -> Unit) { Firebase.firestore .collection(TASK_COLLECTION) .document(task.id) .set(task) .addOnCompleteListener { onResult(it.exception) } }
上記の両方のメソッドは、onResult
パラメータをコールバックとして Firestore API に渡します。このコールバックは、API メソッドが終了するとすぐに実行されます。このコールバックで必要な情報は、タスクの保存プロセス中にエラーが発生したかどうかだけです。これにより、画面にエラーをユーザーに表示できます (タスクを削除するときに以前に見た方法と同様)。
Task
保存する前にオブジェクトを変換していないことにお気づきかもしれません。これは、Firestore がdata class
リフレクションを使用してオブジェクトをドキュメントに自動的に変換できるためです。つまり、Firestore を使用するときにオブジェクトをシリアル化または逆シリアル化する必要がないということです。これにより、大量の定型コードを書かなくて済むだけでなく、コードの保守性も向上します。
ドキュメントを取得する場合も同じことが起こります。Firestore ドキュメントをアプリケーションが認識できるオブジェクトにマッピングするのは非常に簡単で、toObject
API が提供するメソッドを使用するだけで、マッピングが自動的に行われます。たとえば、Firestore から取得したドキュメントを 型のオブジェクトに変換するにはTask
、次の操作を行うだけです。
val task = document.toObject<Task>()
Task
メソッドに型を渡すとtoObject
、リフレクションを使用して、ドキュメントのフィールド値がで見たプロパティに変換されますdata class Task
。
構成可能な画面の更新
もう一度言いますが、タスク編集画面では、 を作成してと のUiState
間で情報を交換する必要はありません。確認する必要があるのは 1 つの(ユーザーが作成または編集しているタスク) だけだからです。EditTaskViewModel
EditTaskScreen
Task
var task = mutableStateOf(Task()) private set
は、ユーザーがフィールドで編集するとプロパティをEditTaskViewModel
更新し、変更ごとにコンポーズ可能な関数が再構成され、ユーザーが入力した新しいプロパティを表示します。ユーザー入力を に渡す方法と、新しい値を にポストバックする方法について詳しくは、このシリーズのパート 2をご覧ください。Task
ViewModels
Screens
今後の改善
ユーザーが当社のアプリを気に入ってくれることを願う一方で、納得できずアカウントを削除したいというユーザーもいます。そのため、また現地の規制に準拠するために、ユーザーがすべてのデータを削除できる方法を提供する必要があります。
StorageService
ユーザーのドキュメントを 1 つずつ調べてすべてのデータを削除するメソッドを追加することを検討してください。メソッドは次のようになります。
// DO NOT USE THIS - ANTIPATTERN! override fun deleteAllForUser(userId: String, onResult: (Throwable?) -> Unit) { Firebase.firestore .collection(TASK_COLLECTION) .whereEqualTo(USER_ID, userId) .get() .addOnFailureListener { error -> onResult(error) } .addOnSuccessListener { result -> for (document in result) document.reference.delete() onResult(null) } }
この方法は、データのセットが小さい場合には問題なく機能しますが、いくつかの問題が発生する可能性があります。たとえば、タスクの削除中にインターネット接続が失敗すると、一部のドキュメントが削除されずにコレクションに残ります。
理想的には、アカウントを削除するアクションをバックエンドに任せ、ユーザーがアカウントを削除したらすべてのドキュメントを一括削除する単一のメソッドを呼び出すことができます。このために独自のCloud Functionsを作成することも、時間を節約してDelete User Data Extensionを使用することもできます。これについては、このシリーズの次回の記事で取り上げる予定です。
次は何か
Android 用の Make it So のソース コードは、この Github リポジトリで入手できます。この記事で紹介したすべてのコードは、v1
フォルダーで入手できます。
Services
この記事では、コールバックを使用してとの間で結果を渡す方法を説明しますViewModels
が、このパターンは Make it So アプリの特定のバージョンでのみ使用されます。したがって、v1.0.0 tag
このコードに具体的にアクセスしたい場合は をチェックアウトする必要があります。より新しいバージョンでは、コールバックの代わりにKotlin CoroutinesとKotlin FlowTask
を使用してモデルを変更します。
iOS/Mac 開発にも興味がある場合は、同じアプリケーションがこれらのプラットフォームでも利用できます。iOS/Mac バージョンでは、ユーザー インターフェイスの構築に SwiftUI を使用します。これは、Compose で UI を構築する方法と同様に、宣言型 UI モデルに従います。ソース コードは、この Github ページにあります。
ご質問やご提案がございましたら、リポジトリのディスカッション ページからお気軽にお問い合わせください。