在 Android 上使用資料清單

本文將說明如何在 Firebase 中處理資料清單。如要瞭解讀取及寫入 Firebase 資料的基本概念,請參閱「在 Android 上讀取及寫入資料」。

取得 DatabaseReference

如要從資料庫讀取及寫入資料,您需要 DatabaseReference 的執行個體:

KotlinJava
private lateinit var database: DatabaseReference
// ...
database = Firebase.database.reference
private DatabaseReference mDatabase;
// ...
mDatabase = FirebaseDatabase.getInstance().getReference();

讀取及寫入清單

附加至資料清單

在多使用者應用程式中,使用 push() 方法將資料附加至清單。每當新子項新增至指定的 Firebase 參照時,push() 方法就會產生專屬鍵。為清單中的每個新元素使用這些自動產生的鍵,多個用戶端就能同時在相同位置新增子項,不會發生寫入衝突。push()產生的專屬鍵會以時間戳記為依據,因此清單項目會自動依時間順序排列。

您可以使用 push() 方法傳回的新資料參照,取得子項目的自動產生鍵值,或為子項目設定資料。對 push() 參照呼叫 getKey(),會傳回自動產生的鍵值。

您可以使用這些自動產生的鍵,簡化資料結構的扁平化作業。詳情請參閱資料扇出範例

監聽子項事件

處理清單時,應用程式應監聽子項事件,而非用於單一物件的值事件。

當節點的子項發生特定作業時,系統會觸發子項事件,例如透過 push() 方法新增子項,或是透過 updateChildren() 方法更新子項。這些方法可一起用來監聽資料庫中特定節點的變更。

如要在 DatabaseReference 上監聽子事件,請附加 ChildEventListener

監聽器 事件回呼 常見用途
ChildEventListener onChildAdded() 擷取項目清單或聆聽項目清單的新增內容。 系統會為每個現有子項觸發一次這個回呼,然後每當有新子項新增至指定路徑時,再次觸發這個回呼。傳遞至接聽程式的 DataSnapshot 包含新子項的資料。
onChildChanged() 監聽清單中項目的變更。每當子項節點遭到修改 (包括子項節點的後代),就會觸發這個事件。傳遞至事件監聽器的 DataSnapshot 包含子項目的更新資料。
onChildRemoved() 監聽從清單中移除的項目。傳遞至事件回呼的 DataSnapshot 包含已移除子項的資料。
onChildMoved() 監聽有序清單中項目的順序變更。 每當更新導致子項重新排序而觸發 onChildChanged() 回呼時,就會觸發這個事件。這項功能適用於以 orderByChildorderByValue 排序的資料。

舉例來說,社群部落格應用程式可能會一併使用這些方法,監控貼文留言中的活動,如下所示:

KotlinJava
val childEventListener = object : ChildEventListener {
    override fun onChildAdded(dataSnapshot: DataSnapshot, previousChildName: String?) {
        Log.d(TAG, "onChildAdded:" + dataSnapshot.key!!)

        // A new comment has been added, add it to the displayed list
        val comment = dataSnapshot.getValue<Comment>()

        // ...
    }

    override fun onChildChanged(dataSnapshot: DataSnapshot, previousChildName: String?) {
        Log.d(TAG, "onChildChanged: ${dataSnapshot.key}")

        // A comment has changed, use the key to determine if we are displaying this
        // comment and if so displayed the changed comment.
        val newComment = dataSnapshot.getValue<Comment>()
        val commentKey = dataSnapshot.key

        // ...
    }

    override fun onChildRemoved(dataSnapshot: DataSnapshot) {
        Log.d(TAG, "onChildRemoved:" + dataSnapshot.key!!)

        // A comment has changed, use the key to determine if we are displaying this
        // comment and if so remove it.
        val commentKey = dataSnapshot.key

        // ...
    }

    override fun onChildMoved(dataSnapshot: DataSnapshot, previousChildName: String?) {
        Log.d(TAG, "onChildMoved:" + dataSnapshot.key!!)

        // A comment has changed position, use the key to determine if we are
        // displaying this comment and if so move it.
        val movedComment = dataSnapshot.getValue<Comment>()
        val commentKey = dataSnapshot.key

        // ...
    }

    override fun onCancelled(databaseError: DatabaseError) {
        Log.w(TAG, "postComments:onCancelled", databaseError.toException())
        Toast.makeText(
            context,
            "Failed to load comments.",
            Toast.LENGTH_SHORT,
        ).show()
    }
}
databaseReference.addChildEventListener(childEventListener)
ChildEventListener childEventListener = new ChildEventListener() {
    @Override
    public void onChildAdded(DataSnapshot dataSnapshot, String previousChildName) {
        Log.d(TAG, "onChildAdded:" + dataSnapshot.getKey());

        // A new comment has been added, add it to the displayed list
        Comment comment = dataSnapshot.getValue(Comment.class);

        // ...
    }

    @Override
    public void onChildChanged(DataSnapshot dataSnapshot, String previousChildName) {
        Log.d(TAG, "onChildChanged:" + dataSnapshot.getKey());

        // A comment has changed, use the key to determine if we are displaying this
        // comment and if so displayed the changed comment.
        Comment newComment = dataSnapshot.getValue(Comment.class);
        String commentKey = dataSnapshot.getKey();

        // ...
    }

    @Override
    public void onChildRemoved(DataSnapshot dataSnapshot) {
        Log.d(TAG, "onChildRemoved:" + dataSnapshot.getKey());

        // A comment has changed, use the key to determine if we are displaying this
        // comment and if so remove it.
        String commentKey = dataSnapshot.getKey();

        // ...
    }

    @Override
    public void onChildMoved(DataSnapshot dataSnapshot, String previousChildName) {
        Log.d(TAG, "onChildMoved:" + dataSnapshot.getKey());

        // A comment has changed position, use the key to determine if we are
        // displaying this comment and if so move it.
        Comment movedComment = dataSnapshot.getValue(Comment.class);
        String commentKey = dataSnapshot.getKey();

        // ...
    }

    @Override
    public void onCancelled(DatabaseError databaseError) {
        Log.w(TAG, "postComments:onCancelled", databaseError.toException());
        Toast.makeText(mContext, "Failed to load comments.",
                Toast.LENGTH_SHORT).show();
    }
};
databaseReference.addChildEventListener(childEventListener);

監聽值事件

建議使用 ChildEventListener 讀取資料清單,但在某些情況下,將 ValueEventListener 附加至清單參照會很有用。

ValueEventListener 附加至資料清單,會將整個資料清單做為單一 DataSnapshot 傳回,然後您就可以逐一存取個別子項。

即使查詢只找到一個相符項目,快照仍會是清單,只是只包含一個項目。如要存取項目,您需要對結果進行迴圈:

KotlinJava
// My top posts by number of stars
myTopPostsQuery.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(dataSnapshot: DataSnapshot) {
        for (postSnapshot in dataSnapshot.children) {
            // TODO: handle the post
        }
    }

    override fun onCancelled(databaseError: DatabaseError) {
        // Getting Post failed, log a message
        Log.w(TAG, "loadPost:onCancelled", databaseError.toException())
        // ...
    }
})
// My top posts by number of stars
myTopPostsQuery.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
        for (DataSnapshot postSnapshot: dataSnapshot.getChildren()) {
            // TODO: handle the post
        }
    }

    @Override
    public void onCancelled(@NonNull DatabaseError databaseError) {
        // Getting Post failed, log a message
        Log.w(TAG, "loadPost:onCancelled", databaseError.toException());
        // ...
    }
});

如果您想在單一作業中擷取清單的所有子項,而非監聽額外的 onChildAdded 事件,這個模式就非常實用。

卸離監聽器

如要移除回呼,請針對 Firebase 資料庫參照呼叫 removeEventListener() 方法。

如果監聽器已多次新增至資料位置,系統會針對每個事件多次呼叫監聽器,您必須多次分離監聽器,才能完全移除。

在父項監聽器上呼叫 removeEventListener() 不會自動移除在子項節點上註冊的監聽器;您也必須在任何子項監聽器上呼叫 removeEventListener(),才能移除回呼。

排序及篩選資料

您可以使用 Realtime Database Query 類別,依鍵、值或子項的值擷取排序後的資料。您也可以將排序結果篩選為特定結果數,或是特定範圍的鍵或值。

排序資料

如要擷取排序後的資料,請先指定其中一個排序依據方法,決定結果的排序方式:

方法 用量
orderByChild() 依據指定子項鍵或巢狀子項路徑的值排序結果。
orderByKey() 依子項鍵排序結果。
orderByValue() 依子項值排序結果。

一次只能使用一種排序依據方法。在同一項查詢中多次呼叫 order-by 方法會擲回錯誤。

以下範例說明如何擷取使用者熱門貼文的清單,並依星等排序:

KotlinJava
// My top posts by number of stars
val myUserId = uid
val myTopPostsQuery = databaseReference.child("user-posts").child(myUserId)
    .orderByChild("starCount")

myTopPostsQuery.addChildEventListener(object : ChildEventListener {
    // TODO: implement the ChildEventListener methods as documented above
    // ...
})
// My top posts by number of stars
String myUserId = getUid();
Query myTopPostsQuery = databaseReference.child("user-posts").child(myUserId)
        .orderByChild("starCount");
myTopPostsQuery.addChildEventListener(new ChildEventListener() {
    // TODO: implement the ChildEventListener methods as documented above
    // ...
});

這會定義查詢,與子項監聽器搭配使用時,可根據使用者 ID,同步處理用戶端與資料庫路徑中使用者貼文的資料,並依每則貼文獲得的星號數排序。這種使用 ID 做為索引鍵的技術稱為資料扇出,詳情請參閱「建構資料庫」。

orderByChild() 方法的呼叫會指定子項鍵,用來排序結果。在本例中,貼文會依據各自 "starCount" 子項目的值排序。如果資料如下所示,查詢也可以依巢狀子項排序:

"posts": {
  "ts-functions": {
    "metrics": {
      "views" : 1200000,
      "likes" : 251000,
      "shares": 1200,
    },
    "title" : "Why you should use TypeScript for writing Cloud Functions",
    "author": "Doug",
  },
  "android-arch-3": {
    "metrics": {
      "views" : 900000,
      "likes" : 117000,
      "shares": 144,
    },
    "title" : "Using Android Architecture Components with Firebase Realtime Database (Part 3)",
    "author": "Doug",
  }
},

在這個範例中,我們可以透過在 orderByChild() 呼叫中指定巢狀子項的相對路徑,依據 metrics 鍵下巢狀結構的值排序清單元素。

KotlinJava
// Most viewed posts
val myMostViewedPostsQuery = databaseReference.child("posts")
    .orderByChild("metrics/views")
myMostViewedPostsQuery.addChildEventListener(object : ChildEventListener {
    // TODO: implement the ChildEventListener methods as documented above
    // ...
})
// Most viewed posts
Query myMostViewedPostsQuery = databaseReference.child("posts")
        .orderByChild("metrics/views");
myMostViewedPostsQuery.addChildEventListener(new ChildEventListener() {
    // TODO: implement the ChildEventListener methods as documented above
    // ...
});

如要進一步瞭解其他資料類型的排序方式,請參閱「查詢資料的排序方式」。

篩選資料

如要篩選資料,建構查詢時,您可以將任何限制或範圍方法與排序方法合併。

方法 用量
limitToFirst() 設定要從排序結果清單開頭傳回的項目數量上限。
limitToLast() 設定要從排序結果清單結尾傳回的項目數量上限。
startAt() 根據所選的排序依據方法,傳回大於或等於指定鍵或值的項目。
startAfter() 根據所選的排序依據方法,傳回大於指定鍵或值的項目。
endAt() 根據所選的排序依據方法,傳回小於或等於指定鍵或值的項目。
endBefore() 根據所選的排序依據方法,傳回小於指定鍵或值的項目。
equalTo() 根據所選的排序依據方法,傳回等於指定鍵或值的項目。

與排序方法不同,您可以合併多個限制或範圍函式。 舉例來說,您可以結合 startAt()endAt() 方法,將結果限制在指定的值範圍內。

即使查詢只有一個相符項目,快照仍會是清單,只是只包含一個項目。如要存取項目,您需要疊代結果:

KotlinJava
// My top posts by number of stars
myTopPostsQuery.addValueEventListener(object : ValueEventListener {
    override fun onDataChange(dataSnapshot: DataSnapshot) {
        for (postSnapshot in dataSnapshot.children) {
            // TODO: handle the post
        }
    }

    override fun onCancelled(databaseError: DatabaseError) {
        // Getting Post failed, log a message
        Log.w(TAG, "loadPost:onCancelled", databaseError.toException())
        // ...
    }
})
// My top posts by number of stars
myTopPostsQuery.addValueEventListener(new ValueEventListener() {
    @Override
    public void onDataChange(@NonNull DataSnapshot dataSnapshot) {
        for (DataSnapshot postSnapshot: dataSnapshot.getChildren()) {
            // TODO: handle the post
        }
    }

    @Override
    public void onCancelled(@NonNull DatabaseError databaseError) {
        // Getting Post failed, log a message
        Log.w(TAG, "loadPost:onCancelled", databaseError.toException());
        // ...
    }
});

限制結果數量

您可以使用 limitToFirst()limitToLast() 方法,為指定的回呼設定要同步處理的子項數量上限。舉例來說,如果您使用 limitToFirst() 將限制設為 100,一開始只會收到最多 100 個 onChildAdded() 回呼。如果 Firebase 資料庫中儲存的項目少於 100 個,系統會為每個項目觸發 onChildAdded() 回呼。

項目變更時,你會收到進入查詢的項目 onChildAdded() 回呼,以及退出查詢的項目 onChildRemoved() 回呼,因此總數會維持在 100。

以下範例說明範例部落格應用程式如何定義查詢,以擷取所有使用者最近的 100 則貼文清單:

KotlinJava
// Last 100 posts, these are automatically the 100 most recent
// due to sorting by push() keys.
databaseReference.child("posts").limitToFirst(100)
// Last 100 posts, these are automatically the 100 most recent
// due to sorting by push() keys
Query recentPostsQuery = databaseReference.child("posts")
        .limitToFirst(100);

這個範例只定義查詢,如要實際同步處理資料,必須附加監聽器

依鍵或值篩選

您可以透過 startAt()startAfter()endAt()endBefore()equalTo(),為查詢選擇任意的開始、結束和等效點。這項功能可用於資料分頁,或尋找具有特定值的子項目的項目。

查詢資料的排序方式

本節說明 Query 類別中各個排序依據方法如何排序資料。

orderByChild

使用 orderByChild() 時,包含指定子項鍵的資料會依下列順序排序:

  1. 指定子項鍵的 null 值會優先顯示。
  2. 接著是指定子項鍵的值為 false 的子項。如果多個子項的值為 false,系統會依鍵依字母順序排序。
  3. 接著是指定子項鍵的值為 true 的子項。如果多個子項的值為 true,系統會依鍵的字典順序排序。
  4. 接著是數值,並以遞增順序排序。如果多個子項的指定子項節點具有相同的數值,系統會依鍵排序。
  5. 字串會排在數字後面,並依字典順序遞增排序。如果多個子項在指定的子項節點中具有相同的值,系統會依鍵的字典順序排序。
  6. 物件會排在最後,並依鍵以遞增順序按字母排序。

orderByKey

使用 orderByKey() 排序資料時,系統會依鍵值以遞增順序傳回資料。

  1. 如果子項的鍵可剖析為 32 位元整數,則會先顯示這些子項,並依遞增順序排序。
  2. 接著是金鑰為字串值的子項,並依字典順序遞增排序。

orderByValue

使用 orderByValue() 時,子項會依值排序。排序條件與 orderByChild() 相同,但會使用節點的值,而不是指定子項鍵的值。

後續步驟