Mobile Development 16 min read

Revisiting SharedPreferences and Migrating to DataStore in Android

This article analyzes the performance pitfalls of Android's SharedPreferences, explains how its apply() method can still cause ANR, introduces Jetpack DataStore as a modern replacement, and provides detailed migration strategies including code examples, coroutine handling, and bytecode transformation techniques.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Revisiting SharedPreferences and Migrating to DataStore in Android

For Android developers, SharedPreferences is a long‑standing key‑value storage solution, but its asynchronous apply() API can still block the main thread and cause ANR when the write queue is not flushed during activity lifecycle events such as handleStopService() , handlePauseActivity() , and handleStopActivity() . The article shows the internal implementation of apply() and how the write operation is queued via QueuedWork .

public void apply() {
    final long startTime = System.currentTimeMillis();
    final MemoryCommitResult mcr = commitToMemory();
    final Runnable awaitCommit = new Runnable() {
        @Override
        public void run() {
            try {
                mcr.writtenToDiskLatch.await();
            } catch (InterruptedException ignored) {}
            if (DEBUG && mcr.wasWritten) {
                Log.d(TAG, mFile.getName() + ":" + mcr.memoryStateGeneration + " applied after " + (System.currentTimeMillis() - startTime) + " ms");
            }
        }
    };
    QueuedWork.addFinisher(awaitCommit);
    // write to disk
    SharedPreferencesImpl.this.enqueueDiskWrite(mcr, postWriteRunnable);
    notifyListeners(mcr);
}

Google’s official documentation confirms that even with apply() , the write may be delayed, leading to UI thread blockage on older Android versions. To avoid these issues, the article recommends replacing SharedPreferences with Jetpack DataStore, which offers type‑safe, coroutine‑based, transactional storage.

DataStore provides two implementations: Preferences DataStore (key‑value) and Proto DataStore (protocol‑buffer based). The article focuses on Preferences DataStore and shows a simple Kotlin declaration:

val Context.dataStore: DataStore
by preferencesDataStore("filename")

Reading and writing values requires a coroutine scope because DataStore operates asynchronously via Kotlin Flow:

// Reading
CoroutineScope(Dispatchers.Default).launch {
    context.dataStore.data.collect { prefs ->
        val value = prefs[booleanPreferencesKey(key)] ?: defValue
    }
}

// Writing
CoroutineScope(Dispatchers.IO).launch {
    context.dataStore.edit { settings ->
        settings[booleanPreferencesKey(key)] = value
    }
}

The core of DataStore is the updateData suspend function, which creates a CompletableDeferred (a Deferred) to await the result, packages the update into a Message.Update , and sends it to a SimpleActor for sequential processing.

override suspend fun updateData(transform: suspend (t: T) -> T): T {
    val ack = CompletableDeferred
()
    val currentDownStreamFlowState = downstreamFlow.value
    val updateMsg = Message.Update(transform, ack, currentDownStreamFlowState, coroutineContext)
    actor.offer(updateMsg)
    return ack.await()
}

The actor runs on a coroutine scope, consumes messages from an unlimited Channel , and dispatches them to handleUpdate or handleRead . handleUpdate eventually calls transformAndWrite , which writes the new data to a temporary file and atomically renames it.

internal suspend fun writeData(newData: T) {
    file.createParentDirectories()
    val scratchFile = File(file.absolutePath + SCRATCH_SUFFIX)
    FileOutputStream(scratchFile).use { stream ->
        serializer.writeTo(newData, UncloseableOutputStream(stream))
        stream.fd.sync()
    }
    if (!scratchFile.renameTo(file)) {
        throw IOException("Unable to rename $scratchFile. This likely means multiple DataStore instances exist.")
    }
}

After understanding DataStore, the article outlines a migration path from SharedPreferences to DataStore. Existing SP data can be migrated automatically using SharedPreferencesMigration when creating the DataStore:

dataStore = context.createDataStore(
    name = "preferenceName",
    migrations = listOf(SharedPreferencesMigration(context, "sp_name"))
)

For large codebases where manual replacement is impractical, the article proposes an ASM‑based bytecode transformation that replaces calls to getSharedPreferences with a custom wrapper DataPreference that forwards operations to DataStore. The transformation swaps the original INVOKEVIRTUAL instruction with an INVOKESTATIC call to an extension function getDataPreferences and inserts a CHECKCAST to keep the type compatible.

static void spToDataStore(MethodInsnNode node, ClassNode klass, MethodNode method) {
    if (node.name.equals("getSharedPreferences") && node.desc.equals("(Ljava/lang/String;I)Landroid/content/SharedPreferences;")) {
        MethodInsnNode hook = new MethodInsnNode(Opcodes.INVOKESTATIC,
            "com/example/spider/StoreTestKt",
            "getDataPreferences",
            "(Landroid/content/Context;Ljava/lang/String;I)Landroid/content/SharedPreferences;",
            false);
        TypeInsnNode cast = new TypeInsnNode(Opcodes.CHECKCAST, "android/content/SharedPreferences");
        InsnList list = new InsnList();
        list.add(hook);
        list.add(cast);
        method.instructions.insertBefore(node, list);
        method.instructions.remove(node);
    }
}

The article also acknowledges limitations: because DataStore writes are asynchronous, a subsequent immediate getBoolean after apply() may read stale data. The recommended pattern is to observe the Flow and react to updates rather than performing synchronous reads after writes.

In conclusion, the author suggests that DataStore’s performance is comparable to MMKV when used appropriately, and encourages developers to evaluate the best key‑value storage solution for their specific scenario.

MigrationPerformanceAndroidKotlinSharedPreferencesDatastore
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.