整合 Firestore SDK 到便利贴应用程序

首先来看看如何取用 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

移动的方式好奇怪!怎麽会抖来抖去的呢?到底发生了什麽事呢?我在这里先卖个关子,大家可以猜猜看,答案明天揭晓!


<<:  Day16:[搜寻演算法]Binary search - 二分搜寻法

>>:  [Day 16] JavaScript 网页事件处理

【Day27】[演算法]-堆积排序法 Heap Sort

堆积排序法(Heap Sort)原理是利用「堆积」的资料结构为基础来完成排序。 堆积的介绍可以参考此...

[Python]Natural Language Toolkit

http://www.nltk.org/ NLTK 是一个主流用於自然语言处理的 Python 库 ...

【第二十四天 - Floyd-Warshall介绍】

Q1. Floyd-Warshall 是什麽 一种利用 Dynamic Programming ,求...

Rebol 语言和你 SAY HELLO!!

第十五天 各位点进来的朋友,你们好阿 小的不才只能做这个系列的文章,但还是希望分享给点进来的朋友,知...

【Day9】React Proptype的验证及套用方法看这里 ! ٩(●˙▿˙●)۶…⋆ฺ

如果我们想要强迫传来的Prop是某种型态或是强迫某个Prop一定要被传入的话, 我们可以使用Prop...