处理数据列表 (Android)

本文将介绍如何在 Firebase 中处理数据列表。如需了解有关 Firebase 数据读写的基础知识,请参阅读取和写入数据 (Android)

获取 DatabaseReference

如需从数据库读取数据以及将数据写入数据库,您需要一个 DatabaseReference 实例:

Kotlin+KTX

private lateinit var database: DatabaseReference
// ...
database = Firebase.database.reference

Java

private DatabaseReference mDatabase;
// ...
mDatabase = FirebaseDatabase.getInstance().getReference();

读取和写入列表

向数据列表附加数据

使用 push() 方法可将数据附加到多用户应用的列表中。每次将新的子项添加到指定的 Firebase 引用时,push() 方法均会生成一个唯一键。利用这些为列表中的每个新元素自动生成的键,多个客户端就可以同时向同一位置添加子项,而不会引起写入冲突。push() 生成的唯一键是以时间戳为基础的,因此列表项会自动按时间顺序排列。

您可以使用 push() 方法所返回新数据的引用,获取为子项自动生成的键的值或为子项设置数据。对 push() 引用调用 getKey() 将返回自动生成的键的值。

然后,您可以使用这些自动生成的键简化数据结构的展平过程。如需了解详情,请参阅数据扇出示例

监听子项事件

在处理列表时,您的应用监听的应是子项事件,而不是用于各个对象的值事件。

当因为某项操作(例如通过 push() 方法添加新的子项,或通过 updateChildren() 方法更新子项)而使得某个节点的子项发生更改时,就会触发子项事件。合并所有这些事件,有助于监听数据库中某个特定节点的更改。

如需监听 DatabaseReference 中发生的子项事件,请附加一个 ChildEventListener

Listener 事件回调函数 典型用法
ChildEventListener onChildAdded() 检索项列表,或监听项列表中是否添加了新项。 该回调函数会针对每个现有子项触发一次,此后每当有新的子项添加至指定路径时会再次触发。传递给监听器的 DataSnapshot 包含新子项的数据。
onChildChanged() 监听列表中的项是否发生了更改。此事件会在某个子节点(包括该子节点的后代)发生修改时触发。传递给事件监听器的 DataSnapshot 包含子项的更新后数据。
onChildRemoved() 监听列表中是否有项被移除。传递给事件回调函数的 DataSnapshot 包含被移除子项的数据。
onChildMoved() 监听有序列表的项顺序是否有更改。每当引发子项重新排序的更新触发了 onChildChanged() 回调函数时,就会触发此事件。该事件用于通过 orderByChildorderByValue 进行排序的数据。

例如,社交博客应用可以结合使用这些方法来监控博文的评论动态,如下所示:

Kotlin+KTX

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)

Java

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 的形式返回整个数据列表,然后您可以遍历该数据快照来访问各个子项。

即使查询仅存在一个匹配项,该快照仍是一个列表,只是仅包含一个项。如需访问该项,您需要遍历查询结果:

Kotlin+KTX

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

Java

// 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() 按子值对结果排序。

您每次只能使用一种排序依据方法。在同一查询中多次调用一种排序依据方法会引发错误。

以下示例演示了如何检索按所得星数排序的用户热门博文列表:

Kotlin+KTX

// 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
    // ...
})

Java

// 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 键下的值对列表元素进行排序。

Kotlin+KTX

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

Java

// 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() 方法来将查询结果限制在指定的取值范围内。

即使查询仅存在一个匹配项,该快照仍是一个列表,只是仅包含一个项。如需访问该项,您需要遍历查询结果:

Kotlin+KTX

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

Java

// 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 篇最新博文的列表:

Kotlin+KTX

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

Java

// 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() 中相同,但这里使用的是节点本身的值而非指定子键的值。

后续步骤