In today’s mobile landscape, providing offline capability in Android apps has become essential for delivering a seamless user experience. However, implementing offline functionality comes with its own set of challenges, including data synchronization and storage management. This is where Supabase, a powerful backend platform, and Room, a local database solution for Android apps, come into play.
Problem Statement and Importance of Offline Persistence
While the backend like Supabase offers powerful features, it lacks built-in offline persistence for now. This presents a challenge for developers who want to provide a seamless user experience in Android apps, especially when network connectivity is unreliable. Without offline support, users encounter issues such as:
- Limited Functionality: App features that rely on data access become unavailable offline.
- Frustrating User Experience: Users are forced to wait for a network connection to complete tasks or access information.
- Data Loss Potential: If users make changes while offline, these changes might not be saved or synchronized properly upon reconnection.
There were existing solutions such as PowerSync, which offers a similar offline first solution with synchronization functionalities but at a cost. These paid solutions often provide comprehensive features but may not always align perfectly with specific project requirements or budget constraints. The motivation behind creating this new library was to provide a more accessible, tailor-made solution that aligns closely with the specific needs of developers using Supabase and Android. By developing a custom solution, it was possible to ensure that no unnecessary overhead or irrelevant functionalities were included, focusing solely on the necessary features that provide the most value for mobile applications requiring offline capabilities.
Introducing the Solution: Supabase Offline Support Library
Enter the Supabase Offline Support Library, a solution designed to simplify the implementation of offline capability in Android apps. This library leverages the strengths of Supabase as a flexible and scalable backend platform and Room as a robust local database framework to provide seamless offline functionality for native Android apps.
Understanding the Architecture
The architecture of the Supabase Offline Support Library is designed to be flexible, scalable, and easy to integrate into any Android app. Key components of the library include:
1. BaseDataSyncer
- Purpose: This abstract class serves as the foundation and contains the abstract method
syncToSupabase
which must be implemented by any subclass to provide a data synchronization strategy/algorithm that must occur between the local database and remote Supabase database keeping data conflict in mind. - Usage: Extend this class to customize the synchronization logic as per the application’s specific data requirements.
2. BaseRemoteEntity
- Purpose: Acts as the base class for any data transfer object (DTO) or class that represents an entity corresponding to a remote table in Supabase. This class ensures that all remote entities follow a consistent structure and remote table consists necessary columns required to work with SyncManager.
- Usage: Include the properties “id”, and “timestamp” as columns in a remote table then extend this class when creating classes that map directly to tables in the Supabase database, ensuring they can be easily managed and synchronized.
For example, if we take this task table in Supabase whose timestamp column represents lastUpdatedTimestamp and id represents id in DTO
Note: @SerialName(“timestamp”) is a must on the property lastUpdatedTimestamp and also the column named “timestamp” of type bigint / int8 , “id” of bigint/ int8 column as a primary key is required in Supabase table to work with SyncManager
3. BaseSyncableEntity
- Purpose: This abstract class is intended for entities stored in the local Room database, which need to synchronize with Supabase and provides properties for all entities that are required for synchronization.
- Usage: When a user is offline and performs CRUD operation on the table then OfflineFieldOpType (int field) determines which operation was during the offline state which is used when the device gets online and synchronization is performed, it should have the following values: (0 for no changes, 1 for insertion, 2 for update, 3 for delete).
4. GenericDao
- Purpose: Provides a set of generic CRUD (Create, Read, Update, Delete) methods that any DAO (Data Access Object) of a Room entity must implement to function with the SyncManager for data synchronization.
This class uses @RawQuery annotation to use dynamic table names at runtime which is not possible with @Query - Usage: Extend this interface in your DAO implementations to provide essential data manipulation methods that are used by the library to perform synchronization tasks.
5. SyncManager
- Purpose: Implements the specific logic for synchronizing data with Supabase by extending
BaseDataSyncer
. It provides a concrete implementation ofsyncToSupabase
, utilizing theSupabaseApiService
to perform the actual data transmission to and from Supabase.getLastSyncedTimestamp(tableName: String)
provides the timestamp when a particular table was last synced with a remote table in the case of the first sync it has a default value of 0. - Usage: Provide a ready-to-execute concrete class to manage data synchronization processes. It handles the logic for when and how to sync data based on network availability and data state.
6. Retrofit Client
- Purpose: A singleton class responsible for setting up and configuring the Retrofit client with the necessary base URL and API key of the Supabase client. It includes an interceptor to add the API key to all requests and uses converter methods to handle CRUD operations with the Supabase REST API.
- Usage: A point for network communication setup, ensuring that all network calls to Supabase are authenticated and correctly formatted.
7. SupabaseApiService
- Purpose: Provides asynchronous methods for performing CRUD operations on Supabase. It is utilized by
SyncManager
to execute insert and update operations, handle request bodies, and parse the responses to retrieve IDs or error messages. - Usage: This service abstracts the REST API calls and is integral to the synchronization process, ensuring data consistency between local and remote states.
8. NetworkHelper
- Purpose: Offers utility methods to check internet connectivity. It includes a method for instantaneous network checks (
isNetworkAvailable()
) and a LiveData provider (getNetworkLiveData()
) that updates observers with the current network state. - Usage: Essential for determining when to initiate or halt synchronization processes based on network availability.
9. Converters.kt
- Purpose: Contains annotation classes (
eq
,lt
,gt
) used to define query parameters for REST API calls. This functionality is part of a custom converter factory (QueryParamConverter
) used in the Retrofit client to modify URLs dynamically based on query requirements. - Usage: Enables dynamic insertion of query conditions in REST API URLs, facilitating insert, update, and delete queries via simple annotations.
10. Extension.kt
- Purpose: Contains extension functions such as
decodeSingle
/decodeList
to parse JSON responses from Supabase REST API calls. It also includes methods likeprepareRequestBody
andprepareRequestBodyWithoutId
to correctly format and encode request bodies for API calls. - Usage: Streamlines data handling by providing utility functions that aid in preparing and processing data exchanged with the Supabase API.
Triggering Synchronization: Mastering Offline-Online Consistency
A crucial aspect of enabling offline functionality is efficiently determining when to synchronize data between the local and remote databases. In our library, the SyncManager
plays a pivotal role in this process. We have provided network-based triggers but you can use your triggering methods like on particular user action, scheduled interval, on each app launch.
Network-Based Synchronization with SyncManager:
Our SyncManager
includes a method, observeNetwork()
, which listens for changes in network availability. Here’s how it works:
observeNetwork()
Method: This method leverages theNetworkHelper
class to observe the device’s network state. It returns LiveData<Boolean>, when an Internet is detected after being offline, it returns true else false.- Implementation Details: Upon detecting network availability,
SyncManager
initiates the synchronization algorithm, ensuring that all local changes are pushed to the remote server and any updates from the remote server are pulled into the local database.
Example Code:
The Synchronization algorithm:
Getting Started
To get started with the Supabase Offline Support Library, follow these simple steps:
- Set up a Supabase project and obtain your API key and base URL, along with the Room dependency.
- Add the library to your Android project’s dependencies using Gradle.
- Configure the library by initializing
SyncManager
and setting up your database entities.
Each local entity that is participating in the synchronization process needs to extend BaseSyncableEntity and their corresponding DAOs need to extend GenericDAO<T>, Each Remote DTO/ Entity representing Supabase Table and wants to sync with the corresponding local table needs to extend BaseRemoteEntity to work with SyncManager
Integration Example and Explanation
Let’s dive into a hands-on tutorial to see how easy it is to implement offline capability in an Android app using the Supabase Offline Support Library with Room. In this tutorial, we’ll integrate our library into a simple task management app that allows users to add, update, and delete tasks both online and offline.
Note: You can write your own synchronization algorithm by extending our BaseDataSyncer class. We have provided a ready to use concrete class named SyncManager with our synchronization algorithm which is explained later.
Let’s start with integration, We will be having 3 tables namely Task, Category, Priority
Structure for local task table and category that are participating in the synchronization process:
Similarly for Remote entities/ DTOs
After configuring the entities and DAOs, initialize the SyncManager in your class as follows:
SyncManager has 3 utility methods:
– fun isNetworkAvailable(): Returns Boolean representing the network availability at that moment
– fun observeNetwork(): Returns LiveData<Boolean> which can be observed where Boolean value reflect the network availability
– fun getLastSyncedTimeStamp(tableName: String): Returns Long value which contains the milliseconds when the local table was last synced with its corresponding remote table.
Observing the network and triggering the synchronization process each the time the network changes i.e. internet goes on/off
Here is the snippet to handle insert/update/delete on the task table, here we are using concept of soft delete i.e. when user deletes the item in UI, the is_delete column of Task table is set true instead of deleting the row immediately when offline
Challenges Faced
One of the significant hurdles faced during the development of the library involved dealing with the Room persistence library’s constraints, specifically its inability to dynamically handle table names in queries. Room, designed for compile-time safety and ease of use, requires that all SQL queries, including table names, be known at compile-time. This design ensures that queries are verified for correctness during compilation, thus preventing runtime errors and improving stability. However, this feature also restricts the library’s flexibility in applications that require dynamic table handling, such as a generic synchronization library.
To address this limitation, I utilized the @RawQuery
annotation that Room provides, which allows executing dynamic SQL queries. By leveraging @RawQuery
, it was possible to craft queries where table names and other parameters are specified at runtime, offering the necessary flexibility for SyncManager to interact with different entities generically.
Another considerable challenge involved interfacing with the Supabase Kotlin client SDK. The SDK’s methods, such as insert()
, update()
, and upsert()
, require specifying a type parameter at compile time. However, in a library designed to handle data synchronization generically across various tables, the specific type of object being operated on is only known at runtime. This posed a problem when attempting to use these methods in a base class intended to serve a wide array of data entities.
Initially, making the syncToSupabase()
method of the SyncManager class reified seemed like a viable solution, as it would allow type-safe usage of these functions. However, Kotlin’s reified types do not support inheritance or use within interfaces and inner functions. This restriction would have severely limited the method’s applicability, preventing it from being defined in a base interface like BaseDataSyncer
, which was designed to be implemented by various data synchronizing classes.
Due to these limitations with the Supabase Kotlin client SDK, I opted to use the Supabase REST API directly. This approach bypassed the type constraints and allowed for more dynamic handling of synchronization tasks. It also involved manually decoding the JSON responses from the API, as the specific model classes to be used could only be determined at runtime. This manual handling of JSON ensures that our synchronization logic remains as flexible and adaptable as necessary, albeit at the cost of some additional complexity in the data processing logic.
Conclusion
By leveraging the power of Supabase and Room, developers can supercharge their Android apps with offline capability, providing users with a seamless experience even in challenging network conditions. The Supabase Offline Support Library simplifies the implementation process and offers a robust solution for managing data synchronization. Try it out in your next Android project and experience the benefits firsthand!
GitHub Repository
For those interested in exploring the code further or contributing to the project, the source code for both our sample application and the underlying library is available on GitHub.
https://github.com/novumlogic/Android-Offline-First-Supabase-Library