This is a guest post by Veeresh Charantimath, Android Developer at Grow Fit, and Arpit Goyal, Head of Consumer Engineering at Grow Fit. Veeresh and Arpit were the first to use HyperTrack with their Firebase app, and we felt it would be great to have them share their learnings building on the two platforms together.
At Grow Fit, we deliver healthy, and packaged food. Every package is prepared fresh from scratch every day, and dispatched within 30 minutes of preparation. Tracking these dispatches is a critical need for our business. In this post, we will see how HyperTrack, Firebase and Twilio come together to deliver this.
Implementation overview
Our drivers use a mobile app to fulfill order dispatches assigned to them. This app uses the Firebase and HyperTrack Android SDKs. More specifically, this is what the order tracking flow in the app looks like:
- Driver logs in to the app, using Firebase Authentication.
- On successful authentication, app gets-or-creates a user object on HyperTrack.
- The app fetches new dispatches for the current date, from the Firebase Realtime Database.
- For a new dispatch, the app starts tracking with HyperTrack, and generates a web tracking URL
- The web tracking URL is saved in the Firebase Realtime Database, and sent via SMS to the customers using the Twilio SMS API.
- When dispatch is completed, app ends tracking on HyperTrack.
Let's look at how these components are integrated, starting with the data models on the Firebase Realtime Database.
Firebase Realtime Database
Firebase Realtime Database is a NoSQL hosted database, where data is synced across all clients in real-time. The database can be imagined as a JSON tree, where each node can be thought of as a database table. In our database setup, we have nodes for Customers, Addresses, Subscriptions, Dispatches, and Drivers.
Let's look at one of these nodes in more detail. The Dispatches node is a tree with dispatch identifiers.
Each of these objects can be expanded further, to show properties of the Dispatch model.
Database listeners
Database updates to a node are available to all clients who are listening to the node. The Firebase Android SDK provides three types of listeners which perform sync updates.
ValueEventListener
ChildEventListener
SingleValueEventListener
Let's look at how these listeners work, and how we use them to build our use-case.
1. ValueEventListener
The ValueEventListener reads and listens for changes to the entire contents of a path. Here’s how we would create a listener to the Dispatches node.
DatabaseReference dispatcheReference = FirebaseDatabase.getInstance().getReference().child("dispatches");
dispatcheReference.addValueEventListener(new ValueEventListener() {
@Override
public void onDataChange(DataSnapshot dataSnapshot) {
Dispatch dispatch = dataSnapshot.getValue(Dispatch.class);
}
@Override
public void onCancelled(DatabaseError databaseError) {
}
});
Attaching a ValueEventListener
to the dispatches node will fetch all the dispatches, and de-serialize them to your dispatch POJO. The DatabaseReference
object represents a particular location in your database and can be used for reading or writing data to that database location. The DataSnapshot
object contains data from a database location. Any time you read the database, you will receive the data as a DataSnapshot
.
There's one caveat in this implementation. Imagine that you attach your ValueEventListener
to the dispatches node, but you want to only know which of the dispatches change. Using a ValueEventListener
here will trigger the onDataChange()
method, with the entire data snapshot of the node. This is not the desired result. To get more granular control on how the data is received, we will use the ChildEventListener
.
2. ChildEventListener
With the ChildEventListener
, we can listen to child events occurring at a location. When child locations are added, removed, changed, or moved, the listener will be triggered for the appropriate event. This listener will give you more control on what exactly got modified. Onto sample code for the listener below.
DatabaseReference dispatcheReference = FirebaseDatabase.getInstance().getReference().child("dispatches");
dispatcheReference.addChildEventListener(new ChildEventListener() {
@Override
public void onChildAdded(DataSnapshot dataSnapshot, String s) {
Dispatch dispatch = dataSnapshot.getValue(Dispatch.class);
}
@Override
public void onChildChanged(DataSnapshot dataSnapshot, String s) {
Dispatch dispatch = dataSnapshot.getValue(Dispatch.class);
}
@Override
public void onChildRemoved(DataSnapshot dataSnapshot) {
Dispatch dispatch = dataSnapshot.getValue(Dispatch.class);
}
@Override
public void onChildMoved(DataSnapshot dataSnapshot, String s) {
Dispatch dispatch = dataSnapshot.getValue(Dispatch.class);
}
@Override
public void onCancelled(DatabaseError databaseError) {
}
});
3. SingleValueEventListener
This is a one-shot listener, which gets the data once, and does not listen to the subsequent changes.
Remember, if a listener has been added multiple times to a data location, it is called multiple times for each event. You must detach it the same number of times to remove it completely. Calling removeEventListener()
on a parent listener does not automatically remove listeners registered on its child nodes; removeEventListener()
must also be called on any child listeners to remove the callback.
Writing data
Writing is a straight forward operation which has two primary methods
- Use
setValue()
to set the data at a given location to the given value. If already exists, overwrites data including any child nodes. - Use
updateChildren()
if you want to write to specific children of a node without overwriting other child nodes
Firebase provides a push()
function that generates a unique ID every time a new child is added to the specified Firebase reference. This is a must read doc which explains generation and ordering of the keys. More on how to read and write.
Database queries
It’s important to know how data can be sorted and filtered from the Realtime Database. Although the flexibility of the queries are limited but can be compensated with proper database structure. Let's look at some examples.
Get all dispatches for a particular date: orderByChild()
orders results by the value of a specified child key, in this case dispatch_date
, and equalTo()
returns items equal to the specified value.
DatabaseReference dispatchReference = FirebaseDatabase.getInstance().getReference().child("dispatches");
Query query = dispatchReference.orderByChild("dispatch_date").equalTo(date);
//add listener to query
Get a driver with a particular phone number: Pass the phone
to the orderByChild()
and equalTo()
methods.
DatabaseReference dispatchReference = FirebaseDatabase.getInstance().getReference().child("drivers");
Query query = dispatchReference.orderByChild("phone").equalTo(phone);
Get last 100 dispatches of a customer: Pass the customer_id
to the orderByChild()
and equalTo()
methods. Use the limitToLast()
method to limit results to 100.
DatabaseReference dispatchReference = FirebaseDatabase.getInstance().getReference().child("dispatches");
Query query = dispatchReference.orderByChild("customer_id").equalTo(customerID).limitToLast(100);
Get all dispatches between start and end date: Use startAt()
and endAt()
to filter between dates (long fields).
DatabaseReference dispatchReference = FirebaseDatabase.getInstance().getReference().child("dispatches");
Query query = dispatchReference.orderByChild("date").startAt(startDate).endAt(endDate);
Tips on data structure
- Plan ahead of time how data is going to be stored for easy retrieval.
- Avoid nesting of nodes and try to flatten your nodes.
- Duplication (De-normalization) of data is encouraged, read more.
- Structure your data according to your views, fetch only what is required to interact with.
Firebase Authentication
We use the Email and Password Authentication with Firebase, which works flawlessly. It is straight forward to implement with their docs.
Firebase Remote Config
We use remote config on Firebase to enforce the drivers to update the app, especially in cases where some may have disabled auto-updates, or if we release a critical update.
HyperTrack
We found integrating HyperTrack to be very straightforward. You can follow the docs here. For every driver in our system, we create a corresponding HyperTrack user object.
HyperTrack.createUser(firebaseUser.getDisplayName(), phoneNumber, phoneNumber, new HyperTrackCallback() {
@Override
public void onSuccess(@NonNull SuccessResponse response) {
if (response.getResponseObject() != null) {
User user = (User) response.getResponseObject();
saveUserToFirebase(user);
}
}
@Override
public void onError(@NonNull ErrorResponse errorResponse) {
Snackbar.make(findViewById(android.R.id.content), "(HT) (Registration) User could not be created - " + errorResponse.getErrorMessage(), Snackbar.LENGTH_LONG).show();
}
});
We store the HyperTrack user identifier in Firebase for further queries and operations. Once new dispatches come in, we start tracking on HyperTrack. Each dispatch is represented with an HyperTrack action object, creating which gives us a web tracking URL.
HyperTrack.startTracking()
ActionParams actionParams = new ActionParamsBuilder().setExpectedPlace(expectedPlace)
.setType(Action.ACTION_TYPE_DELIVERY)
.setLookupId(dispatchId)
.build();
HyperTrack.createAndAssignAction(actionParams, new HyperTrackCallback() {
@Override
public void onSuccess(@NonNull SuccessResponse response) {
Action action = (Action) response.getResponseObject();
String trackingUrl = action.getTrackingURL();
// Upload this to Firebase
}
});
We send this web tracking URL to our customers, through which they are able to track their dispatch in real-time.
Conclusion
Setting up live tracking for our dispatches was a breeze with Firebase and HyperTrack. We could ship a light-weight app for our driver fleet, which was talking to our backend setup with Firebase, and location tracking with HyperTrack. The combination is high recommended!