Persistent data storage in Android Studio

- Andrés Cruz - ES En español

Persistent data storage in Android Studio

Almost all applications 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 changed radically.

In simple terms, persisting data means that the information survives application closure, configuration changes, and even device reboots. In modern Android, choosing the wrong storage mechanism can cause data loss, performance issues, or even app rejection on Google Play.

This article analyzes all current persistent data storage techniques in Android Studio, starting from 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 lead to:

  • Performance issues (main thread blocks)
  • Security and privacy risks
  • Data loss during configuration changes
  • Application rejection on Google Play

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

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


What is persistence in Android?

In computer science, persistence consists of making data manipulated by an application survive over time, regardless of the process that created it. Colloquially speaking: that the data is not deleted when the app closes.

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

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

A well-designed application combines several mechanisms, rather than relying on just one.

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


1. Shared Preferences (SharedPreferences)

SharedPreferences allow storing simple key–value pairs like booleans, numbers, or text. Historically, they were the most widely 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 to store:

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

Classic example:

 
SharedPreferences prefs = getSharedPreferences("MiPreferencia", MODE_PRIVATE);
SharedPreferences.Editor editor = prefs.edit();
editor.putBoolean("modo_oscuro", 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 the 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 configurations.

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 storage (private and secure)

Internal storage 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 upon uninstallation
  • Ideal for sensitive data

Features:

  • Does not require permissions
  • Only accessible by the app
  • Deleted upon uninstallation

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 storage 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 are obsolete
  • Limited access through 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 remains the base engine, but handling it manually implies:

  • Boilerplate 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 Android Jetpack's official abstraction layer 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 storing reconstructible data.

Example:

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

Best practices:

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

Best 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 death.

 

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 storage
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 applications that are secure, efficient, and aligned with current Android Studio standards.


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

Scenario

Simple to-do (To‑Do) application that:

  • Saves preferences (dark mode)
  • Persists tasks in a database
  • Maintains UI state during 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

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.


Únete a la comunidad de desarrolladores que han decidido dejar de picar código y empezar a construir productos reales. Recibe mis mejores trucos de arquitectura cada semana:

I agree to receive announcements of interest about this Blog.