SharedPreferences
🗂️SharedPreferences — Complete Mastery Textbook
From First Principles to System Internals to Production Patterns
Written for Android developers using Kotlin. Covers everything from Android 1.0 history
through modern usage, internal mechanics, all APIs, real-world patterns, and known dangers.
Table of Contents
Part I — History & Foundation
1. What Is SharedPreferences?
2. The History — Why It Was Created
3. The XML File on Disk
4. How SharedPreferences Loads Into Memory
Part II — System Level Internals 5. The In-Memory HashMap — How Reads Are Instant 6. The Editor
— How Writes Work Internally 7. commit() vs apply() — The Deep Difference 8. The QueuedWork Trap
— The Hidden ANR Mechanism 9. The Listener System — How Change Callbacks Work
Part III — Complete API Reference 10. Getting a SharedPreferences Instance 11. All Access Modes
Explained 12. All Data Types You Can Store 13. Reading Data — Every Method 14. Writing Data —
Every Method 15. Deleting Data 16. Checking If a Key Exists 17. Change Listeners
Part IV — Architecture & Patterns 18. SharedPreferences in Clean Architecture 19. SharedPreferences
in Kotlin — Modern Wrappers 20. SharedPreferences with Koin DI 21. Multi-Process
SharedPreferences
Part V — Dangers & Best Practices 22. The 7 Dangers of SharedPreferences 23. Common Mistakes &
How to Avoid Them 24. Security Considerations
Part VI — Real-World Examples 25. Real-World Patterns — 10 Complete
Examples 26. SharedPreferences vs DataStore — When to Use What 27. Migrating From
SharedPreferences to DataStore 28. Quick Reference Cheat Sheet
PART I — HISTORY & FOUNDATION
Chapter 1: What Is SharedPreferences?
SharedPreferences is Android's built-in, simple, key-value data storage system. It stores small pieces
of data as named pairs — a key and a value — and persists them to disk as an XML file so they survive
app restarts, device reboots, and even Android version updates.
Think of it as a persistent dictionary built into every Android app. You put something in — it stays
there. You come back tomorrow, next week, after a reboot — it's still there.
1 App Memory (HashMap) Disk (XML file)
2 ┌──────────────────┐ ┌────────────────────────────────┐
3 │ dark_mode: true │ ←──────→ │ <boolean name="dark_mode" │
4 │ username: "John" │ │ value="true" /> │
5 │ launch_count: 5 │ │ <string name="username" │
6 └──────────────────┘ │ value="John" /> │
7 │ <int name="launch_count" │
8 │ value="5" /> │
9 └────────────────────────────────┘
The in-memory HashMap provides fast reads. The XML file provides persistence.
What SharedPreferences Is For
SharedPreferences is designed specifically for small, simple data:
User preferences (dark mode, language, font size)
App state flags (onboarding completed, first launch, terms accepted)
Simple cached values (last selected tab, last known username)
Settings that control app behavior
What SharedPreferences Is NOT For
Large datasets → use Room
Sensitive credentials (passwords, tokens) → use EncryptedSharedPreferences or Keystore
Lists or collections → use Room
Complex objects or relationships → use Room
Large binary data → use files directly
Chapter 2: The History — Why It Was Created
2008 — The Problem Android Launched With
When Android 1.0 shipped in September 2008, developers needed a way to persist small pieces of
data. The existing options were:
SQLite — powerful but heavyweight for simple key-value needs. Writing SQL for a boolean flag
felt absurd.
Raw files — full control but required managing FileOutputStreams, parsing, threading, error
handling. Far too complex for simple data.
Java Properties files — available in the JVM but not integrated with Android's Context system.
There was a clear gap: developers needed something as simple as a HashMap but that survived app
restarts.
The Design Decision
The Android team made SharedPreferences to fill this gap. The key design decisions were:
1. XML storage — human-readable, debuggable, compatible with Java's existing XML tools
2. Synchronous reads — for simplicity. Reading from memory should be instant.
3. Two write modes — commit() for synchronous confirmed writes, apply() for "fire and forget"
async writes
4. Activity and Application scoping — preferences could be per-Activity ( getPreferences() ) or
app-wide ( getSharedPreferences() )
This design made perfect sense in 2008. Phones had single-core CPUs, apps were simple, and Kotlin
Coroutines did not exist. Synchronous APIs were the norm.
2008–2014 — Widespread Adoption
SharedPreferences became the universal solution for persistence in Android. Every tutorial, every
book, every example used it. By 2012, it was embedded in virtually every Android app ever written.
Google's own apps used it. Third-party SDKs used it. It was the default solution.
2014–2018 — The ANR Epidemic
As Android apps grew more complex — multiple screens, background services, complex lifecycles —
and as the Android user base expanded to include cheap, slow devices with poor I/O performance, a
pattern emerged in crash reporting tools:
ANR (Application Not Responding) errors, with stack traces pointing to:
1 [Link] → [Link]
Bumble (the dating app) published a detailed post-mortem showing SharedPreferences was
causing 6x more ANRsthan expected. Their engineering team spent months tracing the root cause —
it was buried in [Link] , a hidden Android system class that most developers had never
heard of.
The critical insight (covered deeply in Chapter 8): apply() appears async but actually blocks the
main thread during Activity lifecycle transitions.
2019 — Google Acknowledges the Problems
At Google I/O 2019, Google officially acknowledged SharedPreferences had fundamental design flaws
and announced DataStore as its replacement.
The official Android documentation now says:
"Caution: DataStore is a modern data storage solution that you should use instead of
SharedPreferences."
2021 — DataStore Becomes the Recommendation
With DataStore 1.0 stable released in August 2021, Google formally deprecated SharedPreferences as
the recommended solution for new code. However, SharedPreferences itself was NOT removed from
the Android API — it still exists and works. Legacy code using it still runs.
Where SharedPreferences Stands Today (2024+)
SharedPreferences is:
Still in the Android SDK — not removed, not deprecated at the API level
Still used in billions of existing apps
Still used in many third-party SDKs you cannot control
Still acceptable for very simple use cases where DataStore setup is overkill
Not recommended for new code — DataStore is the official modern replacement
Understanding SharedPreferences thoroughly is still essential because:
1. You will encounter it in legacy codebases
2. Third-party SDKs you use internally use it
3. Understanding why it was replaced makes you a better developer
4. Some codebases still use it intentionally for simplicity
Chapter 3: The XML File on Disk
File Location
Every SharedPreferences file is stored at:
1 /data/data/[Link]/shared_prefs/YOUR_FILE_NAME.xml
Breaking this down:
/data/data/[Link]/ — your app's private sandbox (sandboxed by Android, other apps
cannot read this)
shared_prefs/ — subdirectory created automatically by the framework
YOUR_FILE_NAME.xml — the file you named when
calling getSharedPreferences("YOUR_FILE_NAME", ...)
The XML Format
Here is a real SharedPreferences XML file:
1 <?xml version='1.0' encoding='utf-8' standalone='yes' ?>
2 <map>
3 <boolean name="dark_mode" value="true" />
4 <string name="username">john_doe</string>
5 <int name="launch_count" value="42" />
6 <long name="last_login_ms" value="1710000000000" />
7 <float name="text_scale" value="1.5" />
8 <set name="selected_tags">
9 <string>android</string>
10 <string>kotlin</string>
11 <string>compose</string>
12 </set>
13 </map>
Key observations:
It's valid XML with a root <map> element
Each key-value pair is one XML element
The element tag name is the data type
The key is the name attribute
The value is either the value attribute (primitives) or child elements ( Set<String> )
The file is human-readable — useful for debugging, but also a security concern (readable by root
on rooted devices)
File Naming
1 // File will be: /shared_prefs/user_settings.xml
2 getSharedPreferences("user_settings", Context.MODE_PRIVATE)
3
4 // File will be: /shared_prefs/[Link].app_preferences.xml
5 getSharedPreferences("[Link].app_preferences", Context.MODE_PRIVATE)
6
7 // File will be: /shared_prefs/[Link] (activity class name)
8 // Called from an Activity only:
9 getPreferences(Context.MODE_PRIVATE) // uses Activity class name as filename
10
11 // File will be: /shared_prefs/com.yourapp_preferences.xml
12 [Link](context) // standard settings file
The File is Loaded Entirely
This is critical: the entire XML file is loaded into memory at once. There is no lazy loading of
individual keys. When you call getSharedPreferences() for the first time, the framework reads the
entire XML file and parses it into a HashMap.
This has two implications:
1. All keys are available immediately once loaded (reads are instant from memory)
2. Large preference files slow down first access — loading 100KB of XML on the main thread is a
performance problem
Chapter 4: How SharedPreferences Loads Into Memory
The Lazy Loading Mechanism
SharedPreferences uses lazy initialization. The file is NOT loaded when you
call getSharedPreferences() . It starts loading in the background at that point, but the actual file
parsing may not be complete immediately.
Here is the sequence:
1 getSharedPreferences("prefs", MODE_PRIVATE) is called
2 │
3 ▼
4 Android checks if this file is already cached in memory
5 │
6 ┌────┴────┐
7 YES NO
8 │ │
9 ▼ ▼
10 Returns Creates SharedPreferencesImpl object
11 cached Starts background thread to read XML file
12 instance Returns SharedPreferencesImpl immediately (file may not be loaded yet!)
13 │
14 ▼
15 You call getString("key", null)
16 │
17 ▼
18 [Link]() is called internally
19 │
20 ▼
21 IF the background thread is still reading the file:
22 THE CURRENT THREAD BLOCKS AND WAITS ← potential ANR if on main thread!
23 │
24 ▼
25 Once loaded: HashMap lookup → returns value instantly
The dangerous part: if you call getSharedPreferences() and immediately access a value, and the file
hasn't finished loading yet, your thread blocks. On the main thread, this is an ANR source.
The In-Memory Cache
Once loaded, SharedPreferences keeps the entire HashMap in memory for the lifetime of the process.
This is why reads after the initial load are instant — they're just HashMap lookups, no disk I/O.
1 First read: disk → parse XML → populate HashMap → return value (slow)
2 All subsequent reads: HashMap lookup → return value (instant,
microseconds)
Multiple Instances Are the Same Object
Android caches SharedPreferences instances. If you call getSharedPreferences("prefs",
MODE_PRIVATE) from two different places, you get the same underlying object with the same
HashMap.
1 val prefs1 = [Link]("app", MODE_PRIVATE)
2 val prefs2 = [Link]("app", MODE_PRIVATE)
3 // prefs1 === prefs2 — they are literally the same object in memory
PART II — SYSTEM LEVEL INTERNALS
Chapter 5: The In-Memory HashMap — How Reads Are Instant
The heart of SharedPreferences is a HashMap<String, Object> stored in SharedPreferencesImpl .
Every key-value pair from the XML file is loaded into this map.
1 // Simplified from AOSP [Link]
2 final class SharedPreferencesImpl implements SharedPreferences {
3 private Map<String, Object> mMap; // ← the in-memory store
4 private final Object mLock = new Object();
5 private boolean mLoaded = false;
6 // ...
7 }
The Read Path
When you call [Link]("username", null) :
1 // Simplified AOSP source
2 public String getString(String key, @Nullable String defValue) {
3 synchronized (mLock) {
4 awaitLoadedLocked(); // blocks if file not loaded yet
5 String v = (String) [Link](key); // [Link]() — O(1)
6 return v != null ? v : defValue;
7 }
8 }
The synchronized (mLock) is a lock to prevent concurrent reads from racing with ongoing writes. For
pure reads, this lock is very briefly held.
This is why reads feel instant — it's just [Link](key) once loaded.
Thread Safety of Reads
SharedPreferences uses the mLock object to synchronize reads and writes. This means:
Multiple threads can read simultaneously (they all wait for the lock briefly)
A read during a write will wait for the write to update the HashMap
This is a single-writer, many-readers pattern with a simple mutex
Chapter 6: The Editor — How Writes Work Internally
Writing to SharedPreferences requires going through the Editor interface. Here is what happens
internally:
Step 1: Create an Editor
1 val editor = [Link]()
This creates an EditorImpl object with its own temporary HashMap — a copy of the current
preferences for staging changes.
1 // Simplified AOSP EditorImpl
2 public final class EditorImpl implements Editor {
3 private final Object mEditorLock = new Object();
4 private final Map<String, Object> mModified = new HashMap<>(); // ← staging area
5 private boolean mClear = false;
6 // ...
7 }
Step 2: Put Values
1 [Link]("username", "John")
2 [Link]("dark_mode", true)
These calls simply add to mModified — no disk I/O, no file access, just [Link]().
1 public Editor putString(String key, @Nullable String value) {
2 synchronized (mEditorLock) {
3 [Link](key, value); // just a HashMap put — instant
4 return this;
5 }
6 }
Step 3: commit() or apply()
This is where the write actually happens. The Editor applies mModified on top of the main mMap and
schedules a disk write.
1 mModified (staging): { username: "John", dark_mode: true }
2 mMap (current in-memory): { username: "Alice", launch_count: 5 }
3
4 After merge:
5 mMap (updated in-memory): { username: "John", dark_mode: true, launch_count: 5 }
The merge is called commitToMemory() internally. After this, any new reads immediately return the
new values — even before the disk write completes.
Chapter 7: commit() vs apply() — The Deep Difference
This is the most important distinction in SharedPreferences. Most developers use apply() without
understanding what it actually does.
commit() — Synchronous Write
1 val success = [Link]() // returns Boolean
What happens internally:
1 [Link]() is called on calling thread
2 │
3 ▼
4 commitToMemory() — updates in-memory HashMap synchronously
5 │
6 ▼
7 writeToDiskRunnable runs ON THE CALLING THREAD
8 │
9 ▼
10 Serializes HashMap to XML string
11 │
12 ▼
13 Writes XML to .[Link] (backup file)
14 │
15 ▼
16 Renames .[Link] to .xml (atomic rename)
17 │
18 ▼
19 Calls fsync() — waits for kernel to confirm physical disk write
20 │
21 ▼
22 Returns true (success) or false (failure) to your code
Key facts about commit():
Blocks the calling thread until disk write completes
Returns true on success, false on failure — you know if it worked
If called on the main thread: instant ANR risk on slow devices
Safe to call on a background thread
Guarantees data is on disk when it returns
apply() — "Asynchronous" Write (The Dangerous One)
1 [Link]() // returns void
What happens internally:
1 [Link]() is called
2 │
3 ▼
4 commitToMemory() — updates in-memory HashMap SYNCHRONOUSLY
5 │
6 ▼ (in-memory update is immediate — reads see new values instantly)
7 │
8 ▼
9 Creates awaitCommit Runnable (a CountDownLatch)
10 │
11 ▼
12 [Link](awaitCommit) ← THIS IS THE TRAP
13 │
14 ▼
15 Schedules writeToDiskRunnable on background thread
16 │
17 ▼
18 apply() returns immediately — your code continues
19 │
20 ▼ (somewhere later, on background thread)
21 │
22 ▼
23 writeToDiskRunnable runs: serialize XML → write → fsync()
24 │
25 ▼
26 CountDownLatch counts down (signals completion)
27 │
28 ▼
29 [Link](awaitCommit)
apply() looks fast — it returns immediately. But the awaitCommit Runnable added to QueuedWork is
the hidden trap.
The apply() Return Value Problem
apply() returns void . You have no way to know if the write succeeded or failed. If the disk is full, if
there's a permissions error, if the device runs out of battery mid-write — apply() gives you
absolutely no indication. Your data is silently lost.
1 [Link]()
2 // Did it work? You have no idea. No return value, no callback, no error.
Chapter 8: The QueuedWork Trap — The Hidden ANR Mechanism
This is the most important chapter in this book for understanding SharedPreferences dangers. This
explains the root cause of thousands of ANRs in production apps.
What Is QueuedWork?
QueuedWork is an Android framework class ( [Link] ) that maintains a list of
pending asynchronous work items that must complete before certain lifecycle events.
The Trap: waitToFinish()
QueuedWork has a method called waitToFinish() . The Android framework calls this method
automatically at the following lifecycle points:
1 [Link]() → [Link]() →
[Link]()
2 [Link]() → [Link]() →
[Link]()
3 [Link]() → [Link]() →
[Link]()
4 [Link]() → [Link]() →
[Link]()
5 [Link]() ends → [Link]()
These are all called on the main thread.
When waitToFinish() is called, it blocks the main thread until every item in the QueuedWork finisher
list is complete.
When you call apply() , it adds awaitCommit (a CountDownLatch waiter) to QueuedWork 's finisher
list. This means:
1 User rotates device → [Link]() triggered
2 │
3 ▼
4 [Link]() runs ON MAIN THREAD
5 │
6 ▼
7 [Link]() is called
8 │
9 ▼
10 Main thread BLOCKS waiting for pending apply() disk writes to complete
11 │
12 ▼
13 If disk write takes 200ms → UI frozen for 200ms
14 If disk write takes 2 seconds (slow device, large file) → ANR!
The Stack Trace You'll See in Crash Reports
1 "main" prio=5 tid=1 WAIT
2 at [Link](Native Method)
3 at [Link]([Link])
4 at [Link]([Link])
5 at [Link]$1200([Link])
6 at [Link]$[Link]([Link])
7 at [Link]([Link])
8 at [Link]([Link])
9 at [Link]([Link])
Notice: the stack trace does NOT mention SharedPreferences. It points
at [Link] . This is why these ANRs are so hard to diagnose — you're
looking for a SharedPreferences call but the stack trace shows Activity lifecycle code.
The Frequency of the Problem
The waitToFinish() is called:
Every time an Activity stops — this happens on EVERY screen navigation, EVERY rotation, EVERY
time the user presses Home
Every time a Service starts or stops
On some Android versions, even during onPause()
If you call apply() even once per screen — which is very common — the ANR risk is present on
every single screen transition.
The fsync() Factor
The actual time taken is determined by fsync() — the Linux system call that flushes the kernel's
write buffer to physical storage. fsync() time varies enormously:
Fast device (SSD-equivalent flash): 1–5ms
Average device: 10–50ms
Slow/cheap device: 100–500ms
Device under heavy I/O load: 1000ms+
On a device under load or with slow storage, waitToFinish() waiting for fsync() to complete can
easily exceed the 5-second ANR threshold.
Android 8.0 Optimization
Android 8.0 (Oreo) partially improved this by changing waitToFinish() to actively process the work
queue rather than just waiting, reducing but not eliminating the block time. The fundamental problem
— main thread blocking on disk I/O — remains.
Why You Cannot Fix This With apply()
Many developers believe using apply() instead of commit() solves the ANR problem. It does NOT.
The QueuedWork mechanism means you will always have some main-thread block. The only real fix is
to not use SharedPreferences at all, or to use it only for non-critical data where brief pauses are
acceptable.
Chapter 9: The Listener System — How Change Callbacks Work
SharedPreferences provides OnSharedPreferenceChangeListener to be notified when values change.
How It Works Internally
SharedPreferences uses a WeakHashMap to store listeners:
1 // Simplified AOSP
2 private final WeakHashMap<OnSharedPreferenceChangeListener, Object> mListeners =
3 new WeakHashMap<>();
The WeakHashMap means listeners can be garbage collected if you don't hold a strong reference to
them — a common source of bugs.
Registration and Callback Timing
1 val listener = [Link] { prefs, key ->
2 // Called on the MAIN THREAD
3 // key is the changed key (or null on clear())
4 when (key) {
5 "dark_mode" -> updateTheme([Link](key, false))
6 "language" -> updateLanguage([Link](key, "en") ?: "en")
7 }
8 }
9
10 [Link](listener)
11 // Later:
12 [Link](listener)
The callback fires:
After apply() updates the in-memory map (before disk write)
After commit() completes (after disk write)
On the main thread always
PART III — COMPLETE API REFERENCE
Chapter 10: Getting a SharedPreferences Instance
There are three ways to get a SharedPreferences instance. Each has a different scope.
Method 1: getSharedPreferences() — Most Common
1 val prefs = [Link]("file_name", Context.MODE_PRIVATE)
Use from any Context(Activity, Service, Application, etc.)
"file_name" becomes the XML filename: shared_prefs/file_name.xml
Shared across your entire app — same file, same object, anywhere you use the same name
This is what you should use for app-wide preferences
Method 2: getPreferences() — Activity-Specific
1 // Only callable from an Activity
2 val prefs = [Link](Context.MODE_PRIVATE)
Automatically uses the Activity's class name as the file name
MainActivity → shared_prefs/[Link]
Only use this for data specific to one Activity
Method 3: [Link]() — Standard
Settings
1 val prefs = [Link](context)
Returns the app's default shared preferences file
File name: _preferences.xml (e.g., com.wallstreet_preferences.xml )
This is what Android's Preferences UI framework uses automatically
Use this when your data relates to the app's Settings screen
Naming Best Practice
Use your package name as a prefix to avoid conflicts when your preferences file name might collide
with other app files:
1 // Good — uniquely identifies your file
2 [Link]("[Link].user_settings", Context.MODE_PRIVATE)
3
4 // OK — simple but could collide in unusual scenarios
5 [Link]("user_settings", Context.MODE_PRIVATE)
Chapter 11: All Access Modes Explained
The second parameter to getSharedPreferences() is the mode. Understanding each is important.
1 getSharedPreferences("name", Context.MODE_PRIVATE) // ← always use this
2 getSharedPreferences("name", Context.MODE_WORLD_READABLE) // deprecated, dangerous
3 getSharedPreferences("name", Context.MODE_WORLD_WRITEABLE) // deprecated, dangerous
4 getSharedPreferences("name", Context.MODE_MULTI_PROCESS) // use with caution
MODE_PRIVATE (Always Use This)
1 Context.MODE_PRIVATE // value = 0
Only your app can read or write this file
This is the only mode you should ever use in modern apps
All other modes are either deprecated or have serious security implications
MODE_WORLD_READABLE and MODE_WORLD_WRITEABLE (Never Use)
1 Context.MODE_WORLD_READABLE // DEPRECATED since API 17
2 Context.MODE_WORLD_WRITEABLE // DEPRECATED since API 17
Allowed other apps to read/write your preferences file
Removed behavior in Android 7.0 (API 24) — throws SecurityException if used
Never use these. They are a major security vulnerability even on older devices.
MODE_MULTI_PROCESS (Legacy, Unreliable)
1 Context.MODE_MULTI_PROCESS // value = 4
Intended for accessing SharedPreferences across multiple processes
Deprecated since API 23
Was unreliable and prone to data corruption even when it worked
If you need multi-process data sharing, use ContentProvider or a database instead
Chapter 12: All Data Types You Can Store
SharedPreferences natively supports 6 types. All are stored as XML elements.
Type Method XML Default
Boolean putBoolean() / getBoolean() <boolean name="k" false
value="true"/>
Int putInt() / getInt() <int name="k" value="42"/> 0
Long putLong() / getLong() <long name="k" 0L
value="1234567"/>
Float putFloat() / getFloat() <float name="k" value="1.5"/> 0.0f
String putString() / getString() <string name="k">val</string> null
Set<String> putStringSet() / getStringSet() <set name="k"> null
<string>v</string></set>
Note: There is no native support for Double .
Use Long with [Link]() / [Link]() as a workaround.
Storing Non-Native Types
Double:
1 // Store
2 [Link]("latitude", [Link]())
3
4 // Read
5 val latitude = [Link]([Link]("latitude", [Link]()))
Enum:
1 enum class ThemeMode { LIGHT, DARK, SYSTEM }
2
3 // Store
4 [Link]("theme", [Link])
5
6 // Read
7 val theme = [Link]([Link]("theme", "SYSTEM") ?: "SYSTEM")
Data class (serialized to JSON):
1 // Requires [Link] or Gson
2 @Serializable
3 data class UserProfile(val name: String, val email: String)
4
5 // Store
6 [Link]("profile", [Link](profile))
7
8 // Read
9 val profile = [Link]<UserProfile>(
10 [Link]("profile", null) ?: return
11 )
Chapter 13: Reading Data — Every Method
All read methods follow the same pattern: getType(key, defaultValue)
1 // Boolean
2 val isDark: Boolean = [Link]("dark_mode", false)
3
4 // Int
5 val count: Int = [Link]("launch_count", 0)
6
7 // Long
8 val timestamp: Long = [Link]("last_login", 0L)
9
10 // Float
11 val scale: Float = [Link]("text_scale", 1.0f)
12
13 // String (nullable — key might not exist)
14 val username: String? = [Link]("username", null)
15
16 // String with non-null default
17 val language: String = [Link]("language", "en") ?: "en"
18
19 // Set<String> (nullable)
20 val tags: Set<String>? = [Link]("tags", null)
21
22 // Set<String> with default
23 val tags: Set<String> = [Link]("tags", emptySet()) ?: emptySet()
Read All Keys
1 // Get the entire Map — snapshot of current preferences
2 val allPrefs: Map<String, *> = [Link]
3 // Returns Map<String, Any?> — values are Any (Boolean, Int, String, etc.)
4
5 // Iterate all entries
6 for ((key, value) in [Link]) {
7 println("$key = $value (${value?.javaClass?.simpleName})")
8 }
The Default Value Matters
The second parameter to every get method is the default value — returned when the key does not
exist in the file. Choose defaults carefully:
1 // Bad default — null means callers must null-check everywhere
2 val name: String? = [Link]("name", null)
3
4 // Better — provide a sensible fallback
5 val name: String = [Link]("name", "Guest") ?: "Guest"
6
7 // The ?: "Guest" is needed because getString is nullable in Kotlin even with a
default
8 // (due to Java interop — the annotation says it could be null)
Chapter 14: Writing Data — Every Method
The Editor Pattern
All writes go through [Link] . You must call apply() or commit() at the end —
without it, nothing is written.
1 val editor = [Link]()
2
3 // All the put methods:
4 [Link]("dark_mode", true)
5 [Link]("launch_count", 5)
6 [Link]("last_login", [Link]())
7 [Link]("text_scale", 1.5f)
8 [Link]("username", "john_doe")
9 [Link]("favorite_tags", setOf("kotlin", "android"))
10
11 // MUST call one of these — nothing is written without it:
12 [Link]() // async (with hidden dangers — see Chapter 8)
13 // OR
14 [Link]() // sync (safe on background thread)
Method Chaining (Fluent API)
All Editor methods return this — you can chain them:
1 [Link]()
2 .putBoolean("dark_mode", true)
3 .putString("language", "en")
4 .putInt("launch_count", 1)
5 .apply()
This is cleaner and equivalent — the chain creates one Editor and applies all changes at once.
Kotlin Extension Function (apply{} block)
Kotlin's standard library provides an extension for cleaner syntax:
1 // Using Kotlin's edit extension — cleaner than chaining
2 [Link] {
3 putBoolean("dark_mode", true)
4 putString("language", "en")
5 putInt("launch_count", 1)
6 // apply() is called automatically when the block ends
7 }
8
9 // This uses commit() instead of apply()
10 [Link](commit = true) {
11 putBoolean("dark_mode", true)
12 }
The edit { } extension function is from [Link]:core-ktx . It calls apply() by default.
Use edit(commit = true) { } for synchronous commits.
Chapter 15: Deleting Data
Remove a Single Key
1 [Link] {
2 remove("username")
3 }
4 // After this, [Link]("username", null) returns null
Clear All Data
1 [Link] {
2 clear()
3 }
4 // All keys removed — the XML file will be nearly empty: <map></map>
Check Before Remove (Defensive)
1 if ([Link]("username")) {
2 [Link] { remove("username") }
3 }
4 // But this is usually unnecessary — remove() on a non-existent key does nothing
Partial Clear (Selective Logout)
1 [Link] {
2 // Keep device-level settings, clear user-specific data
3 remove("user_id")
4 remove("auth_token")
5 remove("user_email")
6 // dark_mode, language, etc. remain
7 }
Chapter 16: Checking If a Key Exists
1 // Check if a key exists
2 val hasUsername: Boolean = [Link]("username")
3
4 // Common pattern — do something only if key doesn't exist
5 if () {
6 showOnboarding()
7 [Link] { putBoolean("onboarding_shown", true) }
8 }
9
10 // Alternative — use the default value approach
11 val isFirstLaunch: Boolean = 
12 if (isFirstLaunch) {
13 [Link] { putBoolean("launched_before", true) }
14 }
Chapter 17: Change Listeners
Registering a Listener
1 class MyActivity : AppCompatActivity() {
2
3 private val prefsListener = [Link] {
prefs, key ->
4 when (key) {
5 "dark_mode" -> {
6 val isDark = [Link](key, false)
7 applyTheme(isDark)
8 }
9 "language" -> {
10 val lang = [Link](key, "en") ?: "en"
11 applyLanguage(lang)
12 }
13 }
14 }
15
16 override fun onResume() {
17 [Link]()
18 [Link](prefsListener)
19 }
20
21 override fun onPause() {
22 [Link]()
23 [Link](prefsListener)
24 }
25 }
The WeakReference Trap
SharedPreferences stores listeners in a WeakHashMap . If you create a listener as a lambda and don't
store a reference to it, the garbage collector removes it and you stop receiving callbacks:
1 // ❌ WRONG — listener can be garbage collected immediately
2 [Link] { prefs, key ->
3 // This may never be called — no strong reference held
4 }
✅
5
6 // CORRECT — store reference as a member variable
7 private val listener = [Link] { prefs,
key ->
8 // This is held strongly — will receive callbacks
9 }
10 [Link](listener)
Always Unregister
Always unregister in the opposite lifecycle method to prevent leaks and ghost callbacks:
1 // Register in onStart, unregister in onStop
2 override fun onStart() { [Link](listener) }
3 override fun onStop() { [Link](listener)
}
4
5 // OR register in onResume, unregister in onPause
6 override fun onResume() { [Link](listener) }
7 override fun onPause() { [Link](listener)
}
PART IV — ARCHITECTURE & PATTERNS
Chapter 18: SharedPreferences in Clean Architecture
If you still use SharedPreferences (legacy code, migration path), wrap it properly.
Layer Structure
1 PRESENTATION: ViewModel — uses use cases
2 DOMAIN: Use Cases + Repository Interface
3 DATA: Repository Implementation wrapping SharedPreferences
The Interface (Domain Layer)
1 // domain/repository/[Link]
2 interface UserPreferencesRepository {
3 fun isDarkMode(): Boolean
4 fun setDarkMode(enabled: Boolean)
5 fun getLanguage(): String
6 fun setLanguage(lang: String)
7 fun isOnboardingCompleted(): Boolean
8 fun setOnboardingCompleted()
9 fun clearUserData()
10 }
The Implementation (Data Layer)
1 // data/repository/[Link]
2 class UserPreferencesRepositoryImpl(
3 private val prefs: SharedPreferences
4 ) : UserPreferencesRepository {
5
6 companion object {
7 private const val KEY_DARK_MODE = "dark_mode"
8 private const val KEY_LANGUAGE = "language"
9 private const val KEY_ONBOARDING = "onboarding_completed"
10 }
11
12 override fun isDarkMode(): Boolean =
13 [Link](KEY_DARK_MODE, false)
14
15 override fun setDarkMode(enabled: Boolean) =
16 [Link] { putBoolean(KEY_DARK_MODE, enabled) }
17
18 override fun getLanguage(): String =
19 [Link](KEY_LANGUAGE, "en") ?: "en"
20
21 override fun setLanguage(lang: String) =
22 [Link] { putString(KEY_LANGUAGE, lang) }
23
24 override fun isOnboardingCompleted(): Boolean =
25 [Link](KEY_ONBOARDING, false)
26
27 override fun setOnboardingCompleted() =
28 [Link] { putBoolean(KEY_ONBOARDING, true) }
29
30 override fun clearUserData() = [Link] {
31 remove(KEY_DARK_MODE)
32 remove(KEY_LANGUAGE)
33 // Keep KEY_ONBOARDING — device-level flag
34 }
35 }
Chapter 19: SharedPreferences in Kotlin — Modern Wrappers
Property Delegate Wrapper
Kotlin's property delegates let you access SharedPreferences as if they were regular properties — no
boilerplate:
1 // Generic delegate for any SharedPreferences value
2 class SharedPreference<T>(
3 private val prefs: SharedPreferences,
4 private val key: String,
5 private val default: T
6 ) {
7 @Suppress("UNCHECKED_CAST")
8 operator fun getValue(thisRef: Any?, property: KProperty<*>): T {
9 return [Link][key] as? T ?: default
10 }
11
12 operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
13 [Link] {
14 when (value) {
15 is Boolean -> putBoolean(key, value)
16 is Int -> putInt(key, value)
17 is Long -> putLong(key, value)
18 is Float -> putFloat(key, value)
19 is String -> putString(key, value)
20 else -> throw IllegalArgumentException("Unsupported type:
${value?.javaClass}")
21 }
22 }
23 }
24 }
25
26 // Usage — preferences as properties
27 class AppSettings(prefs: SharedPreferences) {
28 var isDarkMode: Boolean by SharedPreference(prefs, "dark_mode", false)
29 var language: String by SharedPreference(prefs, "language", "en")
30 var launchCount: Int by SharedPreference(prefs, "launch_count", 0)
31 }
32
33 // Use it:
34 val settings = AppSettings(prefs)
35 [Link] = true // writes to SharedPreferences automatically
36 val dark = [Link] // reads from SharedPreferences automatically
Extension Functions
1 // Extension to simplify write syntax (already in AndroidX KTX)
2 fun [Link](
3 commit: Boolean = false,
4 action: [Link].() -> Unit
5 ) {
6 val editor = edit()
7 action(editor)
8 if (commit) [Link]() else [Link]()
9 }
10
11 // Usage
12 [Link] {
13 putBoolean("dark_mode", true)
14 putString("language", "en")
15 }
Inline Operator Functions
1 // Access preferences like a map
2 operator fun [Link](key: String) = [Link](key)
3
4 operator fun [Link](key: String, value: Any?) {
5 when (value) {
6 is Boolean -> putBoolean(key, value)
7 is Int -> putInt(key, value)
8 is Long -> putLong(key, value)
9 is Float -> putFloat(key, value)
10 is String -> putString(key, value)
11 is Set<*> -> @Suppress("UNCHECKED_CAST") putStringSet(key, value as
Set<String>)
12 null -> remove(key)
13 }
14 }
15
16 // Usage
17 [Link] {
18 this["dark_mode"] = true
19 this["username"] = "john"
20 this["count"] = 42
21 }
Chapter 20: SharedPreferences with Koin DI
1 // di/[Link]
2 val preferencesModule = module {
3
4 // SharedPreferences instance
5 single<SharedPreferences> {
6 androidContext().getSharedPreferences(
7 "[Link]",
8 Context.MODE_PRIVATE
9 )
10 }
11
12 // Repository
13 single<UserPreferencesRepository> {
14 UserPreferencesRepositoryImpl(get())
15 }
16
17 // Use Cases
18 factory { GetDarkModeUseCase(get()) }
19 factory { SetDarkModeUseCase(get()) }
20 factory { IsOnboardingCompletedUseCase(get()) }
21
22 // ViewModel
23 viewModel {
24 SettingsViewModel(get(), get())
25 }
26 }
Why single for SharedPreferences
1 // ✅ single — one instance, same HashMap everywhere, correct
2 single<SharedPreferences> { [Link]("prefs", MODE_PRIVATE) }
❌
3
4 // factory — multiple instances pointing to same file
5 // Not data corruption like DataStore, but wasteful and inconsistent
6 factory<SharedPreferences> { [Link]("prefs", MODE_PRIVATE) }
Unlike DataStore, multiple SharedPreferences instances pointing to the same file won't corrupt data
(Android caches them at the framework level), but using single is still the correct pattern — it's
explicit, efficient, and consistent.
Chapter 21: Multi-Process SharedPreferences
If your app has multiple processes (unusual but possible with services declared
with android:process ), SharedPreferences behaves unexpectedly.
The Problem
Each process has its own in-memory cache of the HashMap. When Process A writes a value, Process
B's cache is NOT updated. Process B will return the old value until it reloads the file.
Process A writes: dark_mode = true
✅
1
→ HashMap in Process A updated
✅
2
→ XML file updated
❌
3
4 → HashMap in Process B: still has dark_mode = false
MODE_MULTI_PROCESS (Deprecated "Fix")
Context.MODE_MULTI_PROCESS was supposed to fix this by reloading the file on every access. But it
was unreliable and deprecated in API 23 because it still didn't guarantee consistency.
The Right Solution for Multi-Process
If you genuinely need cross-process data sharing:
Use a ContentProvider
Use a bound Service with a Messenger
Use Room (SQLite handles multi-process safely)
Use DataStore with proper multi-process configuration (DataStore 1.1.x+)
PART V — DANGERS & BEST PRACTICES
Chapter 22: The 7 Dangers of SharedPreferences
Danger 1: ANR from QueuedWork (Most Dangerous)
As explained in Chapter 8, apply() blocks the main thread during Activity and Service lifecycle
events via [Link]() .
1 Risk level: HIGH
2 Frequency: Every app transition if you call apply()
3 Symptom: Frozen UI, ANR in crash reports
4 Fix: Migrate to DataStore, or call commit() on a background thread
Danger 2: Silent Write Failures
apply() returns void. Write failures are swallowed silently.
1 [Link] { putString("user_data", importantData) }
2 .apply()
3 // Did it succeed? You will NEVER know.
1 Risk level: MEDIUM
2 Frequency: Rare — disk full, hardware error
3 Symptom: Data disappears without error
4 Fix: Use commit() and check return value, or use DataStore
Danger 3: No Type Safety
1 [Link]().putBoolean("user_id", true) // wrong type
2 val id: Int = [Link]("user_id", 0) // ClassCastException at runtime
1 Risk level: MEDIUM
2 Frequency: Common in large codebases with string keys scattered everywhere
3 Symptom: ClassCastException crash at runtime
4 Fix: Centralize all keys as constants, use a typed wrapper
Danger 4: The WeakReference Listener Trap
1 [Link] { _, _ ->
2 updateUI() // This lambda has no strong reference — GC removes it
3 }
4 // Listener may never fire
1 Risk level: LOW-MEDIUM
2 Frequency: Easy to hit for beginners
3 Symptom: Listener stops firing randomly
4 Fix: Store listener as member variable
Danger 5: Main Thread Loading Block
1 // On main thread, during app startup:
2 val prefs = [Link]("prefs", MODE_PRIVATE)
3 val name = [Link]("name", null) // may block if file not loaded yet
1 Risk level: MEDIUM
2 Frequency: On first access, especially during app startup
3 Symptom: Slow startup on older/cheaper devices
4 Fix: Move first access off main thread, or use DataStore
Danger 6: Unprotected Sensitive Data
1 // BAD — stored as plain text in readable XML
2 [Link] { putString("auth_token", "eyJhbGciOiJIUzI1...") }
3 // Readable by root on rooted devices
4 // Readable by ADB on debug builds
1 Risk level: HIGH for sensitive data
2 Frequency: Common mistake
3 Symptom: Data exposed on rooted/debuggable devices
4 Fix: Use EncryptedSharedPreferences or Keystore
Danger 7: XML Corruption
Unlike DataStore's atomic rename, SharedPreferences uses a backup file approach that can fail. If
power is lost at the wrong moment, the XML file can become invalid — partially written, non-parseable
XML.
1 Risk level: LOW
2 Frequency: Rare — requires power loss at exact moment of write
3 Symptom: ClassCastException or NumberFormatException on read
4 Fix: Wrap reads in try-catch, or use DataStore
Chapter 23: Common Mistakes & How to Avoid Them
Mistake 1: Using commit() on the Main Thread
1 // ❌ WRONG — blocks UI thread
2 [Link]().putBoolean("dark_mode", true).commit()
✅ CORRECT — use apply() or commit() on background thread
3
4 //
5 [Link] { putBoolean("dark_mode", true) } // uses apply()
✅ CORRECT — commit() on background thread if you need confirmation
6
7 //
8 [Link]([Link]) {
9 val success = [Link]()
10 .putBoolean("dark_mode", true)
11 .commit()
12 }
Mistake 2: Forgetting to Call apply() or commit()
1 // ❌ WRONG — nothing is saved
2 val editor = [Link]()
3 [Link]("username", "John")
4 // apply() or commit() never called — data discarded!
✅
5
6 // CORRECT
7 [Link] {
8 putString("username", "John")
9 // apply() called automatically by the extension
10 }
Mistake 3: Lambda Listener Not Held
1 // ❌ WRONG — garbage collected
2 [Link] { _, key ->
3 println("Changed: $key")
4 }
✅ CORRECT
5
6 //
7 private val listener = [Link] { _, key ->
8 println("Changed: $key")
9 }
10 // In onResume:
11 [Link](listener)
12 // In onPause:
13 [Link](listener)
Mistake 4: String Keys Scattered Everywhere
1 // ❌ WRONG — typos cause silent bugs
2 [Link]("darkMode", false) // somewhere
3 [Link] { putBoolean("dark_mode", true) } // different spelling! Different key!
✅
4
5 // CORRECT — constants in one place
6 object PrefsKeys {
7 const val DARK_MODE = "dark_mode"
8 const val LANGUAGE = "language"
9 }
10
11 [Link](PrefsKeys.DARK_MODE, false)
12 [Link] { putBoolean(PrefsKeys.DARK_MODE, true) }
Mistake 5: Modifying a Set In-Place
1 // ❌WRONG — mutating the returned set is undefined behavior
2 val tags = [Link]("tags", null)
3 tags?.add("new_tag") // This MAY or MAY NOT be saved — implementation detail
4 [Link] { putStringSet("tags", tags) }
✅ CORRECT — create a new set
5
6 //
7 val tags = [Link]("tags", null) ?: emptySet()
8 val newTags = [Link]().also { [Link]("new_tag") }
9 [Link] { putStringSet("tags", newTags) }
The Android documentation explicitly warns: "Note that you must not modify the set instance
returned, as the store does not guarantee that its contents will not be modified."
Mistake 6: Large Data in SharedPreferences
1 // ❌ WRONG — entire preferences file loaded into memory
2 // If this string is 100KB, your whole prefs file becomes heavy
3 [Link] { putString("trade_history_json", hugeJson) }
✅
4
5 // CORRECT — large data belongs in Room or files
6 // Store only simple flags/settings in SharedPreferences
Mistake 7: Different File Names for Same Data
1 // Activity A:
2 [Link]("settings", MODE_PRIVATE).getBoolean("dark_mode", false)
3
4 // Activity B — different file name!
5 [Link]("app_settings", MODE_PRIVATE).getBoolean("dark_mode",
false)
6 // This reads from a DIFFERENT file — always returns default!
Chapter 24: Security Considerations
What's Readable
On a rooted device or with ADB on a debug build, your SharedPreferences XML files are readable by:
Other apps with root privileges
Any tool connected via ADB to a debuggable app
1 # On a rooted device or via ADB (debug builds only):
2 adb shell run-as [Link] cat /data/data/[Link]/shared_prefs/[Link]
3 # Output: your entire preferences file in plain XML
What NOT to Store in SharedPreferences
1 // ❌ NEVER store these in plain SharedPreferences:
2 [Link] { putString("password", userPassword) } // plain text password
3 [Link] { putString("auth_token", jwtToken) } // auth token
4 [Link] { putString("credit_card", cardNumber) } // payment data
5 [Link] { putString("api_key", secretApiKey) } // API secrets
6 [Link] { putString("ssn", socialSecurityNumber) } // personal identity
EncryptedSharedPreferences (The Secure Alternative)
For sensitive data that you must store locally, use EncryptedSharedPreferences from the AndroidX
Security library:
1 // [Link]
2 implementation("[Link]:security-crypto:1.1.0-alpha06")
1 import [Link]
2 import [Link]
3
4 // Create master key (backed by Android Keystore)
5 val masterKey = [Link](context)
6 .setKeyScheme([Link].AES256_GCM)
7 .build()
8
9 // Create encrypted SharedPreferences
10 val encryptedPrefs = [Link](
11 context,
12 "secret_prefs", // file name
13 masterKey,
14 [Link].AES256_SIV, // key
encryption
15 [Link].AES256_GCM // value
encryption
16 )
17
18 // Use exactly like regular SharedPreferences
19 [Link] { putString("auth_token", token) }
20 val token = [Link]("auth_token", null)
How it works internally:
Keys are encrypted with AES-256-SIV (deterministic encryption — same key produces same
ciphertext, so the HashMap can look them up)
Values are encrypted with AES-256-GCM (authenticated encryption)
The master key is stored in the Android Keystore — hardware-backed on supported devices
The XML file contains only encrypted bytes — no readable values
PART VI — REAL-WORLD EXAMPLES
Chapter 25: Real-World Patterns — 10 Complete Examples
Example 1: App Settings Manager
1 class AppSettings(context: Context) {
2
3 private val prefs = [Link](
4 "[Link]", Context.MODE_PRIVATE
5 )
6
7 companion object {
8 private const val KEY_DARK_MODE = "dark_mode"
9 private const val KEY_LANGUAGE = "language"
10 private const val KEY_NOTIFICATIONS = "notifications_enabled"
11 private const val KEY_FONT_SIZE = "font_size_sp"
12 private const val KEY_ONBOARDING = "onboarding_completed"
13 }
14
15 // Dark mode
16 var isDarkMode: Boolean
17 get() = [Link](KEY_DARK_MODE, false)
18 set(value) = [Link] { putBoolean(KEY_DARK_MODE, value) }
19
20 // Language
21 var language: String
22 get() = [Link](KEY_LANGUAGE, "en") ?: "en"
23 set(value) = [Link] { putString(KEY_LANGUAGE, value) }
24
25 // Notifications
26 var notificationsEnabled: Boolean
27 get() = [Link](KEY_NOTIFICATIONS, true)
28 set(value) = [Link] { putBoolean(KEY_NOTIFICATIONS, value) }
29
30 // Font size
31 var fontSizeSp: Int
32 get() = [Link](KEY_FONT_SIZE, 14)
33 set(value) = [Link] { putInt(KEY_FONT_SIZE, value) }
34
35 // Onboarding
36 val isOnboardingCompleted: Boolean
37 get() = [Link](KEY_ONBOARDING, false)
38
39 fun markOnboardingCompleted() = [Link] {
40 putBoolean(KEY_ONBOARDING, true)
41 }
42 }
Example 2: Login Session (Non-Sensitive Data Only)
1 class SessionManager(context: Context) {
2
3 private val prefs = [Link]("session", Context.MODE_PRIVATE)
4
5 companion object {
6 private const val KEY_USER_ID = "user_id"
7 private const val KEY_USER_NAME = "user_name"
8 private const val KEY_USER_EMAIL = "user_email"
9 private const val KEY_IS_LOGGED = "is_logged_in"
10 }
11
12 val isLoggedIn: Boolean
13 get() = [Link](KEY_IS_LOGGED, false)
14
15 val userId: String?
16 get() = [Link](KEY_USER_ID, null)
17
18 val userName: String
19 get() = [Link](KEY_USER_NAME, "Guest") ?: "Guest"
20
21 fun saveSession(userId: String, name: String, email: String) {
22 [Link] {
23 putBoolean(KEY_IS_LOGGED, true)
24 putString(KEY_USER_ID, userId)
25 putString(KEY_USER_NAME, name)
26 putString(KEY_USER_EMAIL, email)
27 }
28 }
29
30 fun clearSession() = [Link] {
31 remove(KEY_IS_LOGGED)
32 remove(KEY_USER_ID)
33 remove(KEY_USER_NAME)
34 remove(KEY_USER_EMAIL)
35 }
36 }
Example 3: Launch Counter + First Launch Detection
1 class LaunchTracker(context: Context) {
2
3 private val prefs = [Link]("launch_tracker",
Context.MODE_PRIVATE)
4
5 companion object {
6 private const val KEY_LAUNCH_COUNT = "launch_count"
7 private const val KEY_FIRST_LAUNCH = "first_launch_ms"
8 private const val KEY_LAST_LAUNCH = "last_launch_ms"
9 }
10
11 val launchCount: Int
12 get() = [Link](KEY_LAUNCH_COUNT, 0)
13
14 val isFirstEverLaunch: Boolean
15 get() = 
16
17 val daysSinceFirstLaunch: Int
18 get() {
19 val firstLaunchMs = [Link](KEY_FIRST_LAUNCH, 0L)
20 return (([Link]() - firstLaunchMs) /
86_400_000L).toInt()
21 }
22
23 fun recordLaunch() {
24 val now = [Link]()
25 [Link] {
26 val count = [Link](KEY_LAUNCH_COUNT, 0)
27 putInt(KEY_LAUNCH_COUNT, count + 1)
28 putLong(KEY_LAST_LAUNCH, now)
29 if () {
30 putLong(KEY_FIRST_LAUNCH, now)
31 }
32 }
33 }
34
35 fun shouldShowRatingPrompt(): Boolean {
36 val count = launchCount
37 return count == 10 || count == 30 || count == 60
38 }
39 }
Example 4: Onboarding State Machine
1 class OnboardingManager(context: Context) {
2
3 private val prefs = [Link]("onboarding",
Context.MODE_PRIVATE)
4
5 enum class OnboardingStep {
6 NOT_STARTED, INTRO_SEEN, PERMISSIONS_ASKED, PROFILE_CREATED, COMPLETED
7 }
8
9 var currentStep: OnboardingStep
10 get() = [Link]("step", null)
11 ?.let { runCatching { [Link](it) }.getOrNull() }
12 ?: OnboardingStep.NOT_STARTED
13 set(value) = [Link] { putString("step", [Link]) }
14
15 val isCompleted: Boolean
16 get() = currentStep == [Link]
17
18 fun advanceToNextStep() {
19 val next = when (currentStep) {
20 OnboardingStep.NOT_STARTED -> OnboardingStep.INTRO_SEEN
21 OnboardingStep.INTRO_SEEN -> OnboardingStep.PERMISSIONS_ASKED
22 OnboardingStep.PERMISSIONS_ASKED -> OnboardingStep.PROFILE_CREATED
23 OnboardingStep.PROFILE_CREATED -> [Link]
24 [Link] -> [Link]
25 }
26 currentStep = next
27 }
28 }
Example 5: Selected Tab / Last Screen State
1 class NavigationState(context: Context) {
2
3 private val prefs = [Link]("nav_state",
Context.MODE_PRIVATE)
4
5 var lastSelectedTab: Int
6 get() = [Link]("last_tab", 0)
7 set(value) = [Link] { putInt("last_tab", value) }
8
9 var lastScrollPosition: Int
10 get() = [Link]("scroll_pos", 0)
11 set(value) = [Link] { putInt("scroll_pos", value) }
12
13 fun clearNavigationState() = [Link] { clear() }
14 }
Example 6: Filter and Sort Preferences
1 class FilterPrefs(context: Context) {
2
3 private val prefs = [Link]("filters", Context.MODE_PRIVATE)
4
5 enum class SortOrder { DATE_DESC, DATE_ASC, PROFIT_DESC, PROFIT_ASC }
6
7 var sortOrder: SortOrder
8 get() = [Link]("sort_order", null)
9 ?.let { runCatching { [Link](it) }.getOrNull() }
10 ?: SortOrder.DATE_DESC
11 set(value) = [Link] { putString("sort_order", [Link]) }
12
13 var showProfitablOnly: Boolean
14 get() = [Link]("show_profitable_only", false)
15 set(value) = [Link] { putBoolean("show_profitable_only", value) }
16
17 var selectedStrategies: Set<String>
18 get() = [Link]("selected_strategies", emptySet()) ?: emptySet()
19 set(value) = [Link] { putStringSet("selected_strategies", value) }
20
21 fun addStrategy(strategy: String) {
22 val current = [Link]()
23 [Link](strategy)
24 selectedStrategies = current
25 }
26
27 fun removeStrategy(strategy: String) {
28 val current = [Link]()
29 [Link](strategy)
30 selectedStrategies = current
31 }
32
33 fun resetFilters() = [Link] { clear() }
34 }
Example 7: Feature Flags (Local)
1 class LocalFeatureFlags(context: Context) {
2
3 private val prefs = [Link]("feature_flags",
Context.MODE_PRIVATE)
4
5 // Default to false — features off until explicitly enabled
6 val isNewDashboardEnabled: Boolean
7 get() = [Link]("new_dashboard", false)
8
9 val isBetaModeEnabled: Boolean
10 get() = [Link]("beta_mode", false)
11
12 val maxTradesPerDay: Int
13 get() = [Link]("max_trades_per_day", 10)
14
15 // Update from remote config or admin screen
16 fun updateFlags(newDashboard: Boolean, betaMode: Boolean, maxTrades: Int) {
17 [Link] {
18 putBoolean("new_dashboard", newDashboard)
19 putBoolean("beta_mode", betaMode)
20 putInt("max_trades_per_day", maxTrades)
21 }
22 }
23 }
Example 8: StrictMode-Compatible Background Read
1 // For legacy code that must keep SharedPreferences but wants to avoid StrictMode
violations
2 class SafePrefsReader(
3 private val prefs: SharedPreferences,
4 private val scope: CoroutineScope
5 ) {
6 // Read on IO thread, return via callback
7 fun getBooleanAsync(key: String, default: Boolean, callback: (Boolean) -> Unit) {
8 [Link]([Link]) {
9 val value = [Link](key, default)
10 withContext([Link]) {
11 callback(value)
12 }
13 }
14 }
15
16 // Suspend version for coroutine callers
17 suspend fun getBooleanSuspend(key: String, default: Boolean): Boolean =
18 withContext([Link]) {
19 [Link](key, default)
20 }
21 }
Example 9: Notification Settings Per Channel
1 class NotificationSettings(context: Context) {
2
3 private val prefs = [Link]("notifications",
Context.MODE_PRIVATE)
4
5 // Separate key per notification type
6 fun isChannelEnabled(channelId: String): Boolean =
7 [Link]("notif_$channelId", true)
8
9 fun setChannelEnabled(channelId: String, enabled: Boolean) =
10 [Link] { putBoolean("notif_$channelId", enabled) }
11
12 fun getEnabledChannels(allChannelIds: List<String>): List<String> =
13 [Link] { isChannelEnabled(it) }
14
15 fun enableAll(allChannelIds: List<String>) = [Link] {
16 [Link] { putBoolean("notif_$it", true) }
17 }
18
19 fun disableAll(allChannelIds: List<String>) = [Link] {
20 [Link] { putBoolean("notif_$it", false) }
21 }
22 }
Example 10: A/B Testing Variant Assignment
1 class AbTestingPrefs(context: Context) {
2
3 private val prefs = [Link]("ab_testing",
Context.MODE_PRIVATE)
4
5 // Assign user to a variant once and persist it
6 fun getVariant(testName: String, variants: List<String>): String {
7 val stored = [Link](testName, null)
8 if (stored != null && stored in variants) return stored
9
10 // Assign randomly and persist
11 val assigned = [Link]()
12 [Link] { putString(testName, assigned) }
13 return assigned
14 }
15
16 fun clearVariants() = [Link] { clear() }
17 }
18
19 // Usage:
20 val variant = [Link]("home_cta_test", listOf("control", "variant_a",
"variant_b"))
21 // User always gets the same variant on subsequent app opens
Chapter 26: SharedPreferences vs DataStore — When to Use What
Modern Recommendation
1 New Code:
2 Always → DataStore (Preferences DataStore for simple key-value)
3
4 Legacy Code:
5 Already using SharedPreferences → OK to keep for now
6 Adding new preferences → Add to DataStore, not SharedPreferences
7 Performance problems / ANRs → Migrate to DataStore
8
9 Third-party SDK uses SharedPreferences:
10 → You cannot change it, just be aware of the implications
Side-by-Side Comparison
Aspect SharedPreferences DataStore (Preferences)
API style Synchronous Asynchronous (Flow + suspend)
Main thread safety ❌ Blocks on load + fsync ✅ Never blocks
Write confirmation commit() only Always (suspend returns after write)
Error handling ❌ Silent apply() ✅ Exposed via Flow
Type safety ❌ Runtime ClassCastException ✅ Typed keys, compile-time
Atomic writes Partial (backup mechanism) ✅ Full (temp file + atomic rename)
Reactive updates Listener (main thread only) ✅ Flow (any thread)
Compose integration Listener → manual state ✅ collectAsStateWithLifecycle()
File format XML (human readable) Binary protobuf
File size Larger ~7x smaller
Setup complexity Very simple Moderate
Android version Since API 1 Requires modern dependencies
Use SharedPreferences If:
You're in a legacy codebase with extensive SharedPreferences usage and no time to migrate
You need to support an old Android API level where DataStore might have dependency issues
The data is truly trivial and you need zero setup (one flag in a quick prototype)
Use DataStore If:
Starting a new project
Building in Jetpack Compose (Flow integrates naturally)
You need guaranteed write confirmation
You've had ANR issues related to SharedPreferences
You want proper error handling
You need reactive UI updates
Kotlin Multiplatform (KMP) Support in DataStore
Starting with modern releases, Jetpack DataStore officially supports Kotlin Multiplatform (KMP).
Platform Unification: You can write a single key-value preferences class in commonMain that
compiles and runs on Android, iOS, and Desktop.
Storage Backends:
Android: Persists data inside the application sandbox database folders.
iOS: Leverages Kotlin/Native and Okio to write preferences to the iOS Document Directory,
completely eliminating the need to write custom Swift/Kotlin bridges to UserDefaults .
Reactive Flow: Exposes data reactively using standard Flow<Preferences> in shared common
repositories.
Chapter 27: Migrating From SharedPreferences to DataStore
DataStore has built-in migration support. It handles the migration automatically on first access.
Automatic Migration
1 // Your new DataStore with migration from old SharedPreferences
2 val [Link]: DataStore<Preferences> by preferencesDataStore(
3 name = "app_prefs",
4 migrations = listOf(
5 SharedPreferencesMigration(
6 context = this,
7 sharedPreferencesName = "[Link]" // your old SP file name
8 )
9 )
10 )
What the Migration Does
1. First time [Link] is accessed:
Reads all data from the old SharedPreferences XML file
Converts each key-value pair to DataStore format
Writes to the new .preferences_pb file
Deletes the old SharedPreferences XML file
2. On all subsequent accesses: migration is skipped (file doesn't exist anymore)
Migrating With Key Remapping
If you want to rename keys during migration:
1 SharedPreferencesMigration(
2 context = context,
3 sharedPreferencesName = "old_settings",
4 migrate = { sharedPrefsView, mutablePreferences ->
5 // Map old keys to new keys
6 if (DARK_MODE_KEY !in mutablePreferences) {
7 val oldValue = [Link]("isDarkMode", false) // old
name
8 mutablePreferences[DARK_MODE_KEY] = oldValue // new
name
9 }
10 mutablePreferences
11 }
12 )
Gradual Migration Strategy
For large codebases, migrate file by file:
1 // Step 1: Create DataStore with migration for the most critical file
2 val [Link] by preferencesDataStore(
3 name = "user_prefs",
4 migrations = listOf(SharedPreferencesMigration(this, "user_settings"))
5 )
6
7 // Step 2: Update all code that wrote to "user_settings" to use userPrefsStore
8
9 // Step 3: Next file...
10 val [Link] by preferencesDataStore(
11 name = "app_config",
12 migrations = listOf(SharedPreferencesMigration(this, "app_settings"))
13 )
Chapter 28: Quick Reference Cheat Sheet
Get Instance
1 // App-wide (use this most often)
2 [Link]("name", Context.MODE_PRIVATE)
3
4 // Activity-specific
5 [Link](Context.MODE_PRIVATE)
6
7 // Default settings file
8 [Link](context)
Read
1 [Link]("key", false)
2 [Link]("key", 0)
3 [Link]("key", 0L)
4 [Link]("key", 0f)
5 [Link]("key", null)
6 [Link]("key", emptySet())
7 [Link]("key")
8 [Link] // Map<String, *> of all entries
Write (with KTX extension)
1 [Link] {
2 putBoolean("key", true)
3 putInt("key", 42)
4 putLong("key", 1000L)
5 putFloat("key", 1.5f)
6 putString("key", "value")
7 putStringSet("key", setOf("a", "b"))
8 }
Write with commit() (synchronous)
1 [Link](commit = true) {
2 putString("key", "value")
3 }
Delete
1 [Link] { remove("key") }
2 [Link] { clear() }
Change Listener
1 private val listener = [Link] { prefs,
key ->
2 // called on main thread
3 }
4 [Link](listener) // in onResume/onStart
5 [Link](listener) // in onPause/onStop
Encrypted SharedPreferences
1 val masterKey = [Link](context)
2 .setKeyScheme([Link].AES256_GCM).build()
3
4 val encryptedPrefs = [Link](
5 context, "secure_prefs", masterKey,
6 [Link].AES256_SIV,
7 [Link].AES256_GCM
8 )
In Koin
1 single<SharedPreferences> {
2 androidContext().getSharedPreferences("prefs", Context.MODE_PRIVATE)
3 }
Final Summary
1 SharedPreferences
2 ├── Introduced: Android 1.0, 2008
3 ├── Storage: XML file at /shared_prefs/[Link]
4 ├── In-memory: HashMap<String, Object>
5 ├── Read path: [Link]() — instant after load
6 ├── Write path:
7 │ ├── commit() — synchronous, returns Boolean, blocks caller thread
8 │ └── apply() — "async" but blocks main thread via QueuedWork on lifecycle events
9 ├── Supports: Boolean, Int, Long, Float, String, Set<String>
10 ├── Dangerous because:
11 │ ├── apply() → QueuedWork → ANR on main thread during lifecycle
12 │ ├── No write confirmation from apply()
13 │ ├── No type safety (runtime ClassCastException)
14 │ └── Plain text (readable on rooted devices)
15 ├── Use when: legacy code, quick prototypes, truly trivial data
16 ├── Don't use when: new code, Compose, need reactive updates, sensitive data
17 └── Migrate to: DataStore (official Google recommendation since 2021)
18
19 The 5 Rules:
20 1. Always MODE_PRIVATE — never other modes
21 2. Never commit() on main thread — use apply() or commit() on IO thread
22 3. Always store listener as member variable — WeakReference trap
23 4. Never store sensitive data — use EncryptedSharedPreferences
24 5. Never modify a returned Set<String> in place — create a new set
This guide covers SharedPreferences as of Android API 34. The API itself has not changed
significantly since API 1 — the problems described have existed since 2008 and persist today.