Single Data Source Reactive Android Application
An Android application uses data that come from various data source. Call it Firebase, REST API, SQLite database, Shared preferences.
It's nice if we can maintain it as a single data source from many data sources and made it reactive. Why reactive? Because if there are changes in the data and screen opening it is showing it, it can automatically refresh the data by showing the latest version. It's easier than maintaining another mechanism to update the UI manually.
There is no standard about how to made this reactive data store. One of my favorites is using ReactiveX
Java or Kotlin version.
In this post, I will show you how to make a simple reactive key-value data store using Shared Preferences.
Make a Key-Value Abstraction
This is important if we want to implement another key-value based storage using another method.
interface KeyValueStore {
fun putBoolean(key: String, value: Boolean)
fun getBoolean(key: String, defaultValue: Boolean): Boolean
fun putInt(key: String, value: Int)
fun getInt(key: String, defaultValue: Int): Int
fun putString(key: String, value: String)
fun getString(key: String, defaultValue: String): String
fun putFloat(key: String, value: Float)
fun getFloat(key: String, defaultValue: Float): Float
fun putLong(key: String, value: Long)
fun getLong(key: String, defaultValue: Long): Long
fun putObject(key: String, `object`: Any)
fun <T> getObject(key: String, defaultValue: Any, tClass: Class<T>): T
fun putListString(key: String, value: List<String>)
fun getListString(key: String, defValue: List<String>): List<String>
fun putListObject(key: String, value: List<Any>)
fun <T> getListObject(key: String, defValue: List<T>): List<T>
fun removeKey(key: String)
}
KeyValueStore
Interface
Create a Shared Preference API that Implements This is an example how a simple shared preferences API that implements the KeyValueStore
interface.
Here, I use GSON
to made it easier to store a custom object to shared preferences by serializing it into a JSON-based string.
import android.content.Context
import android.content.SharedPreferences
import android.preference.PreferenceManager
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import id.co.spots.app.data.KeyValueStore
import org.json.JSONException
class SharedPreferenceApi(context: Context,
private val gson: Gson) : KeyValueStore {
private val sharedPreferences: SharedPreferences = PreferenceManager.getDefaultSharedPreferences(context)
private val editor: SharedPreferences.Editor = sharedPreferences.edit()
override fun putBoolean(key: String, value: Boolean) {
editor
.putBoolean(key, value)
.apply()
}
override fun getBoolean(key: String, defaultValue: Boolean): Boolean {
return sharedPreferences.getBoolean(key, defaultValue)
}
override fun putInt(key: String, value: Int) {
editor
.putInt(key, value)
.apply()
}
override fun getInt(key: String, defaultValue: Int): Int {
return sharedPreferences.getInt(key, defaultValue)
}
override fun putString(key: String, value: String) {
editor
.putString(key, value)
.apply()
}
override fun getString(key: String, defaultValue: String): String {
return sharedPreferences.getString(key, defaultValue)
}
override fun putFloat(key: String, value: Float) {
editor.putFloat(key, value).apply()
}
override fun getFloat(key: String, defaultValue: Float): Float {
return sharedPreferences.getFloat(key, defaultValue)
}
override fun putLong(key: String, value: Long) {
editor.putLong(key, value).apply()
}
override fun getLong(key: String, defaultValue: Long): Long {
return sharedPreferences.getLong(key, defaultValue)
}
override fun putObject(key: String, `object`: Any) {
val objectString = convertObjectToString(`object`)
putString(key, objectString)
}
override fun <T> getObject(key: String, defaultValue: Any, tClass: Class<T>): T {
val defValueString = convertObjectToString(defaultValue)
val jsonValue = sharedPreferences.getString(key, defValueString)
return convertJsonStringToObject(jsonValue, tClass)
}
private fun <T> convertJsonStringToObject(jsonValue: String, tClass: Class<T>): T {
return gson.fromJson(jsonValue, tClass)
}
override fun putListString(key: String, value: List<String>) {
putListObject(key, value)
}
@Throws(JSONException::class)
override fun getListString(key: String, defValue: List<String>): List<String> {
val defValueString = convertObjectToString(defValue)
val jsonValue = sharedPreferences.getString(key, defValueString)
return convertJsonStringToListString(jsonValue)
}
private fun convertJsonStringToListString(jsonValue: String): List<String> {
val type = object : TypeToken<List<String>>() {
}.type
return gson.fromJson<List<String>>(jsonValue, type)
}
override fun putListObject(key: String, value: List<Any>) {
val jsonValue = convertObjectToString(value)
editor.putString(key, jsonValue)
editor.apply()
}
@Throws(JSONException::class)
override fun <T> getListObject(key: String, defValue: List<T>): List<T> {
val defValueString = convertObjectToString(defValue)
val jsonValue = sharedPreferences.getString(key, defValueString)
return convertJsonStringToListObject(jsonValue)
}
private fun convertObjectToString(value: Any): String {
return gson.toJson(value)
}
@Throws(JSONException::class)
private fun <T> convertJsonStringToListObject(jsonValue: String): List<T> {
val typeToken = object : TypeToken<List<T>>() {
}.type
return gson.fromJson<List<T>>(jsonValue, typeToken)
}
override fun removeKey(key: String) {
editor.remove(key).apply()
}
}
ReactiveStore
Make an Abstraction for a Here is an example of an abstraction of reactive store. We can make a specific store for each data we want to use in our application by implementing below interface.
I think the abstract methods are already clear.
import io.reactivex.Completable
import io.reactivex.Observable
interface ReactiveStore<V> {
fun get(): Observable<V>
fun save(value: V): Completable
}
Note: If you want to know more about the Observable
concept, please read this documentation of the Rx in this link.
Simple User Reactive Store Example
For example, we want to store an user data that includes name, email and phone number.
First, we create a simple data class of the user.
data class User(val name: String, val email: String, val phone: String)
Second, we create a class named UserReactiveStore
that implements ReactiveStore
interface.
class UserReactiveStore(private val keyValueStore: KeyValueStore) : ReactiveStore<User> {
override fun get(): Observable<User> {
// To be written
}
override fun save(value: User): Completable {
// To be written
}
}
Third, we use PublishSubject
implementation on Rx
to handle the reactive part.
class UserReactiveStore(private val keyValueStore: KeyValueStore) : ReactiveStore<String, User> {
private val userSubject: Subject<User> = PublishSubject.create<User>().toSerialized()
private val userSavedKey = "SAVED_USER"
override fun get(): Observable<User> {
// The observable of user data is initialized with data from key value storage
return userSubject.startWith(
keyValueStore.getObject(
userSavedKey,
User(", ", "), // This is default value if it's not saved yet on shared preferences
User::class.java
)
)
}
override fun save(value: User) {
return Completable.create { emitter ->
// Save to shared preference
keyValueStore.putObject(userSavedKey, value)
// Notify current subject and the observable returned from `get` method
// that it emits a new user value
userSubject.onNext(value)
// finish the completable
emitter.onComplete()
}
}
}
See above code blocks. After the real save
implementation, it notify the subject or the observable based on it so every code that listen to this observable will also be notified when there is change in saved data.
Example implementation
val sharedPreferenceApi = SharedPreferenceApi(CONTEXT, GSON)
val userStore = UserReactiveStore(sharedPreferenceApi)
// Listen to user data changes
userStore
.get()
.subscribe(
{ user -> /*There are changes in user data*/ },
{ error -> /*Handle error*/ }
)
// Save changes
userStore
.save()
.subscribe(
{ /*Save succeeded*/},
{ error -> /*There is error when saving*/ }
)
That's it!
With Rx
, you also get a bonus. It's error handler in which case can handle unknown error for example when something bad happened on saved user data.