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)
}

Create a Shared Preference API that Implements KeyValueStore Interface

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()
    }
}

Make an Abstraction for a ReactiveStore

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.