A Firebase Realtime Database List Data Tutorial
The preceding chapters have introduced the Firebase realtime database and explored the concepts of reading and writing data before working through a tutorial outlining the steps in reading and writing individual data items.
In this, the final chapter covering the Firebase Realtime Database, we will build on the information covered in the Working with Firebase Realtime Database Lists chapter by creating an example app that makes use of many of the list data features of the realtime database.
About the Data List App Project
The project created in this chapter takes the form of a simple To Do list app in which items may be added to, or removed from a list stored in a Firebase realtime database. In addition to these features, the app will also allow the user to search for items contained in the list.
Creating the Data List Project
Launch Android Studio and select the Start a new Android Studio project quick start option from the welcome screen.
Within the new project dialog, enter RealtimeDBList into the Application name field and your domain as the Company Domain setting before clicking on the Next button.
On the form factors screen, enable the Phone and Tablet option and set the minimum SDK to API 16: Android 4.1 (Jellybean). Continue through the screens, requesting the creation of an Empty Activity named RealtimeDBListActivity with a corresponding layout named activity_realtime_dblist.
Configuring the Project for Realtime Database Access
Before code can be written to make use of the realtime database, some library dependencies need to be added to the build configuration.
To add Firebase Realtime Database support, select the Tools -> Firebase menu option and click on the Realtime Database entry in the resulting Firebase assistant panel. Once selected, click on the Save and retrieve data link followed by the Connect to Firebase button followed by the Add the Realtime Database to your app button once the connection has been established.
A dialog will appear listing the changes that will be made to the project build files to add realtime database support to the project. Review these changes before clicking on the Accept Changes button.
Designing the User Interface
Once Android Studio has finished creating the new project, locate the activity_realtime_dblist.xml layout file and load it into the layout editor. Begin by selecting and deleting the default “Hello World!” TextView widget so that the layout canvas is blank. Before adding any components to the view, turn off Autoconnect mode.
The completed layout for the user interface is going to consist of a ListView, a Plain Text EditText and three Button views. Begin by dragging, dropping, positioning and sizing the widgets on the layout canvas so that the layout resembles that shown in Figure 25-1:
Figure 25-1
With the ListView component selected in the layout, use the Properties tool window to change the ID to dataListView.
Click and drag from the constraint anchor located in the center of the top edge of the ListView to the top edge of the parent container and release. This will establish a constraint from the top of the ListView to the top of the parent ConstraintLayout. Repeat these steps to attach the left and right-hand edges of the ListView to the corresponding sides of the parent, and to attach the bottom edge of the ListView to the top of the EditText widget.
With the ListView still selected, set the layout_width and layout_height properties to 0dp. This switches the component to match constraint mode causing the widget to resize based on the constraints applied to it:
Figure 25-2
Next, establish constraints from the left and right-hand edges of the EditText widget to the matching sides of the parent and from the bottom of the EditText to the top of the center button.
Click on the left-most Button widget, then hold down the shift-key while clicking on the two remaining buttons so that all three are selected. Right-click on any of the Button widgets and select the Center Horizontally option from the popup menu.
Add constraints from the bottom of each button to the bottom of the parent layout.
Using the properties tool window, delete the “Name” text from the EditText view and set the text properties of the three buttons to Add, Find and Delete and configure the onClick properties to call methods named addItem, findItems, and deleteItem respectively.
On completion of these steps, the layout should resemble that illustrated in Figure 25-3:
Figure 25-3
Before proceeding, change the ID for the EditText component to itemText and the IDs for the Find and Delete buttons to findButton and deleteButton.
Performing Initialization Tasks
As is now customary, a number of initialization tasks will need to be performed within the onCreate() method located in the RealtimeDBListActivity.java file. Locate this file, load it into the Android Studio code editor and modify it as follows to declare variables, import required packages and perform some basic initialization:
package com.ebookfrenzy.realtimedblist; import android.support.v7.app.AppCompatActivity; import android.os.Bundle; import android.widget.Button; import android.widget.EditText; import android.widget.ListView; import com.google.firebase.database.DatabaseReference; import com.google.firebase.database.FirebaseDatabase; public class RealtimeDBList extends AppCompatActivity { private ListView dataListView; private EditText itemText; private Button findButton; private Button deleteButton; private Boolean searchMode = false; private Boolean itemSelected = false; private int selectedPosition = 0; private FirebaseDatabase database = FirebaseDatabase.getInstance(); private DatabaseReference dbRef = database.getReference("todo"); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_realtime_dblist); dataListView = (ListView) findViewById(R.id.dataListView); itemText = (EditText) findViewById(R.id.itemText); findButton = (Button) findViewById(R.id.findButton); deleteButton = (Button) findViewById(R.id.deleteButton); deleteButton.setEnabled(false); } }
The above code changes obtain references to the user interface components together with an instance of the database reference configured with a path of “/todo”. The Delete button is then disabled to prevent the user from attempting to delete an item from the list without first selecting one. Other variables have also been declared that will be used in later code to track the status of the user interface in terms of the currently selected ListView item and whether or not the user is searching for items.
Implementing the ListView
The list of items will be displayed to the user via the ListView component. The ListView class requires an ArrayAdapter instance and an ArrayList object containing the items to be displayed. This example app will also use an array to store the keys assigned to each child node in the list. Remaining within the RealtimeDBListActivity.java file make the following additions:
package com.ebookfrenzy.firebaselist; . . import android.widget.ArrayAdapter; import android.widget.AdapterView; import android.view.View; . . import java.util.ArrayList; import java.util.Iterator; public class MainActivity extends AppCompatActivity { ArrayList<String> listItems = new ArrayList<String>(); ArrayList<String> listKeys = new ArrayList<String>(); ArrayAdapter<String> adapter; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.activity_realtime_dblist); dataListView = (ListView) findViewById(R.id.dataListView); itemText = (EditText) findViewById(R.id.itemText); addButton = (Button) findViewById(R.id.addButton); findButton = (Button) findViewById(R.id.findButton); deleteButton = (Button) findViewById(R.id.deleteButton); deleteButton.setEnabled(false); adapter = new ArrayAdapter<String>(this, android.R.layout.simple_list_item_single_choice, listItems); dataListView.setAdapter(adapter); dataListView.setChoiceMode(ListView.CHOICE_MODE_SINGLE); dataListView.setOnItemClickListener( new AdapterView.OnItemClickListener() { public void onItemClick(AdapterView<?> parent, View view, int position, long id) { selectedPosition = position; itemSelected = true; deleteButton.setEnabled(true); } }); addChildEventListener(); } }
The code added to the onCreate() method initializes an ArrayAdapter instance with the content of the listItems array and configures the adapter for single item selection. The adapter is then assigned to the ListView component and the ListView component also configured for single item selection (it will only be possible for the user to select and delete items one at a time). An item click listener is then attached to the ListView which records the currently selected item number in the previously declared selectedPosition variable, indicates that an item has been selected and then enables the Delete button.
Code has also been added to call an additional method, the purpose of which is to add a child event listener to the database reference. This method now needs to be added to the project.
Implementing Child Event Listener
The app will need to receive notifications whenever new child nodes are added to the data list. A call to a method named addChildEventListener() has already been added to the onCreate() method and now needs to be added to the RealtimeDBListActivity.java file:
. . import com.google.firebase.database.ChildEventListener; import com.google.firebase.database.DataSnapshot; import com.google.firebase.database.DatabaseError; import com.google.firebase.database.Query; import com.google.firebase.database.ValueEventListener; . . private void addChildEventListener() { ChildEventListener childListener = new ChildEventListener() { @Override public void onChildAdded(DataSnapshot dataSnapshot, String s) { adapter.add( (String) dataSnapshot.child("description").getValue()); listKeys.add(dataSnapshot.getKey()); } @Override public void onChildChanged(DataSnapshot dataSnapshot, String s) { } @Override public void onChildMoved(DataSnapshot dataSnapshot, String s) { } @Override public void onChildRemoved(DataSnapshot dataSnapshot) { String key = dataSnapshot.getKey(); int index = listKeys.indexOf(key); if (index != -1) { listItems.remove(index); listKeys.remove(index); adapter.notifyDataSetChanged(); } } @Override public void onCancelled(DatabaseError databaseError) { } }; dbRef.addChildEventListener(childListener); } . .
For the purposes of this project, only the onChildAdded() and onChildRemoved() callback methods need to do any work. The onChildAdded() method will be called when a new child is added to the data list and will be passed a DataSnapshot object containing the key and value of the new child. As implemented above, the value is added to the ListView adaptor so that it will appear in the list, and the key is stored in the listKeys array for use when deleting items from the data list.
The onChildRemoved() method will, as the name suggests, be called when a child is removed from the list. This method will be called under two different circumstances. The first, and most obvious scenario, is when the current instance of the app removes a child from the list. This will be the result of the user clicking on the Delete button which will, in turn, trigger a call to the deleteItem() method which will be added later in the chapter. This method will remove the item from the database, thereby triggering a call to the onChildRemoved() callback method where the deleted node will be provided in the form of a data snapshot. The code in the onChildRemoved() method extracts the key from the snapshot, identifies the position of the node in the listKeys array and, if it exists, removes the entry from both the listKeys and listItems array.
In the second scenario, the app will receive notification that the child has been removed by another instance of the app. In this situation the child will already have been removed from the database, so only the local data arrays need to be updated to remove the data. As currently written, the onChildRemoved() method is designed to handle both of these possibilities.
Adding Items to the List
When the user taps the Add button, any text entered into the EditText view needs to be added as a new child to the data list within the database tree. During the design of the user interface layout, the onClick property of the Add button was configured to call a method named addItem() which must now be added to the activity Java class:
public void addItem(View view) { String item = itemText.getText().toString(); String key = dbRef.push().getKey(); itemText.setText(""); dbRef.child(key).child("description").setValue(item); adapter.notifyDataSetChanged(); }
The method extracts the string entered into the EditText view before calling the push() method of the database reference to create a new child. The key generated for the new child is stored in the listKeys array, the EditText view is cleared and the value saved to the following path:
/todo/<key>/description
The addition of the new child to the list will trigger a call to the onChildAdded() method of the child event listener which was added to the project earlier in the chapter. This method will extract the value from the data snapshot and add it to the ListView adapter array. The key will also be stored in the listKeys array.
The last task performed by the addItem() method is to notify the adapter that the data has changed, thereby triggering an update of the list displayed to the user.
Deleting List Items
The Delete button is configured to call the deleteItem() method. This method is responsible for deleting the currently selected item from the database.
The code for this method, which now needs to be added to the RealtimeDBListActivity.java file reads as follows:
public void deleteItem(View view) { dataListView.setItemChecked(selectedPosition, false); dbRef.child(listKeys.get(selectedPosition)).removeValue(); }
The above code deselects the currently selected item in the ListView and then uses the selectedPosition variable as an index into the listKeys array. Having identified the key associated with the item to be deleted, that key is used to remove the child node from the database. The remaining steps of the deletion process will be completed by the code in the onChildRemoved() callback method which will be called as a result of the child node being removed from the list.
Querying the List
The last task before testing the app is to implement the code for the Find button. This button is configured to call a method named findItems() when clicked and will need to make use of the Query class to find list items that match the current text in the EditText widget. The list items that match the search criteria will be displayed in the ListView in alphabetical order.
The Find button is intended to be dual purpose. Once the user has initiated a search, it will change into a Clear button which, when clicked, will clear the filtered results and display the full list of items in the original chronological order. This is achieved by performing a second query using “order by key” with no additional filtering:
public void findItems(View view) { Query query; if (!searchMode) { findButton.setText("Clear"); query = dbRef.orderByChild("description"). equalTo(itemText.getText().toString()); searchMode = true; } else { searchMode = false; findButton.setText("Find"); query = dbRef.orderByKey(); } if (itemSelected) { dataListView.setItemChecked(selectedPosition, false); itemSelected = false; deleteButton.setEnabled(false); } query.addListenerForSingleValueEvent(queryValueListener); }
The line of code at the end of the method attaches a single value event listener named queryValueListener to the Query object. This listener will be called when the query operation returns with the results of the search. Implement this listener now so that it reads as follows:
ValueEventListener queryValueListener = new ValueEventListener() { @Override public void onDataChange(DataSnapshot dataSnapshot) { Iterable<DataSnapshot> snapshotIterator = dataSnapshot.getChildren(); Iterator<DataSnapshot> iterator = snapshotIterator.iterator(); adapter.clear(); listKeys.clear(); while (iterator.hasNext()) { DataSnapshot next = (DataSnapshot) iterator.next(); String match = (String) next.child("description").getValue(); String key = next.getKey(); listKeys.add(key); adapter.add(match); } } @Override public void onCancelled(DatabaseError databaseError) { } };
The onDataChanged() method of the listener will be called when the query returns and will be passed a data snapshot containing the items that match the search criteria. The method clears both the adapter and listKeys arrays and then iterates through the children in the snapshot. For each child, the key is stored on the listKeys array and the value added to the adapter so that it will appear in the ListView.
Changing the Database Rules
As this example app does not use Firebase Authentication, some changes need to be made to the database rules. Within the Firebase console, temporarily modify the rules as follows and then click on the Publish button:
{ "rules": { ".read": true, ".write": true } }
Testing the App
Open a browser window, navigate to the Firebase console and open the Data page of the Database panel. Compile and run the app on a physical device or emulator and enter several items to the list by entering text into the EditText field and tapping the Add button:
Figure 25-4
Note that in addition to appearing in the ListView within the app, the items also appear in the database tree within the Firebase console:
Figure 25-5
Select an item from the list and click the Delete button to remove it from the database. Verify that the item is removed from both the ListView and the tree in the Firebase console.
Next, perform a search for an item currently included in the list. Clear the search results, add duplicate items to the list and perform another search verifying that both instances of the item are listed.
Finally, run the app concurrently, using multiple devices or emulator sessions, and test that any data additions or deletions are reflected in realtime in both instances of the app.
Summary
This chapter contained a tutorial intended to demonstrate the use of the Firebase Realtime Database to store and manage data in list form including saving, deleting and searching for list items. This has involved the use of the push() method of the DatabaseReference class together with the Query class and both value and child event listeners.