DataStore Implementation in XML-Based Android Apps

While Jetpack Compose gains popularity, many Android applications still rely on XML-based layouts. Migrating existing XML-based projects to newer technologies like Compose can be a long process. If your application persists in XML, adopting modern data persistence solutions like DataStore can greatly enhance its architecture. This article will guide you through implementing DataStore in your XML-based Android application, ensuring you can leverage the benefits of this modern data persistence library.

What is DataStore?

DataStore is a data storage solution from the Android Jetpack libraries intended to replace SharedPreferences. DataStore offers several advantages, including:

  • Asynchronous API: Built on Kotlin coroutines and Flows, which avoids blocking the main thread.
  • Type Safety: Provides a typesafe way to store data, eliminating runtime errors related to incorrect data types.
  • Data Consistency: Ensures data consistency by handling transactions correctly and reliably.
  • Migration: Supports migrations from SharedPreferences to DataStore seamlessly.

Why Use DataStore in XML-Based Android Apps?

  • Improved Performance: Asynchronous operations reduce the likelihood of ANRs.
  • Enhanced Data Integrity: Type safety and transactional APIs ensure data integrity.
  • Modern Approach: Brings modern Android development practices to legacy XML projects.
  • Easier Testing: Integrates well with dependency injection frameworks, facilitating easier testing.

How to Implement DataStore in XML-Based Android Apps

To integrate DataStore in your XML-based Android application, follow these steps:

Step 1: Add Dependencies

First, add the necessary dependencies to your build.gradle file:


dependencies {
    implementation("androidx.datastore:datastore-preferences:1.0.0") // Replace with latest version if necessary
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:1.6.0")
    implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:1.6.0")
}

Sync the project to apply the changes.

Step 2: Create a DataStore Instance

Create an instance of DataStore in your application class or a singleton. Here’s how to do it using PreferenceDataStoreFactory:


import android.content.Context
import androidx.datastore.core.DataStore
import androidx.datastore.preferences.core.Preferences
import androidx.datastore.preferences.preferencesDataStore

private const val DATA_STORE_NAME = "my_app_preferences"

val Context.dataStore: DataStore by preferencesDataStore(
    name = DATA_STORE_NAME
)

In your Application class (or where appropriate), use this:


import android.app.Application
import android.content.Context

class MyApplication : Application() {

    companion object {
        private lateinit var instance: MyApplication

        fun getAppContext(): Context {
            return instance.applicationContext
        }
    }

    override fun onCreate() {
        super.onCreate()
        instance = this
    }
}

Don’t forget to declare the Application in your `AndroidManifest.xml`:



    ...

Step 3: Define Keys for Data

To interact with DataStore, you need to define keys for your data. DataStore uses these keys to read and write values:


import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey

object PreferenceKeys {
    val EXAMPLE_BOOLEAN = booleanPreferencesKey("example_boolean")
    val EXAMPLE_INTEGER = intPreferencesKey("example_integer")
    val EXAMPLE_STRING = stringPreferencesKey("example_string")
}

Step 4: Read Data from DataStore

Reading data from DataStore involves using Flows. Here’s how to read a boolean preference:


import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.core.emptyPreferences
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.map
import java.io.IOException

class DataStoreManager(private val context: Context) {

    val exampleBooleanFlow: Flow = context.dataStore.data
        .catch { exception ->
            if (exception is IOException) {
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }
        .map { preferences ->
            preferences[PreferenceKeys.EXAMPLE_BOOLEAN] ?: false
        }


    suspend fun saveExampleBoolean(value: Boolean) {
        context.dataStore.edit { preferences ->
            preferences[PreferenceKeys.EXAMPLE_BOOLEAN] = value
        }
    }


    val exampleIntegerFlow: Flow = context.dataStore.data
        .catch { exception ->
            if (exception is IOException) {
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }
        .map { preferences ->
            preferences[PreferenceKeys.EXAMPLE_INTEGER] ?: 0
        }


    suspend fun saveExampleInteger(value: Int) {
        context.dataStore.edit { preferences ->
            preferences[PreferenceKeys.EXAMPLE_INTEGER] = value
        }
    }


    val exampleStringFlow: Flow = context.dataStore.data
        .catch { exception ->
            if (exception is IOException) {
                emit(emptyPreferences())
            } else {
                throw exception
            }
        }
        .map { preferences ->
            preferences[PreferenceKeys.EXAMPLE_STRING] ?: ""
        }


    suspend fun saveExampleString(value: String) {
        context.dataStore.edit { preferences ->
            preferences[PreferenceKeys.EXAMPLE_STRING] = value
        }
    }


}

You’ll need an instance of `CoroutineScope` (such as `lifecycleScope` or `viewModelScope`) to collect from this flow in your Activity/Fragment. Here’s how you’d use this DataStoreManager from an Activity (adapted for XML):


import android.os.Bundle
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.launch
import android.widget.Button
import androidx.lifecycle.lifecycleScope

class MainActivity : AppCompatActivity() {

    private lateinit var dataStoreManager: DataStoreManager
    private lateinit var textView: TextView
    private lateinit var button: Button


    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_main)

        dataStoreManager = DataStoreManager(this)
        textView = findViewById(R.id.dataStoreTextView) // Make sure you have a TextView in your XML
        button = findViewById(R.id.dataStoreButton) // And a button

        lifecycleScope.launch {
            dataStoreManager.exampleBooleanFlow.collectLatest { exampleBoolean ->
                runOnUiThread {
                    textView.text = "Boolean Value: $exampleBoolean" // Update the text view
                }
            }
        }


        button.setOnClickListener {
            lifecycleScope.launch {
                dataStoreManager.saveExampleBoolean(true)
            }
        }
    }
}

Make sure you add this TextView in your layout `activity_main.xml`:




    

Step 5: Write Data to DataStore

Writing data to DataStore uses the edit function. Here’s how to save a boolean value:



  //This part is now located inside DataStoreManager class at Step 4
// suspend fun saveExampleBoolean(value: Boolean) {
//       context.dataStore.edit { preferences ->
//             preferences[PreferenceKeys.EXAMPLE_BOOLEAN] = value
//         }
//   }

Step 6: Observe Changes and Update UI

The example demonstrates observing the exampleBooleanFlow using collectLatest. Any changes to the value trigger UI updates within the Activity.

Additional Considerations

  • Error Handling: The code demonstrates error handling for IOException while reading data. Expand on this to handle other potential exceptions gracefully.
  • Dependency Injection: Use dependency injection frameworks like Dagger or Hilt to manage DataStore instances effectively, making testing easier.
  • Testing: Mock DataStore instances in your tests to ensure the persistence layer is functioning correctly.

Conclusion

Implementing DataStore in XML-based Android apps enables developers to leverage modern, efficient data storage solutions while maintaining their existing codebase. By integrating DataStore, you enhance the reliability, performance, and maintainability of your apps. Following the steps outlined in this guide will provide you with a robust way to manage persistent data in your Android applications, whether transitioning to new technologies like Compose or improving the current architecture.