RepoLib Rx - Ready-to-use Data Layer For Your Android App
The situation has been improving for some time thanks to the different approach of Google, which started suggesting a certain architecture for Android apps. Google is now suggesting to use the MVVM architecture. However, the problem still exists.
Mobile applications still have very poorly-defined architectures or do not have them at all. Moreover, the problem is exacerbated by the fact that there are very few frameworks or libraries that would provide a specific architecture for specific solutions. This makes implementation of each pattern much more time-consuming - you need to write a lot of boilerplate code to build a pattern/architecture structure. This makes development unnecessarily long.
One of such problematic areas in mobile apps is the data layer responsible for retrieving and storing data in local or remote storage. A large number of repetitive operations regarding data synchronization and the lack of a specific architecture will significantly prolong development and make code more error prone. In order to resolve these problems, we decided to develop a unified solution in the form of a ready-to-use library designed for handling data synchronization from different sources - RepoLib Rx.
Library overview
RepoLib Rx is an Kotlin library that provides data synchronization logic between two different data sources according to a selected synchronization strategy. The library is designed with the Repository pattern, which is designed to describe data layer structures. A simplified scheme of the Repository pattern is presented below.
RepoLib Rx is designed to allow applications to perform basic data operations like Create Read/Fetch, Update, Delete (CRUD). This library is responsible for managing requests for data sent from the business layer components to the appropriate data source. The appropriate data source is selected based on the request synchronization strategy provided by the strategy factory implemented by the user. The scheme shown below illustrates the architecture of the library, including the aforementioned strategy.
The library uses the Rx framework for managing data flows. The Rx pattern is utilized using RxJava/RxKotlin. Its main task is to construct reactive data streams responsible for sending requests to data sources and to take data from them.
The library is generic - this means that it doesn’t enforce any specific data models or specific implementations of the data sources. This means that you can use your favourite libraries for managing data sources like Retrofit, Room, Realm or anything else. All you need to do is to make sure that your storage controller class implements a DataSource interface.
As it was mentioned - the main task of the library is to synchronize requests and data between two data sources. Executing a given request on the source results in the transfer of data resulting from the request to the output data stream. The output data stream can be permanently subscribed to by the higher layers because it designed to only publish data updates. Any errors/exceptions will be published on input requests streams such us Create, Update or Delete. These streams are represented by RxJava 2 Completables. The output data stream is represented by an RxJava 2 Flowable.
Usage
The main goal when we were creating this library was to reduce the time needed to implement the data layer in mobile applications. Until now, in order to implement a data layer, it was necessary to write classes responsible for managing storage (data sources), data model mappers, classes that handle queries, etc. You also needed to write the most complex and error prone element - the synchronization logic controller responsible for passing data between sources. Using RepoLib, all logic is defined once, providing a unified architecture for each data model. All you need to do is implement the sources and models.
Download
RepoLib Rx is an open source library published under the Apache 2.0 license. Its source code is published in a GitHub repository. The library is also available in a Bintray/Maven repository as a compiled artifact. This means that the lib can be easily downloaded using the Gradle build system directly from the Maven repo. This is the recommended way of downloading and attaching the library as a project dependency.
To use the library in your project, add the Netguru Maven URLs to the repositories block:
repositories {
maven { url 'https://dl.bintray.com/netguru/maven/' }
}
Then add following the dependencies to the app’s build.gradle module:
dependencies {
implementation 'com.netguru.repolibrx:repolibrx:0.5'
}
Implementation
In order to be able to use the library, apart from downloading it, it is necessary to implement several elements. The full process of initialization consists of the following steps:
- Create a data model entity.
- Implement a Request Strategy Factory interface.
- Implement two data sources (DataSource interface):
- remote DataSource,
- local DataSource.
- Initialize the library.
An example implementation may look like:
1. Create a data model entity.
data class DemoDataEntity(val id: Long, val value: String)
2. Implement a Request Strategy Factory interface.
class DemoAppRequestStrategyFactoryFactory : RequestsStrategyFactory {
override fun select(request: Request): Strategy = when (request) {
is Request.Create -> RequestStrategy.OnlyRemote
is Request.Update -> RequestStrategy.OnlyRemote
is Request.Delete -> RequestStrategy.OnlyRemote
is Request.Fetch -> RequestStrategy.LocalAfterFullUpdateOrFailureOfRemote
}
}
You can also skip this point and use DefaultRequestStrategyFactory.
3. Implement two DataSource interfaces:
a. remote DataSource implementation based on Retrofit
class RetrofitDataSource(private val api: API) : DataSource {
override fun create(entity: DemoDataEntity): Observable = api.create(entity)
override fun update(entity: DemoDataEntity): Observable = api.update(entity)
override fun delete(query: Query)
: Observable {
return if (query is QueryWithParams) {
api.delete(id = query.param("id")).toObservable()
} else {
Observable.error(UnsupportedOperationException("Unsupported query: $query"))
}
}
override fun fetch(query: Query): Observable = api.get()
.flatMap { Observable.fromIterable(it) }
}
b. local DataSource
The local data source can be implemented manually in a similar as in the Retrofit example above.
4. Initialize the library
val repolib = createRepo {
localDataSource = localDemoDataSource
remoteDataSource = remoteDemoDataSource
requestsStrategyFactory = demoAppRequestStrategyFactory
}
You can also skip assignment of requestsStrategyFactory to use the default factory. You can also initialize the library using a constructor instead of using createRepo function
Now you can use your brand new instance of RepoLibRx in your project. To execute requests on your data sources, just subscribe to one of this functions:
-
fun create(T):Completable
creates a passed entity model of type T
-
fun update(T):Completable
updates a passed entity model of type T
-
fun delete(Query):Completable
delete an entity that matches the passed Query object
-
fun fetch(Query):Completable
fetch all objects that match the passed Query objects
To receive data updates subscribe to:
fun outputDataStream(): Flowable<T>
Please remember that data emission from the Flowable returned by these functions depends of your implementation of the DataSource interface. If you do not return data on some requests, you will not get data updates in this stream.
DataSource implementations
The most complex stage in whole library initialization process is the implementation of the data sources - both local and remote. This tasks requires you to adjust API of the storage libraries to the requirements provided by the DataSource interface and the Rx API. Moreover, it is necessary to implement mappers that will be responsible for translating generic Query objects into tool-specific queries (e.g RealmQuery for Realm or SQL String query for Room). In addition, some tools require you to use their specific data models, e.g. Realm requires that the data model extends a RealmObject abstract class to be stored inside its database. This requires you to implement additional functions that will transform data models from business specific models to RealmObjects and vice versa. Moreover, adding the logic responsible for the handling of individual operations makes such classes very complex and requires a lot of boilerplate.
In order to reduce the need to write such complex classes, we have prepared two adapters for the two most popular storage libraries - Realm and Room. The adapters contain an implementation of a DataSource interface that wraps the storage API to match interface requirements. Using such adapters significantly reduces the need to write entire DataSources. This means that it reduces the time need for development. Using the adapters, you just need to write query and data mappers to let them understand your data models and queries. A detailed description of usage with examples is available in the README files:
Both adapters are open source, published under the Apache 2.0 license. Both are also available in a Bintray repository. You can download them by adding the following line to the dependency section in your Gradle file:
For the Room Adapter:
implementation 'com.netguru.repolibrx:roomadapter:0.5'
For the Realm Adapter:
implementation 'com.netguru.repolibrx:realmadapter:0.5'
Please also remember to add the related storage tools - Room or Realm - to your configuration and initialize them according to the relevant documentation. Both steps are necessary to use the aforementioned adapter because the adapters are just wrappers for these tools to match the requirements provided by the DataSource interface of RepoLib Rx.
More info
You can find more details in the Github repository and in the Readme files of each adapter - Realm adapter and Room adapter. The repository also contains a sample app that is an example of using the library in a working application. The application is really simple to show the users how to integrate the RepoLib Rx with the Dependency Injection pattern based on Dagger 2 and a sample MVVM architecture based on Android Architecture Components. The app allows you to create simple notes and store them in a local or remote database. The remote database is implemented using Retrofit and OkHttp. Please be aware that the web server is mocked in the application using the OkHttp request interceptor. The app also implements adapters. By default, the Realm adapter is used, but a full implementation of the Room adapter is also included.
A more detailed description of individual components of the library, adapters, and the sample app can be found in the form of KDocs in the source code. If you find any bugs, don’t hesitate to raise a ticket on GitHub or create a PR with the fix and mention us. We hope that our library will help you speed up your development and allow you to save some time.