Persistent data storage in Android Studio

- Andrés Cruz

ES En español

Persistent data storage in Android Studio

Almost all applications that we use or develop on Android need to store data persistently: user settings, sessions, files, structured information, or interface states. Although the concept of persistence has not changed over the years, the tools, APIs, and best practices have radically changed.

In simple terms, persisting data means that the information survives the closing of the application, configuration changes, and even the restarting of the device. In modern Android, choosing the wrong storage mechanism can lead to data loss, performance problems, or even the rejection of the app on Google Play.

This article analyzes all current techniques for persistent data storage in Android Studio, starting from the classic concepts and bringing them to the current state of Android (Jetpack, Kotlin, and Compose).

In modern versions of Android, incorrectly choosing the storage mechanism can cause:

  • Performance problems (main thread blockages)
  • Security and privacy risks
  • Data loss due to configuration changes
  • Rejection of the application on Google Play

In simple terms, persisting data means that the information survives the closing of the app, configuration changes, and, in many cases, the restarting of the device. Current Android no longer relies on a single solution, but on a combined persistence strategy.

In this expanded and updated guide, we will see all the current options for persistent data storage in Android Studio, when to use each one, practical examples, and which approaches have become obsolete.


What is persistence in Android?

In computing, persistence consists of ensuring that the data manipulated by an application survives over time, regardless of the process that created it. Colloquially speaking: that the data is not deleted when the app is closed.

In modern Android, persistence is divided into three main levels, each with a different purpose:

  1. UI State (memory): temporary data necessary so that the interface does not restart on rotations or configuration changes.
  2. Local persistence (disk): data that must survive the closing of the app.
  3. Structured persistence: relational or queryable data.

A well-designed application combines several mechanisms, it does not depend on just one.

Android explicitly recommends not mixing responsibilities. A common mistake in older articles is to use databases or files to save UI state, which is now considered a bad practice.


1. Shared Preferences (SharedPreferences)

SharedPreferences allow you to store simple key-value pairs such as booleans, numbers, or text. Historically, they were the most used mechanism for saving user settings.

Features

  • Storage in a private XML file
  • Does not require permissions
  • Ideal for small configurations

For many years, SharedPreferences was the standard solution for saving simple key-value pairs.

What it allows you to store:

  • Boolean
  • Int, Long, Float
  • String
  • Set

Classic example:

 
SharedPreferences prefs = getSharedPreferences("MyPreference", MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("dark_mode", true);
editor.apply();

Problems detected over time:

  • Synchronous writes if commit() is used
  • Risk of corruption
  • No concurrency control
  • No strong typing

Current limitations

  • Potential synchronous access
  • Risk of inconsistencies
  • No type safety

For these reasons, it is no longer recommended as a primary solution in new apps.

1.2 DataStore (modern and recommended replacement)

DataStore is part of Jetpack and is the official replacement for SharedPreferences.

There are two variants:

DataStore Preferences

Ideal for simple settings.

 
val Context.dataStore by preferencesDataStore(name = "settings")
 
suspend fun saveTheme(context: Context, dark: Boolean) {
context.dataStore.edit { prefs ->
prefs[booleanPreferencesKey("dark_mode")] = dark
}
}

Reactive reading:

 
val darkModeFlow: Flow<Boolean> = context.dataStore.data
.map { prefs -> prefs[booleanPreferencesKey("dark_mode")] ?: false }

Proto DataStore

Used when you need a strong schema and validation.

  • Ideal for large apps
  • Uses Protobuf
  • Avoids errors due to incorrect keys

When to use DataStore:

  • User preferences
  • Configuration flags
  • Small but persistent data

Advantages

  • Asynchronous
  • Safe from corruption
  • Integration with coroutines
  • Reactive observation with Flow

When to use DataStore

  • User preferences
  • Configuration flags
  • Simple persistent states

2. File storage

2.1 Internal memory (private and secure)

Internal memory remains one of the safest and simplest options.

val file = File(filesDir, "config.txt")
file.writeText("Hello World")

Features:

  • Only accessible by the app
  • Deleted when uninstalled
  • Ideal for sensitive data

Features:

  • Does not require permissions
  • Only accessible by the app
  • Deleted when uninstalled

Example in Kotlin:

 
val file = File(context.filesDir, "config.txt")
file.writeText("Initial configuration")

Reading:

 
val content = file.readText()

Typical use cases:

  • Configuration files
  • Encrypted tokens
  • Private app data

2.2 External memory and Scoped Storage

Since Android 10 (API 29), Google introduced Scoped Storage.

What changed:

  • Free access to the SD card removed
  • WRITE/READ_EXTERNAL_STORAGE permissions deprecated
  • Limited access by sandbox

Recommended example:

 
val file = File(
context.getExternalFilesDir(Environment.DIRECTORY_DOCUMENTS),
"report.pdf"
)

To access files created by other apps, you must use:

  • Storage Access Framework (SAF)
  • Intent ACTION_OPEN_DOCUMENT

Important: incorrect use of storage is one of the most common causes of rejection on Google Play.


3. Databases: SQLite vs Room

3.1 Direct SQLite (low level)

SQLite is still the base engine, but handling it manually implies:

  • Repetitive code
  • Runtime errors
  • Difficult maintenance

Today it is considered a low-level option.

Features

  • Database in a single file
  • Not client-server
  • Fast and lightweight

3.2 Room (current standard)

Room is the official abstraction layer of Android Jetpack over SQLite.

Components:

  • Entity
  • DAO
  • Database

Complete example:

 
@Entity(tableName = "users")
data class User(
@PrimaryKey val id: Int,
val name: String,
val email: String
)
 
@Dao
interface UserDao {
@Insert(onConflict = OnConflictStrategy.REPLACE)
suspend fun insert(user: User)
 
@Query("SELECT * FROM users")
fun getUsers(): Flow<List<User>>
}
 
@Database(entities = [User::class], version = 1)
abstract class AppDatabase : RoomDatabase() {
abstract fun userDao(): UserDao
}

Real advantages:

  • Compile-time verification
  • Integration with Flow and LiveData
  • Easy testing

4. Cache: temporary data

The cache allows you to store rebuildable data.

Example:

 
val cacheFile = File(context.cacheDir, "response.json")

Good practices:

  • Never critical data
  • Limit size
  • The system can delete it

Good practices:

  • Never save critical data
  • Limit size
  • Clean periodically

5. UI State: ViewModel and SavedState

5.1 ViewModel

Designed to:

  • Survive rotations
  • Avoid unnecessary reloads
 
class MainViewModel : ViewModel() {
val counter = MutableLiveData(0)
}

5.2 SavedStateHandle

Allows restoring state after process closure.

 
class SearchViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
 
var query: String
get() = savedStateHandle["query"] ?: ""
set(value) {
savedStateHandle["query"] = value
}
}

Key rule:

UI state should not be saved in the database.

ViewModel

  • Keeps data in memory
  • Survives rotations
  • Does not survive process kill

SavedState / rememberSaveable

  • Lightweight backup
  • Only small data

6. Advanced persistence: Ink API and serialization

In drawing apps:

  • Serialize StrokeInputBatch
  • Save Brush separately
  • Export to image

This approach is essential for:

  • Note-taking apps
  • Whiteboards
  • Real-time collaboration

Which mechanism should I use?

NeedRecommended solution
PreferencesDataStore
UI StateViewModel + SavedState
Relational dataRoom
Private filesInternal memory
Shared filesSAF / Scoped Storage
Temporary dataCache

Conclusion

Persistence in modern Android is not based on a single technique, but on a well-thought-out architecture:

  • ViewModel for UI
  • DataStore for preferences
  • Room for complex data
  • Scoped Storage for files

Updating these concepts is essential to create safe, efficient, and aligned applications with the current standards of Android Studio.


7. End-to-end example: Complete app with ViewModel + DataStore + Room + UI

Scenario

A simple To-Do application that:

  • Saves preferences (dark mode)
  • Persists tasks in the database
  • Maintains UI state on rotations

Architecture

  • UI: Jetpack Compose
  • State: ViewModel + StateFlow
  • Preferences: DataStore
  • Data: Room

DataStore – Preferences

 
val Context.settingsDataStore by preferencesDataStore("settings")
 
class SettingsRepository(private val context: Context) {
val darkMode: Flow<Boolean> = context.settingsDataStore.data
.map { it[booleanPreferencesKey("dark_mode")] ?: false }
 
suspend fun setDarkMode(enabled: Boolean) {
context.settingsDataStore.edit {
it[booleanPreferencesKey("dark_mode")] = enabled
}
}
}

Room – Database

 
@Entity
data class Task(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val title: String,
val completed: Boolean = false
)
 
@Dao
interface TaskDao {
@Query("SELECT * FROM Task")
fun getTasks(): Flow<List<Task>>
 
@Insert
suspend fun insert(task: Task)
}

ViewModel – State logic

 
class TaskViewModel(
private val dao: TaskDao,
private val settings: SettingsRepository
) : ViewModel() {
 
val tasks = dao.getTasks().stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(5000),
emptyList()
)
 
val darkMode = settings.darkMode.stateIn(
viewModelScope,
SharingStarted.Eagerly,
false
)
}

UI – Jetpack Compose

 
@Composable
fun TaskScreen(viewModel: TaskViewModel) {
val tasks by viewModel.tasks.collectAsState()
val darkMode by viewModel.darkMode.collectAsState()
 
LazyColumn {
items(tasks) { task ->
Text(task.title)
}
}
}

8. 100% Jetpack Compose version

rememberSaveable

 
var text by rememberSaveable { mutableStateOf("") }

It is used for:

  • Input text
  • Scroll
  • Temporary selections

StateFlow + Compose

 
val uiState: StateFlow<UiState>

Advantages:

  • Reactive
  • Lifecycle-aware
  • Easy testing

Room + Flow + Compose

  • Room exposes Flow
  • ViewModel transforms it
  • Compose observes it

This avoids:

  • Callbacks
  • Inconsistent states

9. Advanced comparison table

MechanismPerformanceSurvives process killReal casesCommon errors
ViewModelVery highUI StateSaving persistent data
SavedStateMedium✔️Inputs, scro 

Learn how to implement persistent data storage in Android Studio using DataStore, Room, SQLite, and modern best practices step by step.

I agree to receive announcements of interest about this Blog.

Andrés Cruz

ES En español