In previous post, we had known how to read/write list of data object. Today, we’re gonna look at way to display List of Data in an Android App with FirebaseUI FirebaseRecyclerAdapter
.
Related Articles:
– Kotlin Firebase Realtime Database – Read/Write Data example | Android
– Kotlin Firebase Realtime Database – Get List of Data example | Android
I. FirebaseUI Database
To use the FirebaseUI to display list of data, we need:
– Java class for data object (Model)
– Java class for holding UI elements that match with Model’s fields (ViewHolder and layout)
– Custom RecyclerView adapter to map from a collection from Firebase to Android (FirebaseRecyclerAdapter)
– RecyclerView object to set the adapter to provide child views on demand.
1. Model and ViewHolder
– Model
class is a class that represents the data from Firebase:
class Message { var author: String? = "" var body: String? = "" var time: String? = "" // ... }
– ViewHolder
layout (R.layout.item_message) with UI items that correspond to Model
fields:
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"> <TextView android:id="@+id/tvAuthorItem" /> <TextView android:id="@+id/tvTimeItem" /> <TextView android:id="@+id/tvBodyItem" /> </LinearLayout>
– ViewHolder
class contains Android UI fields that point to layout items:
class MessageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bindMessage(message: Message?) { with(message!!) { itemView.tvAuthorItem.text = author itemView.tvTimeItem.text = time itemView.tvBodyItem.text = body } } }
2. FirebaseRecyclerAdapter subclass
We need a subclass of the FirebaseRecyclerAdapter
and implement its populateViewHolder()
method:
private var mAdapter: FirebaseRecyclerAdapter? = null // ... // mMessageReference = FirebaseDatabase.getInstance().getReference("messages") val query = mMessageReference!!.limitToLast(8) mAdapter = object : FirebaseRecyclerAdapter ( Message::class.java, R.layout.item_message, MessageViewHolder::class.java, query) { override fun populateViewHolder(viewHolder: MessageViewHolder?, model: Message?, position: Int) { viewHolder!!.bindMessage(model) } override fun onChildChanged(type: ChangeEventListener.EventType, snapshot: DataSnapshot?, index: Int, oldIndex: Int) { super.onChildChanged(type, snapshot, index, oldIndex) rcvListMessage.scrollToPosition(index) } }
Now look at these lines of code:
val query = mMessageReference!!.limitToLast(8) mAdapter = object : FirebaseRecyclerAdapter( Message::class.java, R.layout.item_message, MessageViewHolder::class.java, query)
– We tell FirebaseRecyclerAdapter
object to use Message.class when reading from the database.
– Each Message will be displayed in a R.layout.item_message
(that has 3 TextView
elements: tvAuthorItem, tvTimeItem, tvBodyItem).
– We indicate class for ViewHolder
– We can just give reference to database node or sort/filter data by using Query
:
val query = mMessageReference val query = mMessageReference.orderByKey() // orderByValue() or orderByChild("...") val query = mMessageReference!!.limitToLast(8) // limitToFirst(..), startAt(...), endAt(...), equalTo(...)
FirebaseRecyclerAdapter
will call populateViewHolder()
method for each Model it finds in database. It passes us the Model
and a ViewHolder
.
So what we should do is map the fields from model to the correct TextView
items:
override fun populateViewHolder(viewHolder: MessageViewHolder?, model: Message?, position: Int) { viewHolder!!.bindMessage(model) }
We can override onChildChanged()
method to do something every time a Child in database has changed its value (add, update…).
3. RecyclerView
Now we set the adapter for RecyclerView
object to provide child views on demand:
val layoutManager = LinearLayoutManager(this) layoutManager.reverseLayout = false rcvListMessage.setHasFixedSize(true) rcvListMessage.layoutManager = layoutManager // private var mAdapter: FirebaseRecyclerAdapter? = null rcvListMessage.adapter = mAdapter
Remember to call adapter cleanup()
method to stop listening for changes in the Firebase database:
override fun onDestroy() { super.onDestroy() mAdapter!!.cleanup() }
4. Dependency
build.gradle file (App-level)
dependencies { // ... implementation 'com.android.support:appcompat-v7:26.0.1' implementation 'com.google.firebase:firebase-database:11.0.4' implementation 'com.firebaseui:firebase-ui-database:2.3.0' } apply plugin: 'com.google.gms.google-services'
II. Practice
1. Goal
We will build an Android App that can:
– create Account, sign in/sign out for Firebase Authentication.
– read/write user to Firebase Realtime Database.
(2 lines above come from this Post).
– write Message item to 2 nodes (/messages/$key
and /user-messages/$userid/$key
) at the same time, then read list of all Message items. (from this Post)
– display list of Messages using FirebaseUI FirebaseRecyclerAdapter
.
2. Technology
– Gradle 3.0.1
– Android Studio 3.x
– Firebase Android SDK 11.x
– Firebase UI Database 2.3.0
3. Project Structure
LoginActivity is for Authentication, then user can enter MessageActivity to send Message to Firebase Realtime Database and show list of Message data.
4. Step by step
4.1 Create Android Project
– Generate new Android Project with package com.javasampleapproach.kotlin.firebase.realtimedb
.
– Follow this instruction to add Firebase Auth and Realtime DB.
4.2 Model
package com.javasampleapproach.kotlin.firebase.realtimedb.model import com.google.firebase.database.IgnoreExtraProperties @IgnoreExtraProperties class User { var name: String? = null var email: String? = null constructor() { // Default constructor required for calls to DataSnapshot.getValue(User.class) } constructor(username: String?, email: String?) { this.name = username this.email = email } }
package com.javasampleapproach.kotlin.firebase.realtimedb.model import com.google.firebase.database.IgnoreExtraProperties import com.google.firebase.database.Exclude @IgnoreExtraProperties class Message { var author: String? = "" var body: String? = "" var time: String? = "" constructor() { // Default constructor required for calls to DataSnapshot.getValue(Message.class) } constructor(author: String, body: String, time: String) { this.author = author this.body = body this.time = time } @Exclude fun toMap(): Map{ val result = HashMap () result.put("author", author!!) result.put("body", body!!) result.put("time", time!!) return result } }
4.3 LoginActivity
In this tutorial, we don’t explain way to authenticate an user again. To know how to implement Firebase Authentication App Client, please visit:
Kotlin Firebase Authentication – How to Sign Up, Sign In, Sign Out, Verify Email | Android
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools" android:id="@+id/main_layout" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:weightSum="3"> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="2" android:gravity="center_horizontal" android:orientation="vertical"> <TextView android:id="@+id/tvTitle" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginBottom="16dp" android:gravity="center" android:text="ozenero.com" android:textSize="28sp" /> <TextView android:id="@+id/tvStatus" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:padding="4dp" android:text="Signed Out" android:textSize="14sp" /> <TextView android:id="@+id/tvDetail" android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:padding="4dp" android:textSize="14sp" tools:text="Firebase User ID: 123456789abc" /> </LinearLayout> <RelativeLayout android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" android:background="#E0E0E0" android:gravity="center_vertical"> <LinearLayout android:id="@+id/email_password_fields" android:layout_width="fill_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingLeft="16dp" android:paddingRight="16dp"> <EditText android:id="@+id/edtEmail" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:hint="Email" android:inputType="textEmailAddress" /> <EditText android:id="@+id/edtPassword" android:layout_width="match_parent" android:layout_height="wrap_content" android:layout_weight="1" android:hint="Password" android:inputType="textPassword" /> </LinearLayout> <LinearLayout android:id="@+id/email_password_buttons" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_below="@+id/email_password_fields" android:orientation="horizontal" android:paddingLeft="16dp" android:paddingRight="16dp"> <Button android:id="@+id/btn_email_sign_in" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="4dp" android:layout_marginRight="4dp" android:layout_weight="1" android:text="Sign In" /> <Button android:id="@+id/btn_email_create_account" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="4dp" android:layout_marginRight="4dp" android:layout_weight="1" android:text="Create Account" /> </LinearLayout> <LinearLayout android:id="@+id/layout_signed_in_buttons" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_centerInParent="true" android:orientation="horizontal" android:paddingLeft="16dp" android:paddingRight="16dp" android:visibility="gone" android:weightSum="2.0"> <Button android:id="@+id/btn_sign_out" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="4dp" android:layout_marginRight="4dp" android:layout_weight="1.0" android:text="Sign Out" /> <Button android:id="@+id/btn_test_message" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_marginLeft="4dp" android:layout_marginRight="4dp" android:layout_weight="1.0" android:text="Test Message" /> </LinearLayout> </RelativeLayout> </LinearLayout>
package com.javasampleapproach.kotlin.firebase.realtimedb import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.view.View import kotlinx.android.synthetic.main.activity_login.* import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseUser import android.widget.Toast import android.util.Log import android.text.TextUtils import android.content.Intent import com.google.firebase.database.FirebaseDatabase import com.javasampleapproach.kotlin.firebase.realtimedb.model.User class LoginActivity : AppCompatActivity(), View.OnClickListener { private val TAG = "LoginActivity" private var mAuth: FirebaseAuth? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_login) btn_email_sign_in.setOnClickListener(this) btn_email_create_account.setOnClickListener(this) btn_sign_out.setOnClickListener(this) btn_test_message.setOnClickListener(this) mAuth = FirebaseAuth.getInstance() } override fun onStart() { super.onStart() val currentUser = mAuth!!.currentUser updateUI(currentUser) } override fun onClick(view: View?) { val i = view!!.id when (i) { R.id.btn_email_create_account -> createAccount(edtEmail.text.toString(), edtPassword.text.toString()) R.id.btn_email_sign_in -> signIn(edtEmail.text.toString(), edtPassword.text.toString()) R.id.btn_sign_out -> signOut() R.id.btn_test_message -> testMessage() } } private fun createAccount(email: String, password: String) { Log.e(TAG, "createAccount:" + email) if (!validateForm(email, password)) { return } mAuth!!.createUserWithEmailAndPassword(email, password) .addOnCompleteListener(this) { task -> if (task.isSuccessful) { Log.e(TAG, "createAccount: Success!") // update UI with the signed-in user's information val user = mAuth!!.currentUser updateUI(user) writeNewUser(user!!.uid, getUsernameFromEmail(user.email), user.email) } else { Log.e(TAG, "createAccount: Fail!", task.exception) Toast.makeText(applicationContext, "Authentication failed!", Toast.LENGTH_SHORT).show() updateUI(null) } } } private fun signIn(email: String, password: String) { Log.e(TAG, "signIn:" + email) if (!validateForm(email, password)) { return } mAuth!!.signInWithEmailAndPassword(email, password) .addOnCompleteListener(this) { task -> if (task.isSuccessful) { Log.e(TAG, "signIn: Success!") // update UI with the signed-in user's information val user = mAuth!!.currentUser updateUI(user) } else { Log.e(TAG, "signIn: Fail!", task.exception) Toast.makeText(applicationContext, "Authentication failed!", Toast.LENGTH_SHORT).show() updateUI(null) } if (!task.isSuccessful) { tvStatus.text = "Authentication failed!" } } } private fun signOut() { mAuth!!.signOut() updateUI(null) } private fun validateForm(email: String, password: String): Boolean { if (TextUtils.isEmpty(email)) { Toast.makeText(applicationContext, "Enter email address!", Toast.LENGTH_SHORT).show() return false } if (TextUtils.isEmpty(password)) { Toast.makeText(applicationContext, "Enter password!", Toast.LENGTH_SHORT).show() return false } if (password.length < 6) { Toast.makeText(applicationContext, "Password too short, enter minimum 6 characters!", Toast.LENGTH_SHORT).show() return false } return true } private fun updateUI(user: FirebaseUser?) { if (user != null) { tvStatus.text = "User Email: " + user.email tvDetail.text = "Firebase User ID: " + user.uid email_password_buttons.visibility = View.GONE email_password_fields.visibility = View.GONE layout_signed_in_buttons.visibility = View.VISIBLE } else { tvStatus.text = "Signed Out" tvDetail.text = null email_password_buttons.visibility = View.VISIBLE email_password_fields.visibility = View.VISIBLE layout_signed_in_buttons.visibility = View.GONE } } private fun writeNewUser(userId: String, username: String?, email: String?) { val user = User(username, email) FirebaseDatabase.getInstance().reference.child("users").child(userId).setValue(user) } private fun getUsernameFromEmail(email: String?): String { return if (email!!.contains("@")) { email.split("@".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] } else { email } } private fun testMessage() { startActivity(Intent(this, MessageActivity::class.java)) } }
4.4 ViewHolder
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="vertical" android:paddingBottom="7dp"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal" android:weightSum="2"> <TextView android:id="@+id/tvAuthorItem" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="author" android:textStyle="bold" /> <TextView android:id="@+id/tvTimeItem" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="1" android:text="time" /> </LinearLayout> <TextView android:id="@+id/tvBodyItem" android:layout_width="match_parent" android:layout_height="wrap_content" android:text="body" android:textColor="@android:color/holo_blue_dark" android:textSize="20sp" /> </LinearLayout>
package com.javasampleapproach.kotlin.firebase.realtimedb.viewholder import android.support.v7.widget.RecyclerView import android.view.View import com.javasampleapproach.kotlin.firebase.realtimedb.model.Message import kotlinx.android.synthetic.main.item_message.view.* class MessageViewHolder(itemView: View) : RecyclerView.ViewHolder(itemView) { fun bindMessage(message: Message?) { with(message!!) { itemView.tvAuthorItem.text = author itemView.tvTimeItem.text = time itemView.tvBodyItem.text = body } } }
4.5 MessageActivity
<?xml version="1.0" encoding="utf-8"?> <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" android:weightSum="4"> <LinearLayout android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="0.5" android:gravity="center_horizontal" android:orientation="vertical"> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_gravity="center_horizontal" android:text="ozenero.com" android:textSize="28sp" /> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:gravity="center" android:text="Messages" android:textSize="18sp" /> </LinearLayout> <android.support.v7.widget.RecyclerView android:id="@+id/rcvListMessage" android:layout_width="match_parent" android:layout_height="0dp" android:layout_weight="2.5" android:padding="16dp" /> <LinearLayout android:layout_width="fill_parent" android:layout_height="0dp" android:layout_weight="1" android:background="#E0E0E0" android:gravity="center" android:orientation="vertical" android:paddingLeft="16dp" android:paddingRight="16dp"> <EditText android:id="@+id/edtSentText" android:layout_width="match_parent" android:layout_height="wrap_content" android:inputType="text" /> <LinearLayout android:id="@+id/layout_signed_in_buttons" android:layout_width="fill_parent" android:layout_height="wrap_content" android:layout_centerInParent="true" android:gravity="right" android:orientation="horizontal" android:weightSum="2"> <Button android:id="@+id/btnBack" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="0.5" android:text="Back" /> <Button android:id="@+id/btnSend" android:layout_width="0dp" android:layout_height="wrap_content" android:layout_weight="0.5" android:text="Send" /> </LinearLayout> </LinearLayout> </LinearLayout>
package com.javasampleapproach.kotlin.firebase.realtimedb import android.support.v7.app.AppCompatActivity import android.os.Bundle import android.text.TextUtils import android.util.Log import android.widget.Toast import kotlinx.android.synthetic.main.activity_message.* import com.google.firebase.auth.FirebaseAuth import com.google.firebase.auth.FirebaseUser import com.google.firebase.database.* import com.javasampleapproach.kotlin.firebase.realtimedb.model.Message import java.text.SimpleDateFormat import java.util.Calendar import com.javasampleapproach.kotlin.firebase.realtimedb.model.User import com.google.firebase.database.DatabaseError import com.google.firebase.database.DataSnapshot import com.google.firebase.database.ChildEventListener import com.firebase.ui.database.FirebaseRecyclerAdapter import android.support.v7.widget.LinearLayoutManager import com.firebase.ui.database.ChangeEventListener import com.javasampleapproach.kotlin.firebase.realtimedb.viewholder.MessageViewHolder class MessageActivity : AppCompatActivity() { private val TAG = "MessageActivity" private val REQUIRED = "Required" private var user: FirebaseUser? = null private var mDatabase: DatabaseReference? = null private var mMessageReference: DatabaseReference? = null private var mMessageListener: ChildEventListener? = null private var mAdapter: FirebaseRecyclerAdapter? = null override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_message) mDatabase = FirebaseDatabase.getInstance().reference mMessageReference = FirebaseDatabase.getInstance().getReference("messages") user = FirebaseAuth.getInstance().currentUser firebaseListenerInit() btnSend.setOnClickListener { submitMessage() edtSentText.setText("") } btnBack.setOnClickListener { finish() } val layoutManager = LinearLayoutManager(this) layoutManager.reverseLayout = false rcvListMessage.setHasFixedSize(true) rcvListMessage.layoutManager = layoutManager val query = mMessageReference!!.limitToLast(8) mAdapter = object : FirebaseRecyclerAdapter ( Message::class.java, R.layout.item_message, MessageViewHolder::class.java, query) { override fun populateViewHolder(viewHolder: MessageViewHolder?, model: Message?, position: Int) { viewHolder!!.bindMessage(model) } override fun onChildChanged(type: ChangeEventListener.EventType, snapshot: DataSnapshot?, index: Int, oldIndex: Int) { super.onChildChanged(type, snapshot, index, oldIndex) rcvListMessage.scrollToPosition(index) } } rcvListMessage.adapter = mAdapter } private fun firebaseListenerInit() { val childEventListener = object : ChildEventListener { override fun onChildAdded(dataSnapshot: DataSnapshot?, previousChildName: String?) { // A new message has been added // onChildAdded() will be called for each node at the first time val message = dataSnapshot!!.getValue(Message::class.java) Log.e(TAG, "onChildAdded:" + message!!.body) } override fun onChildChanged(dataSnapshot: DataSnapshot?, previousChildName: String?) { Log.e(TAG, "onChildChanged:" + dataSnapshot!!.key) // A message has changed val message = dataSnapshot.getValue(Message::class.java) Toast.makeText(this@MessageActivity, "onChildChanged: " + message!!.body, Toast.LENGTH_SHORT).show() } override fun onChildRemoved(dataSnapshot: DataSnapshot?) { Log.e(TAG, "onChildRemoved:" + dataSnapshot!!.key) // A message has been removed val message = dataSnapshot.getValue(Message::class.java) Toast.makeText(this@MessageActivity, "onChildRemoved: " + message!!.body, Toast.LENGTH_SHORT).show() } override fun onChildMoved(dataSnapshot: DataSnapshot?, previousChildName: String?) { Log.e(TAG, "onChildMoved:" + dataSnapshot!!.key) // A message has changed position val message = dataSnapshot.getValue(Message::class.java) Toast.makeText(this@MessageActivity, "onChildMoved: " + message!!.body, Toast.LENGTH_SHORT).show() } override fun onCancelled(databaseError: DatabaseError?) { Log.e(TAG, "postMessages:onCancelled", databaseError!!.toException()) Toast.makeText(this@MessageActivity, "Failed to load Message.", Toast.LENGTH_SHORT).show() } } mMessageReference!!.addChildEventListener(childEventListener) // copy for removing at onStop() mMessageListener = childEventListener } override fun onStop() { super.onStop() if (mMessageListener != null) { mMessageReference!!.removeEventListener(mMessageListener) } } override fun onDestroy() { super.onDestroy() mAdapter!!.cleanup() } private fun submitMessage() { val body = edtSentText.text.toString() if (TextUtils.isEmpty(body)) { edtSentText.error = REQUIRED return } // User data change listener mDatabase!!.child("users").child(user!!.uid).addListenerForSingleValueEvent(object : ValueEventListener { override fun onDataChange(dataSnapshot: DataSnapshot) { val user = dataSnapshot.getValue(User::class.java) if (user == null) { Log.e(TAG, "onDataChange: User data is null!") Toast.makeText(this@MessageActivity, "onDataChange: User data is null!", Toast.LENGTH_SHORT).show() return } writeNewMessage(body) } override fun onCancelled(error: DatabaseError) { // Failed to read value Log.e(TAG, "onCancelled: Failed to read user!") } }) } private fun writeNewMessage(body: String) { val time = SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(Calendar.getInstance().time) val message = Message(getUsernameFromEmail(user!!.email), body, time) val messageValues = message.toMap() val childUpdates = HashMap () val key = mDatabase!!.child("messages").push().key childUpdates.put("/messages/" + key, messageValues) childUpdates.put("/user-messages/" + user!!.uid + "/" + key, messageValues) mDatabase!!.updateChildren(childUpdates) } private fun getUsernameFromEmail(email: String?): String { return if (email!!.contains("@")) { email.split("@".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()[0] } else { email } } }
4.6 Run & Check result
- Use Android Studio, build and Run your Android App:
- Firebase Console:
III. Source code
Kotlin-FirebaseRealtimeDB-FirebaseRecyclerAdapter