Let’s talk about something every app needs: storing data. Whether it’s saving user settings, a favorite theme, or even login details, there’s a simple and efficient way to do it in Android SharedPreferences. But I get it, SharedPreferences might sound a bit old school. That’s why we’ll level it up with Coroutines, Flow, and even look at how to do it securely. Plus, I’ll show you how to organize your code with a UseCase architecture and write effective tests.
If you don't have any knowledge about what Coroutines and Flow are, I'm sure you will have a better Android application development experience if you read this article after reading my article "Kotlin Coroutines Asynchronous Programming in Android".
What is SharedPreferences and Why Should You Care?

Simply put, SharedPreferences is like a little notebook where your app can scribble down key-value pairs of data. It’s perfect for saving stuff like user preferences (dark mode, notifications), small configs, or any info you want to keep even after your app closes. It’s lightweight, easy to use, and gets the job done. But don’t expect it to handle massive data for that, you’d want to check out Room or other database solutions.
Setting Up SharedPreferences
Saving and Retrieving Data: The Basics
Let’s start with the basics. Here’s how you can save and retrieve data using SharedPreferences:
val sharedPreferences = context.getSharedPreferences("MyAppPrefs", Context.MODE_PRIVATE)
// Saving data
sharedPreferences.edit().putString("user_name", "gorkemkara").apply()
// Retrieving data
val userName = sharedPreferences.getString("user_name", "defaultName")
Easy, right? But we’re just scratching the surface. Let’s take it up a notch.
Enhancing SharedPreferences with Coroutines and Flow
Why Use Coroutines and Flow?
Look, blocking the main thread is a no-go. That’s where Coroutines and Flow come into play. By using coroutines, you can perform background operations like reading or writing data without freezing your UI. And with Flow, you can make your data changes reactive. No more clunky, outdated code.
Saving Data Asynchronously with Coroutines
Here’s how you can save data without slowing down your app:
suspend fun saveUserName(userName: String) {
withContext(Dispatchers.IO) {
val sharedPreferences = context.getSharedPreferences("MyApplicationPrefs", Context.MODE_PRIVATE)
sharedPreferences.edit().putString("user_name", userName).apply()
}
}
By wrapping it in withContext(Dispatchers.IO)
, you ensure that your save operation doesn’t block the main thread. Simple and effective!
Observing Changes with Flow
Want to know when a value changes without manually checking? Use Flow to turn your SharedPreferences into a reactive data stream:
fun observeUserName(): Flow<String> = callbackFlow {
val sharedPreferences = context.getSharedPreferences("MyApplicationPrefs", Context.MODE_PRIVATE)
val listener = SharedPreferences.OnSharedPreferenceChangeListener { _, key ->
if (key == "user_name") {
trySend(sharedPreferences.getString("user_name", "defaultName") ?: "defaultName")
}
}
sharedPreferences.registerOnSharedPreferenceChangeListener(listener)
awaitClose { sharedPreferences.unregisterOnSharedPreferenceChangeListener(listener) }
}
With this setup, whenever the “user_name” changes, your app knows about it. No more polling or unnecessary checks. Sweet, right?
Better Code Organization with UseCase
Why Use a UseCase? It’s All About Clean Code
Let’s be real; we all want our code to be neat and tidy. A UseCase helps you organize your logic, making it easy to maintain, test, and expand. Here’s a simple example:
class SaveUserNameUseCase(private val repository: UserRepository) {
suspend operator fun invoke(userName: String) {
repository.saveUserName(userName)
}
}
interface UserRepository {
suspend fun saveUserName(userName: String)
fun observeUserName(): Flow<String>
}
By wrapping your logic inside a UseCase, your UI code remains clean, and you avoid spaghetti code that no one (including future you) wants to deal with.
Securing Your Data: Encrypting SharedPreferences
Why Encryption Matters
Storing sensitive information like user passwords or authentication tokens in plain text is a recipe for disaster. That’s where EncryptedSharedPreferences comes in. It encrypts your data, making sure prying eyes can’t read what they shouldn’t. Check out the official guide on EncryptedSharedPreferences for more.
How to Use EncryptedSharedPreferences
Here’s how you set it up:
val masterKey = MasterKey.Builder(context).setKeyScheme(MasterKey.KeyScheme.AES256_GCM).build()
val encryptedSharedPreferences = EncryptedSharedPreferences.create(
context,
"EncryptedPrefs",
masterKey,
EncryptedSharedPreferences.PrefKeyEncryptionScheme.AES256_SIV,
EncryptedSharedPreferences.PrefValueEncryptionScheme.AES256_GCM
)
encryptedSharedPreferences.edit().putString("secure_user_name", "JaneDoe").apply()
This way, even if someone gets access to your app’s files, they won’t be able to read your users’ sensitive data. Peace of mind, right there.
Writing Tests for SharedPreferences
Why Testing is Important
Testing ensures that your app behaves as it should, even when you’re not looking. With SharedPreferences, you’ll want to confirm that data is correctly saved, retrieved, and observed. Here’s how to do it:
Unit Testing SharedPreferences with Mockito and JUnit
class UserRepositoryTest {
private lateinit var sharedPreferences: SharedPreferences
private lateinit var repository: UserRepository
@Before
fun setUp() {
sharedPreferences = mock(SharedPreferences::class.java)
repository = UserRepositoryImpl(sharedPreferences)
}
@Test
fun saveUserName_savesDataCorrectly() {
val editor = mock(SharedPreferences.Editor::class.java)
`when`(sharedPreferences.edit()).thenReturn(editor)
repository.saveUserName("gorkemkara")
verify(editor).putString("user_name", "gorkemkara")
verify(editor).apply()
}
@Test
fun observeUserName_emitsCorrectValues() = runBlockingTest {
val flow = repository.observeUserName()
assertEquals("defaultName", flow.first())
}
}
Using Mockito and JUnit, you can easily mock SharedPreferences and verify the behavior of your methods. Clean, simple, and it gives you confidence that your code works!
You’ve Got This!

There you have it!
You now know how to handle data storage in Android using SharedPreferences, make it async with Coroutines and Flow, keep it secure with encryption, and organize your logic with a UseCase. Plus, you’re equipped with the skills to write tests that make sure everything works as expected. For more in-depth info, check out the Android data storage guide and the Coroutines documentation. Keep building awesome apps!
Did you like this article?
You can subscribe to my newsletter below and get updates about my new articles.