KotlinCoroutineでFirestoreにアクセスする

# この記事の内容
kotlin coroutineを使用して、Firestoreのデータ追加とデータ読み取り、コールバックでのデータ読み取りをします。

### 前提
FirebaseAuthでのログイン・ログアウトが実装されている前提です。

### Firestoreの構成
Firestoreの構成は以下の階層構造になっています。
```
users - collection
　┣　{auth.user1.uid} - document
　┃　　┗　spot_list - collection
　┃　　　　┣　spotデータ1 - document
　┃　　　　┗　spotデータ2 - document
　┗　{auth.user2.uid} - document
　　　　　┗　spot_list - collection
　　　　　　　┗　spotデータ3 - document
```
Firestoreは、先頭にCollectionというDocumentを複数格納するコンテナがあります。
Documentは、SubCollectionもしくは単純な文字列とか数値とかを格納できます。

記事にするFirestoreの構成は、先頭を「users」というCollectionにしました。このusersという名前は、Firestoreを設計する人が自由に決めて良い名前です。
usersには、FirebaseAuthのuser.uidをドキュメントIDにもつDocument（以降ユーザドキュメントと呼びます）が複数入ります。
ユーザドキュメントには、「spot_list」というSubCollectionを格納します。
spot_listには、spotデータを格納するドキュメントを複数格納する、こんな構成にします。

## 実装

### dependencies
依存関係を追加します。
```diff_gradle:app/build.gradle.kts
    // Firebase
    implementation(platform("com.google.firebase:firebase-bom:32.2.0"))
    implementation("com.google.firebase:firebase-analytics-ktx")
    implementation("com.google.firebase:firebase-auth-ktx")
+   implementation("com.google.firebase:firebase-firestore-ktx")
```

### DI
Firestoreのインスタンスを取得するProviderを追加します。
Firestoreのインスタンスは、「Firebase.firestore」で取得できます。
```kotlin:FirebaseModule.kt
@Module
@InstallIn(SingletonComponent::class)
object FirebaseModule {
    @Provides
    fun provideAuth(): FirebaseAuth = Firebase.auth.apply { setLanguageCode("ja") }

+   @Provides
+   fun provideFirestore(): FirebaseFirestore = Firebase.firestore
```

### インフラ層
Spotデータの追加、Spotデータ一覧の取得、Spotデータの変更コールバック
の3つを実装します。

Firestoreでは、CollectionやDocumentのReference（参照）に対して、スナップショットを要求するという感じで実装していきます。

#### spot_listコレクションのリファレンス取得
```kotlin:SpotRepositoryImpl.kt
    private val spotListRef: CollectionReference?
        get() = auth.currentUser?.let { user ->
            firestore
                .collection(COLLECTION_NAME_USERS)
                .document(user.uid)
                .collection(COLLECTION_NAME_SPOT_LIST)
        }
```
Firebase.authでログイン中のユーザがいれば、そのユーザのspot_listコレクションに対するReferenceを返却する
という実装です。
このCollectionReferenceに対して、spotドキュメントを追加する、spotドキュメントの一覧を取得する、spot_listコレクションの変更を監視するという実装を行っていきます。

#### Spotデータの追加（=spotドキュメントを追加する）
```kotlin:SpotRepositoryImpl.kt
    override fun add(spot: Spot): Flow<SpotFuture<Spot>> {
        val spotListRef = this.spotListRef ?: return flowOf(SpotFuture.Error(Throwable("user unauthorized")))

        return flow<SpotFuture<Spot>> {
            val docRef = spotListRef
                .add(FirestoreSpot.fromSpot(spot))
                .addOnFailureListener {
                    throw it
                }
                .await()

            Timber.d("<SpotRepository>add ${docRef.id}")
            emit(SpotFuture.Success(spot.copy(id = docRef.id)))
        }.onStart {
            emit(SpotFuture.Proceeding)
        }.catch { cause ->
            emit(SpotFuture.Error(cause))
        }.flowOn(coroutineContext)
    }
```
FirestoreSpotがSpotデータです。
spotListRefに対して、add(FirestoreSpot)して、データの追加を行っています。
addで追加することで、新しいドキュメントIDが自動発番されます。


#### Spotデータ一覧の取得（=spotドキュメントの一覧を取得する）
```kotlin:SpotRepositoryImpl.kt
    override fun getAll(): Flow<SpotFuture<List<Spot>>> {
        val spotListRef = this.spotListRef ?: return flowOf(SpotFuture.Error(Throwable("user unauthorized")))

        return spotListRef
            .snapshots()    // ①
            .map { querySnapshot: QuerySnapshot ->
                // ②
                val firestoreSpotList = querySnapshot.documents.map { documentSnapshot ->
                    documentSnapshot.toObject<FirestoreSpot>()
                }
                // ③
                @Suppress("USELESS_CAST")
                SpotFuture.Success(
                    firestoreSpotList.mapNotNull {
                        it?.toDomain()
                    }
                ) as SpotFuture<List<Spot>>
            }
            .catch { cause ->
                emit(SpotFuture.Error(cause))
            }
            .flowOn(coroutineContext)
    }
```
①spotListRefのsnapshotを取得します。
②snapshotからドキュメント一覧を取得します。
③結果を返却します。

#### Spotデータの変更コールバック（=spot_listコレクションの変更を監視する）
```kotlin:SpotRepositoryImpl.kt
    override val spotListFlow: Flow<List<SpotChange>>
        get() = callbackFlow {
            val listener: EventListener<QuerySnapshot> = EventListener<QuerySnapshot> { snapshotQuery, error ->
                if (error != null) {
                    Timber.e("FirestoreSnapshotListener(Spot) ${error.localizedMessage}")
                } else {
                    try {
                        snapshotQuery?.documentChanges?.mapNotNull { documentChange ->
                            val firestoreSpot = documentChange.document.toObject<FirestoreSpot>()
                            val spot = firestoreSpot.toDomain() ?: return@mapNotNull null
                            val type = when (documentChange.type) {
                                DocumentChange.Type.ADDED -> SpotChange.Type.ADDED
                                DocumentChange.Type.MODIFIED -> SpotChange.Type.MODIFIED
                                DocumentChange.Type.REMOVED -> SpotChange.Type.REMOVED
                            }
                            SpotChange(type, spot)
                        }?.let { trySend(it) }
                    } catch (error: Exception) {
                        Timber.e("FirestoreSnapshotListener(Spot) ${error.localizedMessage}")
                    }
                }
            }

            val spotListRef = this@SpotRepositoryImpl.spotListRef ?: return@callbackFlow
            val registrationListener = spotListRef.addSnapshotListener(listener)
            awaitClose { registrationListener.remove() }
        }
```
一番下の行と下から2行目の行で、監視リスナーの登録と削除を行っています。
監視リスナーには、変更時点のスナップショットが通知されます。
スナップショットから、変更のあったドキュメント（snapshotQuery?.documentChanges）を取得して、変更をコールバックFlowで通知しています。

### Usecase

#### データを追加する
データ追加を一回だけ呼び出すようにしています。

```kotlin:SpotAddCase.kt
class SpotAddCase @Inject constructor(
    private val spotRepository: SpotRepository,
) {
    suspend fun add(): SpotFuture<Spot> {
        return spotRepository.add(
            Spot.newSpot(
                name = "name",
                location = LatLng(35.6809591, 139.7673068),
                address = "address",
                tel = "tel",
                url = "url",
                imageUrl = "imageUrl",
                memo = listOf("memo"),
            )
        ).first()
    }
}
```

#### データを取得する
初期データとして、一回だけデータの取得を呼び出しています。
```kotlin:SpotGetCase.kt
class SpotGetCase @Inject constructor(
    private val spotRepository: SpotRepository,
) {
    suspend fun getInitialData(): SpotFuture<List<Spot>> {
        return spotRepository.getAll().first()
    }
}
```

#### 変更を監視する
変更の監視Flowを返す関数と、その結果をViewModelが保持している一覧とマージする関数の2つを実装しました。
```kotlin:SpotWatchCase.kt
class SpotWatchCase @Inject constructor(
    private val spotRepository: SpotRepository,
) {
    fun addListener(): Flow<List<SpotChange>> {
        return spotRepository.spotListFlow
    }

    fun merge(currentList: List<Spot>, changeList: List<SpotChange>): List<Spot> {
        val list = currentList.toMutableList()

        val modList = changeList.filter { it.type == SpotChange.Type.ADDED || it.type == SpotChange.Type.MODIFIED }
        modList.forEach { spotChange ->
            val index = list.indexOfFirst { it.id == spotChange.spot.id }
            if (index < 0) {
                list.add(spotChange.spot)
            } else {
                list[index] = spotChange.spot
            }
        }

        val removeList = changeList.filter { it.type == SpotChange.Type.REMOVED }
        removeList.forEach { spotChange ->
            val index = list.indexOfFirst { it.id == spotChange.spot.id }
            if (index >= 0) {
                list.removeAt(index)
            }
        }

        return list
    }
}
```

### ViewModel
まぁ...呼んでいるだけですね。
```kotlin:SpotListViewModel.kt
@HiltViewModel
class SpotListViewModel @Inject constructor(
    private val spotGetCase: SpotGetCase,
    private val spotWatchCase: SpotWatchCase,
    private val spotAddCase: SpotAddCase,
) : ViewModel() {
    private val _spotListFlow: MutableStateFlow<List<Spot>> = MutableStateFlow(emptyList())
    val spotListFlow = _spotListFlow.asStateFlow()

    fun refreshSpotList() {
        viewModelScope.launch {
            val spotListFuture = spotGetCase.getInitialData()
            if (spotListFuture is SpotFuture.Success) {
                _spotListFlow.value = spotListFuture.value
            } else {
                // TODO エラー処理
            }

            spotWatchCase.addListener().collect { spotChanges ->
                _spotListFlow.value = spotWatchCase.merge(_spotListFlow.value, spotChanges)
            }
        }
    }

    fun add() {
        viewModelScope.launch {
            spotAddCase.add()
        }
    }
}
```

### Screen
addをタップすると、Spotデータが追加され、その結果をコールバックで受け取り、LazyColumnの要素として表示しています。
```kotlin:SpotListScreen.kt
@Composable
fun SpotListScreen(
    viewModel: SpotListViewModel = hiltViewModel(),
) {
    val spotListState = viewModel.spotListFlow.collectAsState()

    LaunchedEffect(Unit) {
        viewModel.refreshSpotList()
    }

    Column {
        Button(onClick = { viewModel.add() }) {
            Text("add")
        }

        LazyColumn {
            items(spotListState.value) {
                Text(it.id ?: "")
            }
        }
    }
}
```

## いざ実行！
[全部のソースはこちらです。](https://github.com/jozuko/PlanFun/tree/a0930f39082c7149fca9d0e7a2156d5e6540e475)
FirebaseAuthの実装とかいろいろ入ってしまっていてすみません...
![firestore_add_AdobeExpress.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3491965/5d214464-2ebe-1ddb-a44e-2566e0c3d53c.gif)