What is Real Time Database?
A real-time database is a database system which uses real-time processing to handle workloads whose state is constantly changing. This differs from traditional databases containing persistent data, mostly unaffected by time. For example, a stock market changes very rapidly and is dynamic. The Firebase Realtime Database It's a cloud-hosted database powered by Google. Data is stored in JSON format and synchronized in realtime to every connected client(devices). You can build the using their iOS, Android and JavaScript SDKs and all of your clients share one Realtime Database instance and automatically receive updates with any changes in the content of the database.
How does it work?
The Firebase Realtime Database allows you to build rich, collaborative applications by providing secure access to the database directly from client-side code. Data is persisted locally, and even while offline, realtime events continue to fire, it gives the end user a responsive experience. When the device regains connection, the Realtime Database syncs the local data changes with the remote updates that occurred while the client was offline, handling any merge any conflicts automatically. The Realtime Database provides a flexible, expression-based rules language, called Firebase Realtime Database Security Rules, to define how your data should be structured and when data can be read from or written to. If integrated with Firebase Authentication, you can define who has access to what data, and how they can access it. It's a NoSQL database and as such has different optimizations and functionality compared to a relational database. The Realtime Database API is designed to only allow operations that can be executed quickly. This enables you to build a great realtime experience that can serve millions of users without compromising on responsiveness.
Setup
To begin using Firebase you will need to create a free account at https://firebase.google.com/.
- Create a project
- Choose add "Add Firebase to your Android app"
- Enter the app package name (com.antoinecampbell.firebase.demo)
- Enter the SHA-1 fingerprint from your debug certificate
- Download the generated google-services.json file and place it into the app/ directory
- Update the rules for your database to allow reads and writes from all users, as this demo will not cover Firebase Authentication.
{ "rules": { ".read": "true", ".write": "true" } }
- The demo app is ready to be launched
- Optionally, you may bootstrap your database with some records by importing the users-export.json file to your database
App Setup
The first step to getting the app ready to use Firebase is to add Google Play Services to the project. In the root build.gradle file adds the following line to your dependencies (/build.gradle):
classpath 'com.google.gms:google-services:3.0.0'
Next, add the Firebase Realtime Database SDK to your app level build.gradle file dependencies (/app/build.gradle):
compile 'com.google.firebase:firebase-database:9.4.0'
Then, apply the Google Play Services plugin at the bottom of your app level build.gradle file (/app/build.gradle):
apply plugin: 'com.google.gms.google-services'
Finally, we will override the application class to enable Firebase's offline mode before any database references are used (FirebaseDemoApplication.java):
@Override
public void onCreate() {
super.onCreate();
FirebaseDatabase.getInstance().setPersistenceEnabled(true);
}
Retrieving Data
To retrieve data from Firebase Realtime Database we need to query the path containing the list of users, in this case, the path is "users". The data and its children will be returned when queried so all stored users will be returned from the querying the path "users". The only way to query is asynchronous via listeners which return a snapshot of the data at the time of the event. When a listener has attached a snapshot of the data is sent. The listener will be called whenever data on the path being watched is changed, including its children. One thing to keep in mind when querying data from Firebase Realtime Database is that it's not like your typical database. In the efforts to be as fast as possible, some of the more advanced querying options are not available to you such as; partial text searching, object references, and joining. One solution the Firebase team recommends to deal with these limitations is to denormalize the data. The query below returns the value of the "users" path from the database, which is the list of stored users. Also, the Firebase SDK provides built-in deserialization functionality to parse the data into your POJO.
DatabaseReference database = FirebaseDatabase.getInstance().getReference();
...
database.child("users").addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
List users = new ArrayList<>();
for (DataSnapshot userDataSnapshot : dataSnapshot.getChildren()) {
User user = userDataSnapshot.getValue(User.class);
users.add(user);
}
adapter.updateList(users);
}
...
});
There may be cases where you do not want to receive constant updates on data changes and instead simply fetch the data once. To do so, use the addListenerForSingleValueEvent() method instead. In offline situations, data would be returned from the local database if present.
Saving Data
In the demo application, new users are pushed onto the users path. In Firebase, the concept of pushing creates a unique identifier for the new item adding it to the end of the specified path. Data in Firebase Realtime Database is stored as JSON and pushing creates a nested element beneath the specified path. The JSON below shows all the data that is stored for two users.
{
"users" : {
"-Krp8OS_w2ctz41LNeLA" : {
"firstName" : "Raj",
"email" : "rag@gmail.com",
"uid" : "-Krp8OS_w2ctz41LNeLA"
},
"-KrpUlwUggZl3YnhiQgm" : {
"firstName" : "Pankaj",
"email" : "pankaj@gmail.com",
"uid" : "-KrpUlwUggZl3YnhiQgm"
},
"-KrpV9oOPQvuoDg8EHln" : {
"firstName" : "Vinod",
"email" : "vinod@gmail.com",
"uid" : "-KrpV9oOPQvuoDg8EHln"
}
}
You will notice that the key for each user is duplicated within the user itself. The reason being when Firebase returns the data from the users path, accessing the children of the path will not have a unique identifier as part of the object when deserialized into a POJO. So, during the saving process, the user object is set with the unique identifier returned from the push before saving the contents of the user. Essentially, when saving a user, the identifier for the user is created before the content of the user is saved. This allows for simple deserialization, having the unique identifier as part of the user, removing the need to set the identifier after deserialization.
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
User user = new User();
user.setUid(database.child("users").push().getKey());
user.setTitle(emailTextView.getText().toString());
user.setFirstName(firstNameTextView.getText().toString());
database.child("users").child(user.getUid()).setValue(user);
finish();
}
});
Updating Data
Updating your data with Firebase is just like saving, except there is no need to do a push to generate a new key.
fab.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View view) {
user.setEmail(emailTextView.getText().toString());
user.setFirstName(firstNameTextView.getText().toString());
database.child("users").child(user.getUid()).setValue(user);
finish();
}
});
Optionally, a completion listener can be passed to the setValue() method to be notified when the data saves to Firebase servers. However, the listener would not be called when in offline mode so anything UI-related that needed to happen after a save may not be executed. In the demo, this completion listener is omitted, and we know the data will eventually get synchronized when the user is back online.
Deleting Data
Much like saving data, removing data from Firebase is rather simple. We remove the path associated with the user, in this case, "users/<uid>". The code below is from the MainActivity in the demo app.
@Override
public void itemRemoved(int position) {
User user = adapter.getItem(position);
adapter.removeItem(position);
database.child("users").child(user.getUid()).removeValue();
}
Recall that the "users" path is being watched by the listener and deleting a user will trigger a call to onDataChange() passing the new list of users to the adapter. However, if the adapter receives a new list of users the removal animation will not get a chance to complete before notifiyDataSetChanged() is called. To keep the UI running smoothly, calls to updateList() are essentially ignored if the data change was made on the user's device. If the change was made externally, by another device or from the Firebase console, the app will just refresh the content.
public void updateList(List users) {
// Allow recyclerview animations to complete normally if we already know about data changes
if (users.size() != this.users.size() || !this.users.containsAll(users)) {
this.users = users;
notifyDataSetChanged();
}
}
References
1. https://firebase.google.com/docs/database
Great stuff. Quite informative.
ReplyDelete