CS 3180 Mobile Application Development

Week 11: DataStore & File Storage

CS 3180 — Mobile Application Development

Preferences DataStore · Proto DataStore · Flow · SharedPreferences Migration

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

How Does an App Remember?

"You open a golf scoring app. Your preferred scorecard view is set, dark mode is on, your last tournament is pre-selected. You never had to configure any of that again. Where did those settings come from?"

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Android Storage Options

Persistence in Android exists on a spectrum of complexity

Mechanism Best For Introduced
SharedPreferences Simple key-value pairs Android 1.0
Preferences DataStore Key-value pairs, modern Jetpack 2020
Proto DataStore Typed objects Jetpack 2020
Room Structured relational data Jetpack 2017
Files Binary/text files Android 1.0

Today: DataStore. Next week: Room.

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

The Problem with SharedPreferences

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

SharedPreferences: The Old Way

// Write:
val prefs = getSharedPreferences("settings", MODE_PRIVATE)
prefs.edit()
    .putString("theme", "dark")
    .putBoolean("notifications", true)
    .apply()

// Read:
val theme = prefs.getString("theme", "light")

Simple. Familiar. But this API is hiding serious problems.

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

SharedPreferences: The Problems

  • Blocking I/Oapply() writes to disk on the main thread synchronously during onStop()
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

SharedPreferences: The Problems

  • Blocking I/Oapply() writes to disk on the main thread synchronously during onStop()
  • No error propagation — failures are silently swallowed
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

SharedPreferences: The Problems

  • Blocking I/Oapply() writes to disk on the main thread synchronously during onStop()
  • No error propagation — failures are silently swallowed
  • No type safetygetString("enabled", null) compiles; crashes at runtime
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

SharedPreferences: The Problems

  • Blocking I/Oapply() writes to disk on the main thread synchronously during onStop()
  • No error propagation — failures are silently swallowed
  • No type safetygetString("enabled", null) compiles; crashes at runtime
  • No Flow/coroutine support — manual callbacks required for reactive updates
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

SharedPreferences: The Problems

  • Blocking I/Oapply() writes to disk on the main thread synchronously during onStop()
  • No error propagation — failures are silently swallowed
  • No type safetygetString("enabled", null) compiles; crashes at runtime
  • No Flow/coroutine support — manual callbacks required for reactive updates
  • Race conditions — concurrent reads/writes aren't truly safe

DataStore fixes every one of these.

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Preferences DataStore

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

What is Preferences DataStore?

  • Stores key-value pairs asynchronously and safely
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

What is Preferences DataStore?

  • Stores key-value pairs asynchronously and safely
  • Built on Kotlin coroutines and Flow
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

What is Preferences DataStore?

  • Stores key-value pairs asynchronously and safely
  • Built on Kotlin coroutines and Flow
  • All I/O happens off the main thread — no ANR risk
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

What is Preferences DataStore?

  • Stores key-value pairs asynchronously and safely
  • Built on Kotlin coroutines and Flow
  • All I/O happens off the main thread — no ANR risk
  • Errors surface as exceptions — nothing is silently swallowed
  • Consistent state — atomic reads and writes

Think of it as SharedPreferences, rebuilt with modern Kotlin idioms.

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Step 1: Add the Dependency

// build.gradle.kts
dependencies {
    implementation("androidx.datastore:datastore-preferences:1.1.1")
}

One library. No separate runtime, no additional setup.

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Step 2: Create the DataStore

// At the top level of a file (not inside a class):
val Context.dataStore: DataStore<Preferences> by preferencesDataStore(
    name = "settings"
)
  • Uses Kotlin extension property on Context
  • by preferencesDataStore(...) is a delegate — creates the singleton
  • The name maps to the file on disk: files/datastore/settings.preferences_pb
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Step 3: Define Keys

object SettingsKeys {
    val THEME         = stringPreferencesKey("theme")
    val NOTIFICATIONS = booleanPreferencesKey("notifications")
    val FONT_SIZE     = intPreferencesKey("font_size")
    val LAST_TOURNEY  = stringPreferencesKey("last_tournament_id")
}

Keys are typedstringPreferencesKey, booleanPreferencesKey, intPreferencesKey, floatPreferencesKey, longPreferencesKey, doublePreferencesKey, stringSetPreferencesKey

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Reading from DataStore

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Reading: Flow of Preferences

// In a Repository:
val themeFlow: Flow<String> = context.dataStore.data
    .map { prefs ->
        prefs[SettingsKeys.THEME] ?: "light"    // default value
    }
  • context.dataStore.data is a Flow<Preferences>
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Reading: Flow of Preferences

// In a Repository:
val themeFlow: Flow<String> = context.dataStore.data
    .map { prefs ->
        prefs[SettingsKeys.THEME] ?: "light"    // default value
    }
  • context.dataStore.data is a Flow<Preferences>
  • .map { } extracts the specific value you care about
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Reading: Flow of Preferences

// In a Repository:
val themeFlow: Flow<String> = context.dataStore.data
    .map { prefs ->
        prefs[SettingsKeys.THEME] ?: "light"    // default value
    }
  • context.dataStore.data is a Flow<Preferences>
  • .map { } extracts the specific value you care about
  • The flow emits immediately with the current value
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Reading: Flow of Preferences

// In a Repository:
val themeFlow: Flow<String> = context.dataStore.data
    .map { prefs ->
        prefs[SettingsKeys.THEME] ?: "light"    // default value
    }
  • context.dataStore.data is a Flow<Preferences>
  • .map { } extracts the specific value you care about
  • The flow emits immediately with the current value
  • Then re-emits every time the value changes
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Collecting in a ViewModel

class SettingsViewModel(
    private val repository: SettingsRepository
) : ViewModel() {

    val theme: StateFlow<String> = repository.themeFlow
        .stateIn(
            scope = viewModelScope,
            started = SharingStarted.Eagerly,
            initialValue = "light"
        )
}
  • stateIn() converts FlowStateFlow for the UI layer
  • SharingStarted.Eagerly — start collecting immediately
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Collecting in Compose

@Composable
fun SettingsScreen(vm: SettingsViewModel = viewModel()) {
    val theme by vm.theme.collectAsState()

    Column {
        Text("Current theme: $theme")
        Button(onClick = { vm.updateTheme("dark") }) {
            Text("Switch to Dark")
        }
    }
}

The UI recomposes automatically when the theme changes — from any source.

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Writing to DataStore

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Writing: edit {} Block

// In a Repository:
suspend fun updateTheme(theme: String) {
    context.dataStore.edit { prefs ->
        prefs[SettingsKeys.THEME] = theme
    }
}
  • edit {} is a suspend function — must call from a coroutine
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Writing: edit {} Block

// In a Repository:
suspend fun updateTheme(theme: String) {
    context.dataStore.edit { prefs ->
        prefs[SettingsKeys.THEME] = theme
    }
}
  • edit {} is a suspend function — must call from a coroutine
  • The block receives a MutablePreferences you can modify
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Writing: edit {} Block

// In a Repository:
suspend fun updateTheme(theme: String) {
    context.dataStore.edit { prefs ->
        prefs[SettingsKeys.THEME] = theme
    }
}
  • edit {} is a suspend function — must call from a coroutine
  • The block receives a MutablePreferences you can modify
  • The write is atomic — all-or-nothing
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Writing: edit {} Block

// In a Repository:
suspend fun updateTheme(theme: String) {
    context.dataStore.edit { prefs ->
        prefs[SettingsKeys.THEME] = theme
    }
}
  • edit {} is a suspend function — must call from a coroutine
  • The block receives a MutablePreferences you can modify
  • The write is atomic — all-or-nothing
  • The change triggers any active dataStore.data flows
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Writing Multiple Values Atomically

suspend fun resetToDefaults() {
    context.dataStore.edit { prefs ->
        prefs[SettingsKeys.THEME]         = "light"
        prefs[SettingsKeys.NOTIFICATIONS] = true
        prefs[SettingsKeys.FONT_SIZE]     = 16
        prefs[SettingsKeys.LAST_TOURNEY]  = ""
    }
}

All four values are written atomically in one operation.

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

ViewModel: Trigger the Write

class SettingsViewModel(
    private val repository: SettingsRepository
) : ViewModel() {

    fun updateTheme(newTheme: String) {
        viewModelScope.launch {
            repository.updateTheme(newTheme)
        }
    }

    fun resetToDefaults() {
        viewModelScope.launch {
            repository.resetToDefaults()
        }
    }
}
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

A Complete Settings Repository

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

SettingsRepository

class SettingsRepository(private val context: Context) {

    private val dataStore = context.dataStore

    // Read streams
    val theme: Flow<String> = dataStore.data
        .catch { e ->
            if (e is IOException) emit(emptyPreferences())
            else throw e
        }
        .map { prefs -> prefs[SettingsKeys.THEME] ?: "light" }

    val notificationsEnabled: Flow<Boolean> = dataStore.data
        .catch { e ->
            if (e is IOException) emit(emptyPreferences())
            else throw e
        }
        .map { prefs -> prefs[SettingsKeys.NOTIFICATIONS] ?: true }
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

SettingsRepository (continued)

    // Write operations
    suspend fun setTheme(theme: String) {
        dataStore.edit { prefs -> prefs[SettingsKeys.THEME] = theme }
    }

    suspend fun setNotificationsEnabled(enabled: Boolean) {
        dataStore.edit { prefs -> prefs[SettingsKeys.NOTIFICATIONS] = enabled }
    }

    suspend fun setLastTournament(id: String) {
        dataStore.edit { prefs -> prefs[SettingsKeys.LAST_TOURNEY] = id }
    }
}
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Migrating from SharedPreferences

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Built-in Migration

val Context.dataStore by preferencesDataStore(
    name = "settings",
    produceMigrations = { context ->
        listOf(
            SharedPreferencesMigration(
                context = context,
                sharedPreferencesName = "legacy_settings"
            )
        )
    }
)
  • DataStore reads the old SharedPreferences file on first access
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Built-in Migration

val Context.dataStore by preferencesDataStore(
    name = "settings",
    produceMigrations = { context ->
        listOf(
            SharedPreferencesMigration(
                context = context,
                sharedPreferencesName = "legacy_settings"
            )
        )
    }
)
  • DataStore reads the old SharedPreferences file on first access
  • Copies all matching keys automatically
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Built-in Migration

val Context.dataStore by preferencesDataStore(
    name = "settings",
    produceMigrations = { context ->
        listOf(
            SharedPreferencesMigration(
                context = context,
                sharedPreferencesName = "legacy_settings"
            )
        )
    }
)
  • DataStore reads the old SharedPreferences file on first access
  • Copies all matching keys automatically
  • Deletes the SharedPreferences file after migration
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Built-in Migration

val Context.dataStore by preferencesDataStore(
    name = "settings",
    produceMigrations = { context ->
        listOf(
            SharedPreferencesMigration(
                context = context,
                sharedPreferencesName = "legacy_settings"
            )
        )
    }
)
  • DataStore reads the old SharedPreferences file on first access
  • Copies all matching keys automatically
  • Deletes the SharedPreferences file after migration
  • One-time operation — subsequent runs are unaffected
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Migration Caveats

  • Keys are migrated by string name — your new preferencesKey names must match the old SharedPreferences keys
  • Non-matching keys are skipped (not an error)
  • If migration fails, DataStore throws — handle appropriately
// Old SharedPreferences key: "theme"
// New DataStore key must match:
val THEME = stringPreferencesKey("theme")  // ✅ matches — will migrate
val THEME = stringPreferencesKey("app_theme")  // ❌ won't find old data
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Proto DataStore

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Preferences vs Proto DataStore

Preferences DataStore

  • Key-value pairs
  • Primitive types only
  • String/Boolean/Int/etc.
  • Simpler setup
  • Good for settings

Proto DataStore

  • Typed Kotlin objects
  • Nested structures
  • Custom serialization
  • More setup required
  • Good for complex state
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Proto DataStore: The Idea

// Define your data class:
@Serializable
data class UserPreferences(
    val theme: String = "light",
    val notificationsEnabled: Boolean = true,
    val lastTournamentId: String = ""
)

// Create a typed DataStore:
val Context.userPrefsDataStore: DataStore<UserPreferences>
    by dataStore(
        fileName = "user_prefs.json",
        serializer = UserPreferencesSerializer
    )

The entire object is read/written atomically — no key juggling.

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Proto DataStore: The Serializer

object UserPreferencesSerializer : Serializer<UserPreferences> {

    override val defaultValue: UserPreferences = UserPreferences()

    override suspend fun readFrom(input: InputStream): UserPreferences {
        return try {
            Json.decodeFromStream(input)
        } catch (e: SerializationException) {
            throw CorruptionException("Cannot read preferences.", e)
        }
    }

    override suspend fun writeTo(t: UserPreferences, output: OutputStream) {
        Json.encodeToStream(t, output)
    }
}

Serializer<T> — three members: defaultValue, readFrom, writeTo

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Settings Screen

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

What the Golf App Needs to Remember

object GolfAppKeys {
    val SCORECARD_VIEW  = stringPreferencesKey("scorecard_view")   // "compact" | "full"
    val DARK_MODE       = booleanPreferencesKey("dark_mode")
    val DEFAULT_TOURNEY = stringPreferencesKey("default_tournament_id")
    val PLAYER_NAME     = stringPreferencesKey("player_name")
    val SHOW_PAR_COLORS = booleanPreferencesKey("show_par_colors")
}

These are the preferences a real client settings screen would expose.

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Settings Screen Flow

App launches
    ↓
SettingsRepository.lastTournamentId (Flow) emits
    ↓
MainViewModel collects → navigates to last tournament
    ↓
User opens Settings
    ↓
Toggles dark mode → SettingsViewModel.setDarkMode(true)
    ↓
DataStore.edit { } writes asynchronously
    ↓
themeFlow emits new value
    ↓
All collectors (including top-level theme provider) recompose
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Live Code: Settings ViewModel

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

What We'll Build

SettingsScreen
    ↓ observes
SettingsViewModel
    ↓ reads/writes
SettingsRepository
    ↓ wraps
DataStore<Preferences>
    ↓ stores
settings.preferences_pb  (on disk)

We'll implement dark mode + default tournament ID — two contrasting types.

Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Repository

class SettingsRepository(context: Context) {

    private val dataStore = context.dataStore

    val isDarkMode: Flow<Boolean> = dataStore.data
        .catch { emit(emptyPreferences()) }
        .map { it[GolfAppKeys.DARK_MODE] ?: false }

    val defaultTournamentId: Flow<String> = dataStore.data
        .catch { emit(emptyPreferences()) }
        .map { it[GolfAppKeys.DEFAULT_TOURNEY] ?: "" }

    suspend fun setDarkMode(enabled: Boolean) {
        dataStore.edit { it[GolfAppKeys.DARK_MODE] = enabled }
    }

    suspend fun setDefaultTournament(id: String) {
        dataStore.edit { it[GolfAppKeys.DEFAULT_TOURNEY] = id }
    }
}
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

ViewModel

class SettingsViewModel(private val repo: SettingsRepository) : ViewModel() {

    val isDarkMode: StateFlow<Boolean> = repo.isDarkMode
        .stateIn(viewModelScope, SharingStarted.Eagerly, false)

    val defaultTournamentId: StateFlow<String> = repo.defaultTournamentId
        .stateIn(viewModelScope, SharingStarted.Eagerly, "")

    fun toggleDarkMode(enabled: Boolean) {
        viewModelScope.launch { repo.setDarkMode(enabled) }
    }

    fun setDefaultTournament(id: String) {
        viewModelScope.launch { repo.setDefaultTournament(id) }
    }
}
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Composable

@Composable
fun SettingsScreen(vm: SettingsViewModel = viewModel()) {
    val isDark by vm.isDarkMode.collectAsState()

    Column(Modifier.padding(16.dp)) {
        Text("Settings", style = MaterialTheme.typography.headlineMedium)

        Spacer(Modifier.height(16.dp))

        Row(verticalAlignment = Alignment.CenterVertically) {
            Text("Dark Mode", Modifier.weight(1f))
            Switch(
                checked = isDark,
                onCheckedChange = { vm.toggleDarkMode(it) }
            )
        }
    }
}
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Summary

  • SharedPreferences — legacy, avoid in new code
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Summary

  • SharedPreferences — legacy, avoid in new code
  • Preferences DataStore — the modern key-value persistence layer
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Summary

  • SharedPreferences — legacy, avoid in new code
  • Preferences DataStore — the modern key-value persistence layer
  • Key pattern: stringPreferencesKey, booleanPreferencesKey, etc.
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Summary

  • SharedPreferences — legacy, avoid in new code
  • Preferences DataStore — the modern key-value persistence layer
  • Key pattern: stringPreferencesKey, booleanPreferencesKey, etc.
  • Reading: dataStore.data.map { prefs -> prefs[KEY] ?: default }
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Summary

  • SharedPreferences — legacy, avoid in new code
  • Preferences DataStore — the modern key-value persistence layer
  • Key pattern: stringPreferencesKey, booleanPreferencesKey, etc.
  • Reading: dataStore.data.map { prefs -> prefs[KEY] ?: default }
  • Writing: dataStore.edit { prefs -> prefs[KEY] = value } (suspend)
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Summary

  • SharedPreferences — legacy, avoid in new code
  • Preferences DataStore — the modern key-value persistence layer
  • Key pattern: stringPreferencesKey, booleanPreferencesKey, etc.
  • Reading: dataStore.data.map { prefs -> prefs[KEY] ?: default }
  • Writing: dataStore.edit { prefs -> prefs[KEY] = value } (suspend)
  • Repository → ViewModel → Composable — the full stack
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Summary

  • SharedPreferences — legacy, avoid in new code
  • Preferences DataStore — the modern key-value persistence layer
  • Key pattern: stringPreferencesKey, booleanPreferencesKey, etc.
  • Reading: dataStore.data.map { prefs -> prefs[KEY] ?: default }
  • Writing: dataStore.edit { prefs -> prefs[KEY] = value } (suspend)
  • Repository → ViewModel → Composable — the full stack
  • Migration: SharedPreferencesMigration for existing apps
Week 11 Monday: DataStore & Persistent Storage
CS 3180 Mobile Application Development

Due This Week

Item Due
Assignment 2: Settings App Friday 11:59 PM
Chapter 9 Quiz Sunday 11:59 PM
zyBook Ch 9 activities Sunday 11:59 PM

Wednesday: File storage, image caching with Coil

Week 11 Monday: DataStore & Persistent Storage

SPEAKER NOTES: Welcome to Week 11. Last week we tackled async programming — coroutines, dispatchers, Flow. Today we apply those concepts to a concrete problem: how does an app persist user preferences and data across sessions? This is something every real app does, and Android has gone through a significant evolution in how it's done.

SPEAKER NOTES: Let students sit with this question. Every app they use "remembers" something. How? The answer is persistence — writing data to a storage medium that survives the app being closed or the device being rebooted. Today's session covers the right way to do this in modern Android.

SPEAKER NOTES: Draw the spectrum on the board. SharedPreferences is the old way — still works, but has serious problems. DataStore is the current recommended replacement. Room handles complex structured data. Files handle raw binary. Today's session focuses on DataStore because it's what students will use for settings, preferences, and lightweight persistence in their projects.

SPEAKER NOTES: Before teaching DataStore, establish why SharedPreferences is insufficient. Students may have used it before. Let's understand its failure modes so DataStore's design decisions make sense.

SPEAKER NOTES: This is what most students have probably seen before. It looks harmless — and for tiny, trivial use cases, it works. But let's look at what's actually happening under the hood when this code runs.

SPEAKER NOTES: This one has caused real ANRs in production apps. Every call to apply() schedules a disk write that happens synchronously in onStop() — on the main thread. A slow disk write means a dropped frame or a frozen screen during navigation.

SPEAKER NOTES: If a write fails — disk full, file corrupt, permissions issue — SharedPreferences doesn't tell you. The error is swallowed silently. Users lose settings with no indication anything went wrong.

SPEAKER NOTES: The key is just a String. You stored a Boolean under "enabled" but called getString()? Compiles fine. Crashes at runtime. SharedPreferences has no way to enforce that reads and writes use matching types.

SPEAKER NOTES: SharedPreferences has a listener API, but it's clunky — register/unregister manually, no structured concurrency. You can't observe a preference as a Flow. Every reactive update requires manual wiring.

SPEAKER NOTES: These aren't edge cases — they're properties of the API. Google recognized all of this and built DataStore as a deliberate redesign. It's built on coroutines and Flow from the ground up, which is why last week's content was a prerequisite.

SPEAKER NOTES: Preferences DataStore is the direct replacement for SharedPreferences. Same concept — key-value pairs — but built on coroutines, Flow, and safe off-main-thread I/O.

SPEAKER NOTES: Same concept as SharedPreferences — key-value pairs. The difference is entirely in how it's implemented.

SPEAKER NOTES: Built on coroutines and Flow from the ground up — not bolted on. Every read is a Flow, every write is a suspend function.

SPEAKER NOTES: The compiler enforces this. suspend functions cannot be called on the main thread without a coroutine scope. No more accidental blocking I/O.

SPEAKER NOTES: Errors propagate through the Flow — you can catch them with the catch() operator. Atomic reads and writes mean you can never observe a half-written state. The conceptual model is identical to SharedPreferences — the implementation is entirely different.

SPEAKER NOTES: Just one dependency. Note: if you also need Proto DataStore, that's a separate artifact. For Preferences DataStore — the one we're starting with — this single line is everything.

SPEAKER NOTES: This is idiomatic Kotlin. The preferencesDataStore delegate handles creating and managing the DataStore singleton — you never instantiate it yourself. The extension on Context means you can access it from any Activity, Fragment, or wherever Context is available. The name just names the underlying file.

SPEAKER NOTES: This is the first major improvement over SharedPreferences. Keys carry type information. getString() vs putBoolean() confusion is impossible — the compiler simply won't allow it. Group keys in an object so they're easy to find. The name string is what's stored on disk; the variable name is what you use in code.

SPEAKER NOTES: Reading from DataStore returns a Flow — a stream of values that emits the current state and re-emits whenever the value changes. This is the reactive model that connects naturally to ViewModels and Compose.

SPEAKER NOTES: dataStore.data emits the full Preferences object as a Flow — it re-emits every time any preference changes.

SPEAKER NOTES: map() lets you project out just the one key you care about — no unnecessary recomposition for unrelated changes.

SPEAKER NOTES: Unlike a one-shot function call, the flow emits right away with the stored value — no waiting.

SPEAKER NOTES: This is where DataStore really shines. You don't ask "what is the theme?" once — you observe the theme. Whenever it changes (from any screen, any coroutine), every observer gets notified automatically. No manual refresh, no polling. This is exactly the same reactive model from Room's Flow queries that we'll see next week.

SPEAKER NOTES: The ViewModel converts the cold Flow from the repository into a hot StateFlow that the UI can observe. This is the same stateIn() pattern from Week 10. The ViewModel is the bridge between the DataStore (source of truth) and the composable (consumer).

SPEAKER NOTES: The composable is completely unaware of DataStore. It just observes a StateFlow and renders the value. This composable would render correctly whether the theme comes from DataStore, a test fake, or a hard-coded value. Clean separation.

SPEAKER NOTES: Writing to DataStore is a suspend function — it must be called from a coroutine. This enforces that writes happen off the main thread. No more silent background writes that can corrupt state.

SPEAKER NOTES: The suspend keyword means the compiler prevents main-thread writes. You cannot call edit {} outside a coroutine — it's a compile error.

SPEAKER NOTES: The MutablePreferences block is the transaction boundary — you can modify multiple keys inside one edit block.

SPEAKER NOTES: If you modify three keys in one edit block, either all three are written or none are. No partial writes.

SPEAKER NOTES: The edit block is transactional. If you modify three keys inside one edit block, either all three are written or none of them are. No more partial writes. The write automatically notifies all active Flow collectors — the UI updates automatically without any additional wiring.

SPEAKER NOTES: This is where atomicity really matters. If the user taps "Reset to Defaults," you want either all settings reset or none — not a half-reset state where theme is light but notifications are still the old value. One edit block = one atomic transaction.

SPEAKER NOTES: The ViewModel wraps the suspend calls in viewModelScope.launch. The composable calls these regular functions — it doesn't know or care that they launch coroutines. This is the standard ViewModel pattern for any async operation.

SPEAKER NOTES: Let's put all the pieces together in a production-quality repository. This is the complete pattern that Assignment 2 will use.

SPEAKER NOTES: The catch() operator handles I/O errors — if the preferences file is corrupt or missing, emit emptyPreferences() (a blank slate with all defaults) rather than crashing. This is the production-grade pattern. In a real app you'd factor the catch() into a helper to avoid repetition.

SPEAKER NOTES: Clean, focused methods. Each write targets one key. The ViewModel orchestrates combinations (e.g., when the user changes theme AND saves a tournament ID). The repository stays simple — it's just a gateway to DataStore.

SPEAKER NOTES: Many Android codebases still use SharedPreferences. DataStore has a built-in migration path — you can switch without losing any existing user data. Let's see how.

SPEAKER NOTES: No manual migration code. DataStore detects on first access whether the preferences_pb file exists yet. If it doesn't, the migration runs.

SPEAKER NOTES: No manual key mapping. DataStore matches by string key name — which is why the new preferencesKey names must match the old string names exactly.

SPEAKER NOTES: After migration, the old SharedPreferences XML file is deleted. DataStore is now the sole source of truth.

SPEAKER NOTES: This is one of the most elegant parts of the DataStore design. You don't write migration logic — you just declare that a migration exists. DataStore detects whether migration has happened and runs it once. After that, SharedPreferences is gone. Users never lose data.

SPEAKER NOTES: The most common migration mistake: renaming keys during migration. If your old code stored "theme" but your new key is named "app_theme", DataStore skips it silently and the user loses their setting. Match the string names exactly during migration, then you can rename if needed after the migration completes.

SPEAKER NOTES: Preferences DataStore handles key-value pairs. Proto DataStore handles typed objects — full Kotlin data classes persisted with type safety. It requires more setup but gives you richer data models.

SPEAKER NOTES: Both are in the same Jetpack library. For simple settings (theme, notifications, last-selected item), Preferences DataStore is the right choice. For complex app state that maps naturally to data classes — user profiles, complex configuration objects — Proto DataStore fits better. The assignment uses Preferences DataStore. I want you aware Proto exists so you know when to reach for it.

SPEAKER NOTES: Proto DataStore wraps a full Kotlin object rather than a bag of key-value pairs. You read the whole object, copy it with changes, and write it back. The serializer handles converting to/from bytes. This is cleaner when your settings form a natural unit. The trade-off is the serializer boilerplate — shown on the next slide.

SPEAKER NOTES: This is the boilerplate cost of Proto DataStore. You implement Serializer<T> once per data type. defaultValue is what DataStore returns if the file doesn't exist yet — no null checks. readFrom deserializes bytes to your object; writeTo serializes your object to bytes. The CorruptionException is DataStore's contract for unreadable data — it resets to defaultValue rather than crashing. With kotlinx.serialization, readFrom and writeTo are essentially one-liners.

SPEAKER NOTES: Let's connect DataStore to our client scenario. What user preferences would the Hope Foundation golf scoring app actually need to persist?

SPEAKER NOTES: Walk through each one: scorecard view (does the user prefer a compact card or full scorecard?), dark mode (obvious), default tournament (which tournament should pre-populate when the app opens?), player name (pre-fill entry forms), par colors (should scores be colored red/green vs par?). Assignment 2 will implement a subset of this.

SPEAKER NOTES: Draw this on the board. This is the full reactive loop. The critical insight: when the user toggles dark mode on the Settings screen, every composable in the app that observes the theme — the top-level MaterialTheme wrapper, any component that reads it directly — automatically recomposes. No event bus, no callbacks, no manual "apply theme." The Flow does all the wiring.

SPEAKER NOTES: Let's build the ViewModel that bridges DataStore and the Compose UI. Open Android Studio and start from the Week 11 project.

SPEAKER NOTES: We'll implement two preferences: a boolean (dark mode toggle) and a string (tournament ID picker). That covers the two most common patterns. Students can extend to the other keys on their own for the assignment.

SPEAKER NOTES: Type this live. Point out the consistent structure: each Flow uses catch() for error handling, maps to extract one value, and provides a default with ?:. Each suspend function uses a focused edit block. This is the template students will follow for any DataStore repository.

SPEAKER NOTES: The ViewModel is thin — it converts Flows to StateFlows and wraps suspend calls in launch. This is intentional. No business logic lives here: the repository holds the data contract, the ViewModel provides a stable interface for the UI. If you needed to validate the tournament ID, that logic would go in the repository.

SPEAKER NOTES: The composable is declarative and clean. The Switch reflects isDark (from DataStore) and calls toggleDarkMode when toggled. The ViewModel handles the write. The Flow handles the update. The composable just renders. Run the app: flip the switch, close and reopen the app — the setting persists. That's DataStore working.

SPEAKER NOTES: Blocking I/O, no type safety, no Flow, silent error swallowing. Still works, but DataStore replaces it in all new code.

SPEAKER NOTES: Built on coroutines and Flow. Async, safe, type-checked. Direct replacement for SharedPreferences.

SPEAKER NOTES: Keys carry type information. The compiler prevents reading a Boolean key as a String — a whole class of runtime crashes eliminated.

SPEAKER NOTES: Reading returns a Flow — it emits the current value immediately, then re-emits on every change. No polling, no callbacks.

SPEAKER NOTES: Writing is always a suspend function — off the main thread, atomic, triggers all active Flow collectors automatically.

SPEAKER NOTES: Repository owns the DataStore. ViewModel converts Flow to StateFlow. Composable observes StateFlow. Each layer knows nothing about the layers below it.

SPEAKER NOTES: This is the vocabulary for the assignment. Assignment 2 builds a full settings screen — students implement DataStore persistence for multiple preferences. Wednesday's demo extends this to file storage: exporting data as CSV, reading back, and caching images.

SPEAKER NOTES: Assignment 2 is due Friday — this is the main deliverable of the week. Start today. The Wednesday demo fills in file handling, which is a bonus: students who want to export tournament data as CSV or cache course photos will have the tools. The core DataStore content from today is what the assignment requires.