Offline mode in the Licious Rider App

Shaktipada Mohanty
Licious Technology
Published in
8 min readSep 28, 2023

--

Recently, Licious launched its in-house delivery management dashboard built using ReactJS and an Android application to manage customer order deliveries effectively. Some of the critical goals of this release were to have control over the data and events, keep track of an order SLA (promised delivery time), product quality adherence at the time of delivery and effectively manage the in-house delivery fleet.

The delivery utility app viz. Licious Rider Application is an Android-based application with a backend comprising Sprint Boot (Java), MySQL and Cassandra. The dispatch centre dashboard is a web-based application which is built using ReactJS.

Earlier, Licious relied on a third-party SAAS system for its last-mile operations. Hence, less control over feature development, rollouts and data. With the in-house app, Licious successfully eliminated those constraints. Listing some of the salient features of the Rider Application below.

  • Strict workflows and shipment state management
  • Product scanning at the time of pickup and delivery
  • Real-time riders’ live location pings
  • Riders’ attendance management
  • Minimise cash collection with in-app payment collection using UPI QR
  • Product quality adherence on delivery by capturing proof of delivery

A few features were not included for the initial release, primary among them? Operate offline. By doing so, riders would no longer require reliance on network availability to complete order deliveries. Hence easing rider anxiety.

How and when to enable the offline mode?

The first thing is to identify for which part of a trip the delivery executive aka “rider”, would have to perform the most critical operations. And while doing so, what is the probability of a decent network availability? After analysing historical data, it was found that it would be beneficial if the app was operable offline at the time of delivery instead of making the entire trip available offline. At the time of delivery, a typical rider operation will be to identify the shipment by scanning, collect payment using payment QR for pay-on-delivery orders, collect delivery proof and mark the order as delivered. This requires significant data exchanges hence, a decent network availability. Also, a delivery location has a higher chance of being a network dark zone, such as in the basements of a tall building or a society with high-rise apartments. So, we got our when.

Next is to identify how to initiate the offline mode. This can be determined based on the absence of network connectivity or a timeout from a liveliness API. In Android, mobile network strength can be identified using Connectivity Manager. Using Connectivity Manager Observers, we can know about the network status. So now we are aware of the network strength and availability.

override fun observeMobile(): Flow<IConnectivityObserver.Status> {
return callbackFlow {
val callback = object : ConnectivityManager.NetworkCallback() {
override fun onAvailable(network: Network) {
super.onAvailable(network)
isMobileConnected = true
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
connectivityManager.bindProcessToNetwork(network)
}
if (!isWifiConnected) {
launch { send(IConnectivityObserver.Status.Available) }
}
launch { send(IConnectivityObserver.Status.Available) }
}

override fun onLost(network: Network) {
super.onLost(network)
isMobileConnected = false
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
connectivityManager.bindProcessToNetwork(null)
}
if (!isWifiConnected) {
launch { send(IConnectivityObserver.Status.Lost) }
}
launch { send(IConnectivityObserver.Status.CellularDisconnected) }
}
}
requestNetworkState(requestMobile, callback)
awaitClose {
unregisterNetworkCallback(callback)
}
}.distinctUntilChanged()
}

To check for liveliness, the real-time rider geolocation API can be used. This API periodically pings the rider device’s current geo-location to the backend every 10–30 seconds. This operation happens in the app background. The average response time of this API is ~18 milliseconds. If this API times out after X seconds of wait and N number of retries have happened, it is safe to conclude that either the backend service is unavailable or there is network congestion. So, the app can employ this API to identify network congestion or backend liveliness.

As explained earlier, we considered the delivery point as an offline zone. For the app to operate offline, it needs preloaded data. This data will include orders’ shipments, payment, customer and delivery location details. When the rider starts towards the delivery location, the required data can be fetched and stored on the device. On arrival at the customer's doorsteps, if the offline conditions are met with the preloaded data the app can switch to an offline mode.

Storing data on the Android device using the Room database

The data in Android devices can be stored in the Room Database. Room is a part of Android Jetpack and is highly recommended instead of using SQLite APIs directly. The primary use case involves caching relevant data, enabling users to browse content even when users are offline or their devices cannot access the network.

Room database high-level architecture

The Room persistence library provides an abstraction layer over SQLite to allow fluent database access while harnessing the full power of SQLite. In particular, Room has the following benefits:

  • Compile-time verification of SQL queries.
  • Convenience annotations that minimise repetitive and error-prone boilerplate code.
  • Streamline database migration paths.

Primary Components

  • The database class holds the database and serves as the main access point for the underlying connection to the app’s persisted data.
  • Data entities that represent tables in your app’s database.
  • Data access objects (DAOs) offer methods that enable the app to perform data queries, updates, insertions, and deletions in the database.

Unidirectional Data Flow Across the Components

Repositories designate a data source as the single source of truth principle for the rest of the app. It communicates with server and local storage as per the use case and exposes data accordingly.

Blueprint of data flow in Android

Once the offline mode is active the app operations for that period get stored on the device Room database. Here, the operations are the API requests that were supposed to happen if the device was online. In some cases, where the rider has to capture the delivery proof as an image, the media gets stored locally on the device file system, with the file path in the Room database for reference. The use of the Room persistence library facilitates efficient data caching and access, ensuring a seamless offline experience for riders.

Data sync when the device is back online

Upon regaining the network, the app needs to synchronise the offline operation data with the server and allow the rider to proceed with the next set of actions. Here WorkManager plays a pivotal role in syncing data to the backend.

Background sync with WorkManager

WorkManager is part of Android Jetpack. It is an architecture component for background work that requires a combination of opportunistic and guaranteed execution. WorkManager is the recommended solution for persistent work, which remains scheduled through app restarts and system reboots. Since most background processing is best accomplished through persistent work, WorkManager is the primary recommended API for background processing.

OneTimeWorkRequest with worker

When a OneTimeWorkRequest is created, it specifies the worker class that will be responsible for performing the task. The OneTimeWorkRequest can also define input data that will be passed to the worker when it starts, as well as any constraints that must be met before the worker can execute.

Once a OneTimeWorkRequest is created, it can be enqueued to the WorkManager instance using the enqueue() method. When the OneTimeWorkRequest is enqueued, it is added to a queue of pending work requests. The WorkManager library takes care of scheduling the worker to run when its constraints are met and resources are available.

OneTimeWorkRequest is useful when you need to execute a task only once, and you don’t need to worry about scheduling or rescheduling it. The WorkManager library takes care of retrying failed work, ensuring that the work is executed even if the app is killed or the device is restarted, and respecting battery optimisation settings to minimise battery usage.

NetworkType Work Constraints

NetworkType.CONNECTED: This constraint indicates that the worker requires network connectivity to run. The worker will be delayed until the device has an active network connection.

Retry Policy and backoff Policy

When you call retry from the worker. Your work is then rescheduled according to a backoff delay and backoff policy.

  • Backoff delay: Specifies the minimum amount of time to wait before retrying your work after the first attempt. This value will be 10 seconds.
  • Backoff policy: Defines how the backoff delay should increase over time for subsequent retry attempts. WorkManager supports 2 backoff policies, LINEAR and EXPONENTIAL. The minimum backoff delay is set to 10 seconds. Since the policy is LINEAR the retry interval will increase by approximately 10 seconds with each new attempt. For instance, the first run finishing with Result.retry() will be attempted again after 10 seconds, followed by 20, 30, 40, and so on.

HLD of Work Manager

Work Manager Architecture

Consider the above diagram, which shows how WorkManager works and its architecture, follow the below steps:

  1. Create a Worker subclass that implements the doWork() method for your job.
  2. Then create WorkRequest that specifies the Worker and all arguments.
  3. The last step is to enqueue your job by passing the WorkRequest to the WorkManager instance.

The basic classes of WorkManager:
Worker: The work needs to be defined here.
WorkRequest: It defines a work, like which worker class is going to be executed.
WorkManager: It enqueues and manages the work request.
WorkInfo/work database: It contains information about the work.

Whenever a rider comes online, all the offline data needs to be synced with the server. Here, the data consists of media data, rider delivery actions and background location data. To make data transfer faster we can use a synchronous request strategy for rider delivery actions and media metadata. Location data and media files can be transferred in the background asynchronously. It is recommended to obtain the image upload URL first to ensure that the rider doesn’t get blocked while uploading and can proceed with other tasks. Media can be uploaded in the background. In case of multiple images, the background service will work on a first-in, first-out (FIFO) basis. The device will maintain a list in the local cache and the media files in the device file system.

Android offline initiation and background sync flow

Once the data is with the backend, the required operations need to be carried out such as saving the data into the database after proper validation, pushing media (if any) to S3 and notifying the upstream services regarding the order update in the form of callbacks.

Backend data flow

Conclusion

While going through the article, you might have noticed one thing. The article explains how to do offline delivery for a single order in a certain no-network location. What if there are multiple deliveries in the same location? Like a society. Can the above process support this action?

The answer is no. As of now, it is a single-order offline flow. After completing a delivery, the rider needs to move to an open area with network connectivity to synchronise existing offline data before proceeding with the next order delivery within the same community.

Stay tuned for our next article on how we are targeting this problem statement. Enhance offline features to serve orders from a specific point of interest or a cluster.

--

--