0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

KotlinCoroutineでFirestoreにアクセスする

Posted at

この記事の内容

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

依存関係を追加します。

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」で取得できます。

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コレクションのリファレンス取得

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ドキュメントを追加する)

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ドキュメントの一覧を取得する)

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コレクションの変更を監視する)

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

データを追加する

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

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()
    }
}

データを取得する

初期データとして、一回だけデータの取得を呼び出しています。

SpotGetCase.kt
class SpotGetCase @Inject constructor(
    private val spotRepository: SpotRepository,
) {
    suspend fun getInitialData(): SpotFuture<List<Spot>> {
        return spotRepository.getAll().first()
    }
}

変更を監視する

変更の監視Flowを返す関数と、その結果をViewModelが保持している一覧とマージする関数の2つを実装しました。

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

まぁ...呼んでいるだけですね。

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の要素として表示しています。

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 ?: "")
            }
        }
    }
}

いざ実行!

全部のソースはこちらです。
FirebaseAuthの実装とかいろいろ入ってしまっていてすみません...
firestore_add_AdobeExpress.gif

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?