Build Order Tracking with HyperTrack and Firebase

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:

  1. Driver logs in to the app, using Firebase Authentication.
  2. On successful authentication, app gets-or-creates a user object on HyperTrack.
  3. The app fetches new dispatches for the current date, from the Firebase Realtime Database.
  4. For a new dispatch, the app starts tracking with HyperTrack, and generates a web tracking URL
  5. The web tracking URL is saved in the Firebase Realtime Database, and sent via SMS to the customers using the Twilio SMS API.
  6. 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.

  1. ValueEventListener
  2. ChildEventListener
  3. 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

  1. Use setValue() to set the data at a given location to the given value. If already exists, overwrites data including any child nodes.
  2. 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

  1. Plan ahead of time how data is going to be stored for easy retrieval.
  2. Avoid nesting of nodes and try to flatten your nodes.
  3. Duplication (De-normalization) of data is encouraged, read more.
  4. 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!