iT邦幫忙

2021 iThome 鐵人賽

DAY 11
0

首先來看看如何取用 Firebase SDK 的服務:

val firestore = FirebaseFirestore.getInstance()

要取用 firestore 的服務非常簡單,只要呼叫 getInstance() 就好,也不用自己再去寫一次 Singleton pattern 的雙重鎖定,寫了只是浪費時間,Firebase 已經幫我們寫好了,相關實作可以在原始碼中找到:

// FirebaseFirestore.java
@NonNull
public static FirebaseFirestore getInstance() {
  FirebaseApp app = FirebaseApp.getInstance();
  if (app == null) {
    throw new IllegalStateException("You must call FirebaseApp.initializeApp first.");
  }
  return getInstance(app, DatabaseId.DEFAULT_DATABASE_ID);
}

// FirebaseApp.java
@NonNull
public static FirebaseApp getInstance() {
  synchronized (LOCK) {
    FirebaseApp defaultApp = INSTANCES.get(DEFAULT_APP_NAME);
    if (defaultApp == null) {
      throw new IllegalStateException(
          "Default FirebaseApp is not initialized in this "
              + "process "
              + ProcessUtils.getMyProcessName()
              + ". Make sure to call "
              + "FirebaseApp.initializeApp(Context) first.");
    }
    return defaultApp;
  }
}

查詢資料

基於我們之前所新增的資料,那時候新增了一個 "Notes" 的 Collection,所以現在我們要想辦法使用 firestore api 來查詢這些資料,其查詢方式也非常簡單:

private val query = firestore.collection(COLLECTION_NOTES)
        .limit(100)

Firestore 是使用類似 builder pattern 的方式來組合出查詢,以上述的查詢為例,我們指定了要查詢 COLLECTION_NOTES 這個 Collection ,而且數量限制 100 筆,組合出查詢之後,接著就是使用該查詢來獲取資料:

query.addSnapshotListener { result, e ->
    result?.let { onSnapshotUpdated(it) }
}

addSnapshotListener 可以讓我們隨時都收到最新的資訊,只要有新的更新,這個 Listener 就會再呼叫一次。其中所有更新的資訊都在 result 裡面,如果發生錯誤,result 就會是空值,錯誤的內容將會在 e 得知,下圖的說明為 Firestore 的源碼:

Screen Shot 2021-09-04 at 4.05.21 PM.png

轉換資料

Firestore 轉換資料有兩種方式,第一個是使用反射幫你轉換成 Model,這機制跟 Gson 是一樣的,第二個是自己寫轉換資料的邏輯,可以想像成是自己使用 JsonObject 跟 JsonArray 來做反序列化。

Gson 是一個很常使用於 JSON 資料轉換的函式庫,其特點是只要定義好了 Model ,而且這 Model 的格式跟 JSON 是可以一對一互相對映的,Gson 就可以使用反射的機制幫我們產生 runtime model ,專案也可以因此大大減少樣板程式碼(Boilerplate code)。

最後我選擇了後者,自己寫轉換資料格式的邏輯,原因如下:

  1. 原有的 Note Model 中,資料無法與 Firestore 的欄位一一對應,所以如果選擇用反射的方式的話,就還要另外設計一個新的 Model 用來做資料轉換。
  2. 有了新的 Model 之後,為了要使用其中的資料,我必需還要寫資料轉換的邏輯才能轉成 Note ,這樣算下來,開發的時間反而還變長了。
  3. 效能考量,選擇方案一的話,反射本身的效能就比較差了,現在還要多出額外的記憶體空間來儲存這些中間轉換的物件,對於一個需要快速反應的 App 來說,方案一實在是很不划算。

下面程式碼是方案二的實作:

private fun onSnapshotUpdated(snapshot: QuerySnapshot) {
    val allNotes = snapshot
        .map { document -> documentToNotes(document) }

    // Use allNotes as an Observable event 
}

private fun documentToNotes(document: QueryDocumentSnapshot): Note {
    val data: Map<String, Any> = document.data
    val text = data[FIELD_TEXT] as String
    val color = YBColor(data[FIELD_COLOR] as Long)
    val positionX = data[FIELD_POSITION_X] as String? ?: "0"
    val positionY = data[FIELD_POSITION_Y] as String? ?: "0"
    val position = Position(positionX.toFloat(), positionY.toFloat())
    return Note(document.id, text, position, color)
}

附上今天專案中所用到的所有常數:

companion object {
    const val COLLECTION_NOTES = "Notes"
    const val FIELD_TEXT = "text"
    const val FIELD_COLOR = "color"
    const val FIELD_POSITION_X = "positionX"
    const val FIELD_POSITION_Y = "positionY"
}

建立、修改資料

修改資料非常簡單,只要將每個欄位都儲存到 map 結構中,再呼叫 set 即可,:

private fun setNoteDocument(note: Note) {
    val noteData = hashMapOf(
        FIELD_TEXT to note.text,
        FIELD_COLOR to note.color.color,
        FIELD_POSITION_X to note.position.x.toString(),
        FIELD_POSITION_Y to note.position.y.toString()
    )

    firestore.collection(COLLECTION_NOTES)
        .document(note.id)
        .set(noteData)
}

而且很方便的是,如果該 id 不存在,Firestore 就會自動幫我們建立一個新的 Document。

完整程式碼

由於已經完成大部分實作,其餘的部分只剩下 RxJava 的整合,這裡使用的是 BehaviorSubject 來接收以及發送資料:

class FirebaseNoteRepository: NoteRepository {
    private val firestore = FirebaseFirestore.getInstance()
    private val notesSubject = BehaviorSubject.createDefault(emptyList<Note>())

    private val query = firestore.collection(COLLECTION_NOTES)
        .limit(100)

    init {
        query.addSnapshotListener { result, e ->
            result?.let { onSnapshotUpdated(it) }
        }
    }

    override fun getAllNotes(): Observable<List<Note>> {
        return notesSubject.hide()
    }

    override fun putNote(note: Note) {
        setNoteDocument(note)
    }

    private fun onSnapshotUpdated(snapshot: QuerySnapshot) {
        val allNotes = snapshot
            .map { document -> documentToNotes(document) }

        notesSubject.onNext(allNotes)
    }

    private fun setNoteDocument(note: Note) {
        val noteData = hashMapOf(
            FIELD_TEXT to note.text,
            FIELD_COLOR to note.color.color,
            FIELD_POSITION_X to note.position.x.toString(),
            FIELD_POSITION_Y to note.position.y.toString()
        )

        firestore.collection(COLLECTION_NOTES)
            .document(note.id)
            .set(noteData)
    }

    private fun documentToNotes(document: QueryDocumentSnapshot): Note {
        val data: Map<String, Any> = document.data
        val text = data[FIELD_TEXT] as String
        val color = YBColor(data[FIELD_COLOR] as Long)
        val positionX = data[FIELD_POSITION_X] as String? ?: "0"
        val positionY = data[FIELD_POSITION_Y] as String? ?: "0"
        val position = Position(positionX.toFloat(), positionY.toFloat())
        return Note(document.id, text, position, color)
    }

    companion object {
        const val COLLECTION_NOTES = "Notes"
        const val FIELD_TEXT = "text"
        const val FIELD_COLOR = "color"
        const val FIELD_POSITION_X = "positionX"
        const val FIELD_POSITION_Y = "positionY"
    }
}

建置並執行

串完了 Firestore SDK ,我們就來實際跑看看吧!程式運行的結果如下:

https://user-images.githubusercontent.com/7949400/132089576-ace2f9b8-cc60-40df-ab81-1a9352f5e638.gif

移動的方式好奇怪!怎麼會抖來抖去的呢?到底發生了什麼事呢?我在這裡先賣個關子,大家可以猜猜看,答案明天揭曉!


上一篇
Firebase Firestore
下一篇
RxJava - Backpressure
系列文
Jetpack Compose X Android Architecture X Functional Reactive Programming30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言