עבודה עם רשימות נתונים ב-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() כדי לצרף נתונים לרשימה באפליקציות עם כמה משתמשים. ה-method push() יוצרת מפתח ייחודי בכל פעם שמתווסף צאצא חדש לקובץ העזר הספציפי של Firebase. השימוש במפתחות האלה שנוצרים באופן אוטומטי לכל רכיב חדש ברשימה מאפשר לכמה לקוחות להוסיף צאצאים לאותו מיקום בו-זמנית, בלי התנגשויות בכתיבה. המפתח הייחודי שנוצר על ידי push() מבוסס על חותמת זמן, כך שהפריטים ברשימה מסודרים אוטומטית בסדר כרונולוגי.

אפשר להשתמש בהפניה לנתונים החדשים שמוחזרים על ידי השיטה push() כדי לקבל את הערך של המפתח שנוצר באופן אוטומטי של הצאצא, או כדי להגדיר נתונים עבור הצאצא. קריאה ל-getKey() על הפניה ל-push() מחזירה את הערך של המפתח שנוצר באופן אוטומטי.

אפשר להשתמש במפתחות האלה שנוצרים באופן אוטומטי כדי לפשט את שטיח הנתונים. למידע נוסף, ראו דוגמה למאווררת נתונים.

האזנה לאירועים של ילדים

כשעובדים עם רשימות, האפליקציה צריכה להאזין לאירועים צאצאים במקום לאירועים משמעותיים שמשמשים לאובייקטים בודדים.

אירועי הצאצאים מופעלים בתגובה לפעולות ספציפיות שקורות לצאצאים של צומת מפעולה, כמו צאצא חדש שנוסף באמצעות השיטה push() או צאצא שמתעדכן באמצעות השיטה updateChildren(). כל אחד מהם יכול להיות שימושי כדי להאזין לשינויים בצומת ספציפי במסד נתונים.

כדי להאזין לאירועי צאצא ב-DatabaseReference, צריך לצרף ChildEventListener:

Listener קריאה חוזרת (callback) של אירוע שימוש רגיל
ChildEventListener onChildAdded() מאחזרים רשימות של פריטים או מאזינים לתוספות לרשימת פריטים. קריאת החזרה (callback) הזו מופעלת פעם אחת לכל צאצא קיים, ואז שוב בכל פעם שנוסף צאצא חדש לנתיב שצוין. השדה DataSnapshot שמוענק למאזין מכיל את הנתונים של הצאצא החדש.
onChildChanged() האזנה לשינויים בפריטים ברשימה. האירוע הזה מופעל בכל פעם שמשנים צומת צאצא, כולל שינויים בצאצאים של צומת הצאצא. השדה DataSnapshot שמוענק למאזין האירועים מכיל את הנתונים המעודכנים של הצאצא.
onChildRemoved() האזנה לפריטים שמוסרים מרשימת פריטים. הערך של DataSnapshot שמוענק ל-event callback מכיל את הנתונים של הצאצא שהוסרה.
onChildMoved() האזנה לשינויים בסדר הפריטים ברשימה מסודרת. האירוע הזה מופעל בכל פעם שהקריאה החוזרת (callback) של onChildChanged() מופעלת על ידי עדכון שגורם לסדר מחדש של הצאצא. הוא משמש עם נתונים שממוינים באמצעות orderByChild או orderByValue.

לדוגמה, אפליקציית בלוגים חברתית עשויה להשתמש בשיטות האלה יחד כדי לעקוב אחרי הפעילות בתגובות לפוסטים, כפי שמוצג בהמשך:

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 אחד, ואז אפשר להעביר אותה בלולאה (loop) כדי לגשת לילדים ספציפיים.

גם אם יש רק התאמה אחת לשאילתה, קובץ snapshot עדיין יהיה רשימה, רק שהוא יכיל פריט אחד. כדי לגשת לפריט, צריך להריץ לולאה על התוצאה:

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 נוספים.

ניתוק של רכיבי מעקב

כדי להסיר קריאות חוזרות, קוראים ל-method‏ removeEventListener() במזהה של מסד הנתונים ב-Firebase.

אם הוספת מאזין כמה פעמים למיקום נתונים, הוא ייכלל כמה פעמים בכל אירוע, ותצטרכו לנתק אותו כמה פעמים כדי להסיר אותו לגמרי.

קריאה ל-removeEventListener() ב-listener כהורה לא מסירה באופן אוטומטי מאזינים שרשומים בצומתי הצאצא שלו. צריך גם להפעיל את 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
    // ...
});

כך מגדירים שאילתה, שכשמשלבים אותה עם מאזין צאצא מסנכרנת את הלקוח עם הפוסטים של המשתמש מהנתיב במסד הנתונים, על סמך מזהה המשתמש שלו, לפי סדר מספר הכוכבים שקיבל כל פוסט. השיטה הזו של שימוש במזהים כמפתחות של אינדקס נקראת 'הרחבת נתונים'. תוכלו לקרוא מידע נוסף על כך בקטע מבנה מסד הנתונים.

בקריאה ל-method‏ 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",
  }
},

בדוגמה זו, ניתן לסדר את רכיבי הרשימה לפי הערכים המקוננים במפתח metrics, על ידי ציון הנתיב היחסי אל הצאצא שהוצב בקריאה ל-orderByChild().

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

מידע נוסף על סדר של סוגי נתונים אחרים זמין במאמר איך מתבצע הסדר של נתוני השאילתות.

סינון נתונים

כדי לסנן נתונים, אפשר לשלב כל אחת מהשיטות של limit או range עם שיטת order-by כשיוצרים שאילתה.

שיטה שימוש
limitToFirst() מגדיר את המספר המקסימלי של פריטים להחזרה מתחילת רשימת התוצאות הממוזערת.
limitToLast() המדיניות הזו מגדירה את המספר המקסימלי של פריטים שיוחזרו בסוף רשימת התוצאות הממוינת.
startAt() החזרת פריטים שגדולים או שווים למפתח או לערך שצוינו, בהתאם לשיטת הסדר שנבחרה.
startAfter() הפונקציה מחזירה פריטים שגדולים מהמפתח או מהערך שצוינו, בהתאם לשיטת הסדר שנבחרה.
endAt() החזרת פריטים בסכום שקטן מהמפתח או מהערך שצוינו, בהתאם לשיטה 'סידור לפי' שנבחרה.
endBefore() הפונקציה מחזירה פריטים שערכם קטן מהמפתח או מהערך שצוינו, בהתאם לשיטת הסדר שנבחרה.
equalTo() הפונקציה מחזירה פריטים שווים למפתח או לערך שצוינו, בהתאם לשיטת הסדר שנבחרה.

בשונה מהשיטה 'סידור לפי שיטות', כאן אפשר לשלב מספר פונקציות של מגבלה או טווח. לדוגמה, אפשר לשלב את השיטות startAt() ו-endAt() כדי להגביל את התוצאות לטווח ערכים מסוים.

גם אם יש רק התאמה אחת לשאילתה, קובץ snapshot עדיין הוא רשימה, אלא שהוא מכיל רק פריט אחד. כדי לגשת לפריט, צריך להריץ לולאה על התוצאה:

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

הגבלת מספר התוצאות

אפשר להשתמש ב-methods limitToFirst() ו-limitToLast() כדי להגדיר מספר מקסימלי של צאצאים שיסונכרנו בקריאה חוזרת (callback) נתונה. לדוגמה, אם משתמשים ב-limitToFirst() כדי להגדיר מגבלה של 100, בהתחלה מקבלים רק עד 100 קריאות חזרה של onChildAdded(). אם יש פחות מ-100 פריטים במסד הנתונים של Firebase, תתבצע קריאה חוזרת של onChildAdded() לכל פריט.

כשהפריטים משתנים, אתם מקבלים onChildAdded() קריאות חזרה (callbacks) על פריטים שמתווספים לשאילתה ו-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(), אלא שהערך של הצומת משמש במקום הערך של מפתח צאצא שצוין.

השלבים הבאים