Harnessing Jetpack’s DataStore: Crafting Persistent Android Applications

Ken Ruiz Inoue
Deuk
Published in
9 min readFeb 22, 2024

Introduction

In today’s mobile applications, ensuring data persistence beyond the lifecycle of app components is a critical challenge. The Android operating system, with its intelligent management of activities and components, often terminates these to conserve resources. This poses a particular problem for developers aiming to maintain user configurations seamlessly across sessions. Enter the Android Jetpack's DataStore: a robust solution designed to transcend the limitations of local variables for data persistence.

This guide explores the utilization of Android DataStore, illustrating how it fits into a well-organized architecture that encompasses both a Repository and a ViewModel. We will craft a straightforward user interface featuring text and a toggle switch to adjust and represent the user’s text size preference dynamically.

So, without further delay, let’s dive in!

Step 1: Project Creation and Dependency Integration

Initiating a New Project

Begin your journey into Android DataStore by launching Android Studio and creating a new project. Select the “Empty Activity” template to ensure a clean starting point. Once your project is set up, you’ll have a blank canvas ready to integrate the powerful capabilities of DataStore.

Select Empty Activity

Configuring Dependencies

Your next step involves navigating to the build.gradle.kt file within your app module. This file is where you'll declare the dependencies necessary for your project. Add these two libraries inside the dependencies block.

implementation("androidx.datastore:datastore-preferences:1.0.0")
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0")

The datastore-preferences library provides the necessary interfaces for DataStore operations, offering an asynchronous, safe way to persist key-value pairs. Meanwhile, the lifecycle-viewmodel-compose library helps provide a ViewModel in a lifecycle-conscious way, facilitating the use of DataStore within a modern architectural approach.

Synchronizing Your Project

After carefully adding the above dependencies, it’s essential to synchronize your project with the Gradle files. This action ensures that Android Studio correctly downloads and integrates the specified libraries, setting the stage for a seamless development experience.

Step 2: Crafting the Data Store Repository

This step involves creating the DataStoreRepository.kt class, a pivotal component that will act as the intermediary between our application's UI and the stored preferences. Position this class in the same directory as your MainActivity.kt to maintain a simple project structure.

// Your Package

import android.content.Context
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.edit
import androidx.datastore.preferences.preferencesDataStore
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map

// Defines a repository class to interact with the DataStore
class DataStoreRepository(private val context: Context) {
// Extension property on Context to lazily create and initialize the preferences DataStore
// The 'preferencesDataStore' name "settings" will be used as the filename for the preferences
private val Context.dataStore by preferencesDataStore(name = "settings")

companion object {
// Defines a key for storing the text size preference in the DataStore
// This key is used to retrieve and save the preference value
val TEXT_SIZE_KEY = booleanPreferencesKey("is_text_size_small")
}

// Key Point: Accessing Persisted Value with a Key
val isTextSizeSmall: Flow<Boolean> = context.dataStore.data
.map { preferences ->
// Retrieves the current value of text size preference from the DataStore
// Returns false as a default value if the preference is not set
preferences[TEXT_SIZE_KEY] ?: false
}

// Coroutine function to update the text size preference in the DataStore
suspend fun setTextSize(isTextSizeSmall: Boolean) {
context.dataStore.edit { settings ->
// Applies the new value to the TEXT_SIZE_KEY in the DataStore
settings[TEXT_SIZE_KEY] = isTextSizeSmall
}
}
}

Key Point: Accessing Persisted Value with a Key

  • Key-Value Pair Management: The DataStore operates on a key-value pair system, where each unique key corresponds to a specific piece of data you wish to store. This system is crucial for efficiently organizing and retrieving your app’s ettings or preferences.
  • Declaration and Usage of Keys: In the DataStoreRepository class, keys are declared to represent different preferences or configurations. For instance, TEXT_SIZE_KEY is defined as a boolean preference key to store and access the text size setting. This approach ensures type safety and clear identification of the data being managed.
  • Dynamic Data Observability: By exposing the preference data as a Flow<Boolean>, the Data Store allows for real-time observation of changes. This means your app can react immediately to preference updates, ensuring a responsive and personalized user experience.

Step 3: MainViewModel Implementation

Now that we’ve established data persistence with the DataStoreRepository, let’s create the MainViewModel.kt file. Acting as a bridge between the repository and our UI, the ViewModel efficiently manages app data and user interactions.

// Your Package

import androidx.lifecycle.ViewModel
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch

// MainViewModel manages UI-related data in a lifecycle-conscious way and interacts with the DataStoreRepository
class MainViewModel(private val repository: DataStoreRepository) : ViewModel() {
// Key Point 1: Data Exposition via StateFlow
// _isTextSizeSmall is a private mutable state flow to handle the text size state internally within the ViewModel
private val _isTextSizeSmall = MutableStateFlow(false)
// Public version of isTextSizeSmall to expose as an immutable StateFlow. External classes observe this for UI updates.
val isTextSizeSmall: StateFlow<Boolean> = _isTextSizeSmall

// Key Point 2: ViewModel Initialization and Data Collection
init {
// On ViewModel initialization, start collecting the text size preference from the repository
viewModelScope.launch {
repository.isTextSizeSmall.collect { size ->
_isTextSizeSmall.value = size // Update the internal state flow with the latest preference value
}
}
}

// toggleTextSize updates the text size preference in the repository and the internal state flow
fun toggleTextSize() {
viewModelScope.launch {
_isTextSizeSmall.value = !_isTextSizeSmall.value // Toggle the current value
repository.setTextSize(_isTextSizeSmall.value) // Persist the new value in the DataStore
}
}
}

// Key Point 3: Custom ViewModel Factory Implementation
// MainViewModelFactory is used to instantiate MainViewModel with specific constructor parameters, like the repository
// This class is necessary when manually handling ViewModel creation, such as without using dependency injection frameworks like Hilt
class MainViewModelFactory(private val repository: DataStoreRepository) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(MainViewModel::class.java)) {
@Suppress("UNCHECKED_CAST")
return MainViewModel(repository) as T // Cast and return MainViewModel with the repository
}
throw IllegalArgumentException("Unknown ViewModel class") // Throw error if the ViewModel class doesn't match
}
}

Key Point 1: Data Exposition via StateFlow

We employ a two-tiered StateFlow structure to expose data from the ViewModel. The private _isTextSizeSmall holds the mutable state internally, ensuring encapsulation. The public isTextSizeSmall, an immutable StateFlow, allows the UI to observe and react to changes in text size preference.

Key Point 2: ViewModel Initialization and Data Collection

Upon initialization, the ViewModel immediately begins to collect the text size preference stored in DataStore through repository.isTextSizeSmall.collect. This ensures that the UI reflects the current user preferences right from the start.

Key Point 3: Custom ViewModel Factory Implementation

To instantiate MainViewModel with DataStoreRepository as a constructor parameter, we utilize MainViewModelFactory. This approach is essential in scenarios where dependency injection libraries like Hilt are not used, providing a straightforward way to manage dependencies manually.

Step 4: Data Store in Action

Finally, let’s stitch all the pieces together to see the DataStore-fueled app in action. Update MainActivity.kt as follows.

// Your Package

import android.os.Bundle
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.sp
import androidx.lifecycle.viewmodel.compose.viewModel

class MainActivity : ComponentActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContent {
// In a production app, dependency injection (e.g., Hilt, Koin) would initialize the repository
// Here, we manually create the repository and ViewModel for simplicity
val repository = DataStoreRepository(applicationContext)
val viewModelFactory = MainViewModelFactory(repository)
// Obtain an instance of MainViewModel using the factory
// Key Point 1: ViewModel Initialization in Compose Context
val mainViewModel: MainViewModel = viewModel(factory = viewModelFactory)
// Pass the ViewModel to our composable function to build the UI
MyApp(viewModel = mainViewModel)
}
}
}

// MyApp composable function sets up the UI structure using the MainViewModel
@Composable
fun MyApp(viewModel: MainViewModel) {
// Collects the current text size state from the ViewModel as a Compose state
// Key Point 2: Collecting State from ViewModel in Compose Context
val textSize by viewModel.isTextSizeSmall.collectAsState()
// Render the app content with the current text size and a toggle function
AppContent(textSize = textSize, onToggleTextSize = { viewModel.toggleTextSize() })
}

// AppContent composable displays the UI elements, including text and a switch
@Composable
fun AppContent(textSize: Boolean, onToggleTextSize: () -> Unit) {
Column(
modifier = Modifier.fillMaxSize(), // Fill the available space
horizontalAlignment = Alignment.CenterHorizontally, // Center content horizontally
verticalArrangement = Arrangement.Center // Center content vertically
) {
// Displays the text size status, adjusting the font size based on the current setting
Text(
text = "Text Size: ${if (textSize) "Large" else "Small"}",
style = MaterialTheme.typography.labelMedium.copy(fontSize = if (textSize) 24.sp else 16.sp)
)
// A switch to toggle the text size between large and small
Switch(checked = textSize, onCheckedChange = { onToggleTextSize() })
}
}

Key Point 1: ViewModel Initialization in Compose

We initialize the ViewModel in a Compose context using the viewModel() function, made possible by the lifecycle-viewmodel-compose dependency introduced earlier. This approach allows us to integrate the ViewModel seamlessly within our Compose UI.

Key Point 2: State Collection in Compose

Through the collectAsState() Compose function, we actively listen to and display the StateFlow from MainViewModel. This method enables real-time UI updates in response to data changes, ensuring that the app persistently reflects the current settings from DataStore.

Running the App

Once you launch the application, you’ll have the flexibility to adjust the text size setting through a user-friendly toggle. This change is not only immediate but also persistent, meaning that your preference will be retained across app restarts. The stored configuration remains intact until the application is explicitly uninstalled, showcasing the effectiveness of Android DataStore in maintaining user settings reliably.

Conclusion

That wraps up our guide on integrating Android DataStore for persistent data management! We’ve journeyed through setting up a project, creating a data repository, and seamlessly connecting it with the UI through a ViewModel. As we move away from SharedPreferences, embracing DataStore offers a robust, asynchronous solution that aligns with modern Android architecture practices.

Interested in deepening your Android development skills? Explore my collection of tutorials and guides designed to elevate your coding journey to the next level.

Your feedback and support fuel my passion for sharing knowledge. If you found this tutorial helpful, please leave some claps and follow me for more insightful content. Thank you for joining me, and happy coding until our next adventure!

Deuk Services: Your Gateway to Leading Android Innovation

Are you looking to boost your business with top-tier Android solutions?Partner with Deuk services and take your projects to unparalleled heights.

🚀 Boost Your Productivity with Notion

New to Notion? Discover how it can revolutionize your productivity

Ready to take your productivity to the next level? Integrate this content into your Notion workspace with ease:

1. Access the Notion Version of this Content

2. Look for the Duplicate button at the top-right corner of the page

3. Click on it to add this valuable resource to your Notion workspace.

Seamlessly integrate this guide into your Notion workspace for easy access and swift reference. Leverage Notion AI to search and extract crucial insights, enhancing your productivity. Start curating your knowledge hub with Notion AI today and maximize every learning moment.

--

--

Deuk
Deuk

Published in Deuk

Expert Native Android Solutions: Forging Business Success

Ken Ruiz Inoue
Ken Ruiz Inoue

Written by Ken Ruiz Inoue

Full-Stack Engineer. Doing tech 24/7.

No responses yet