If you’re building Android apps with Jetpack Compose, chances are you’ve already used Compose Preview. Or at least clicked the little Preview tab in Android Studio and hoped it would magically show your UI. Sometimes it does.Sometimes it doesn’t. In this blog, we’ll break down Compose Preview, covering everything from core mechanics to practical tips....
If you’ve ever opened your beautifully designed app on a tablet only to see it looking like a stretched-out phone screen, you know the pain. Or maybe you’ve watched your carefully crafted layout break apart on a foldable device. Trust me, I’ve been there. The good news..? Responsive and Adaptive UI in Jetpack Compose makes...
Android 12 introduced one of the biggest visual upgrades in Android history: Material You. At the heart of this design shift is a small but powerful feature called Monochrome Icons.
If you’ve ever noticed your app icons changing color to match your wallpaper, that’s Monochrome Icons doing their job.
In this guide, we’ll break down:
What Monochrome Icons are
How they power Android themed icons
Why Android 12+ relies on them
How to implement them correctly
What Are Monochrome Icons in Android?
Monochrome Icons are simplified versions of app icons that use a single color only.
They remove:
Gradients
Shadows
Multiple colors
Decorative details
Instead, they focus on shape and clarity.
Android uses these icons as a base to dynamically apply system colors based on the user’s wallpaper and theme.
In short: Monochrome Icons are the foundation of Android themed icons.
Why Android 12+ Needs Monochrome Icons
Before Android 12, app icons were static. Every icon looked the same regardless of theme.
Android 12 changed this with dynamic theming, where the system extracts colors from the user’s wallpaper and applies them across:
Quick settings
Widgets
System UI
App icons
For this to work cleanly, Android needs icons that are easy to recolor. That’s where Monochrome Icons come in.
Without a proper monochrome layer, Android cannot theme your app icon correctly.
How Themed Icons Work Behind the Scenes
Here’s what happens when a user enables themed icons:
Android checks if your app supports Monochrome Icons
If supported, the system loads the monochrome drawable
Android applies dynamic colors from the Material You palette
The icon adapts instantly when wallpaper or theme changes
If your app does not include a monochrome icon:
The icon stays unchanged
It breaks visual consistency
It looks outdated next to themed apps
Where Monochrome Icons Live in Your App
Monochrome Icons are defined inside your adaptive icon XML.
This file is usually located at:
res/mipmap-anydpi-v26/ic_launcher.xml
This is where Android expects your monochrome icon to be declared.
If your resources are misconfigured, Android won’t find the monochrome drawable.
Checking Android Version (Optional Use Case)
While Android handles themed icons automatically, you may want to check Android version in Kotlin for UI consistency elsewhere.
Kotlin
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {// Android 12 or higher// Themed icons and Material You are supported}
Build.VERSION.SDK_INT gives the device’s Android version
VERSION_CODES.S represents Android 12
This helps you align other UI elements with themed icons
Again, Monochrome Icons themselves do not need Kotlin logic.
Common Mistakes Developers Make
1. Using Colored Monochrome Icons
Monochrome means one solid color only. Shades or gradients will break theming.
2. Overly Detailed Icons
Thin lines disappear when recolored. Bold shapes work best.
3. Forgetting the <monochrome> Tag
Without it, Android ignores themed icons entirely.
4. Relying on PNGs
Vector drawables scale better and theme more reliably.
Why Monochrome Icons Improve User Experience
From a user perspective, Monochrome Icons:
Make the home screen feel cohesive
Reduce visual noise
Adapt naturally to dark and light themes
Feel modern and intentional
From a developer perspective:
Your app looks native on Android 12+
Better alignment with Material You
Improved visual trust and polish
Conclusion
Monochrome Icons may look simple, but they power one of Android’s most advanced design features.
If your app targets Android 12 or higher, supporting Monochrome Icons is no longer optional. It’s part of building a modern, user-first Android experience.
Keep your icons simple. Let the system do the coloring. And embrace Material You the way it was designed.
Jetpack Compose introduces a very different mental model compared to XML-based Android UI. One line that often confuses beginners (and even experienced Android devs at first) is:
Kotlin
onValueChange = { value = it }
Especially when value is defined like this:
Kotlin
varvaluebyremember { mutableStateOf(0) }
At first glance, this line looks almost too simple — and that’s exactly why it’s confusing.
Let’s break down what’s really happening, why it’s written this way, and how it fits into Compose’s state-driven architecture.
The Big Picture: Compose Is State-Driven
Before diving into syntax, it’s important to understand how Compose thinks.
In classic Android:
You updated UI elements directly
UI held its own state
You manually synced UI ↔ data
In Jetpack Compose:
State owns the UI
UI is a function of state
When state changes → UI recomposes automatically
This single line:
Kotlin
onValueChange = { value = it }
is the bridge between user interaction and state updates.
What remember { mutableStateOf(...) } Really Does
Consider this state declaration:
Kotlin
varvaluebyremember { mutableStateOf(0) }
This does three important things:
1. mutableStateOf
Creates an observable state holder. Compose watches this value and tracks where it’s used.
2. remember
Ensures the state survives re-composition. Without remember, the value would reset every time Compose redraws the UI.
3. by keyword
This is Kotlin property delegation. It allows you to write:
Kotlin
value = 5
instead of:
Kotlin
value.value = 5
So value behaves like a normal variable, but Compose is quietly observing it.
What onValueChange Is (Conceptually)
Most interactive Compose components (such as TextField, Slider, Checkbox) follow the same pattern:
If you’ve ever worked with Android’s BroadcastReceiver, you know there’s a golden rule: keep your work quick. The system expects your receiver to finish in about 10 seconds, or it’ll label your app as unresponsive (ANR). But what happens when you need just a bit more time? That’s exactly where goAsync() comes to the rescue....
State management is the backbone of any modern Android app. If state is messy, your UI becomes unpredictable, buggy, and hard to maintain. Jetpack Compose was designed to solve many of these problems, but only if you understand how State Management in Jetpack Compose actually works. In this post, we will break down modern state...
Have you ever opened an app on your Android phone, switched to another app for a while, and then returned only to find everything reset? Your form data gone, your scroll position lost, or the app back at the home screen?
Understanding Process Death in Android is crucial for building apps that feel reliable and professional. In this guide, we’ll explore what it is, why it happens, and most importantly, how to handle it properly in your Android apps.
What Exactly Is Process Death in Android?
Process Death in Android occurs when the Android operating system kills your app’s process to free up memory for other apps. Think of it like your phone doing some housekeeping — when memory gets tight, Android decides which apps to close to keep everything running smoothly.
Here’s the tricky part: when your app’s process dies, your activities might still appear to be in the back stack. When the user returns, Android recreates the activity, but all your runtime data is gone unless you’ve saved it properly.
This is different from the normal activity lifecycle. Your app doesn’t just pause or stop — it’s completely terminated and then brought back to life.
Why Does Process Death Happen?
Android needs to manage limited device resources efficiently. Here are the main reasons Process Death in Android occurs:
Low Memory Situations
When your device runs low on RAM, Android starts killing background processes. Apps you haven’t used recently are the first to go.
Developer Options Testing
During development, you can enable “Don’t keep activities” in Developer Options. This immediately destroys activities when they leave the foreground, making it easier to test Process Death scenarios.
Long Background Duration
If your app stays in the background for an extended period while the user uses other memory-intensive apps, there’s a higher chance your process will be killed.
System Updates or Crashes
Sometimes system events or crashes can trigger process termination across multiple apps.
Real-World Example: The Shopping Cart Problem
Let me share a common scenario that illustrates Process Death in Android perfectly.
Imagine you’re building a shopping app. A user adds five items to their cart, then switches to their messaging app to check a friend’s recommendation. They spend 10 minutes chatting, during which Android kills your shopping app’s process to free memory.
When they return to your app, if you haven’t handled Process Death properly, their cart is empty.
Frustrating, right?
This is exactly why understanding and handling Process Death in Android matters for user experience.
How to Detect Process Death in Android
Before we fix it, let’s learn how to detect it. Add this code to your activity:
Kotlin
classMainActivity : AppCompatActivity() {overridefunonCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)if (savedInstanceState != null) {// Activity was recreated after process death Log.d("ProcessDeath", "Activity restored after process death") } else {// Normal first-time creation Log.d("ProcessDeath", "Activity created normally") } }}
The onCreate() method receives a savedInstanceState parameter. When this parameter is not null, it means Android is restoring your activity after Process Death. If it’s null, your activity is being created for the first time or normally resumed.
This simple check helps you understand when restoration is happening.
Saving State with onSaveInstanceState
The primary way to handle Process Death in Android is by overriding onSaveInstanceState(). This method is called before your activity might be destroyed, giving you a chance to save important data.
Kotlin
classShoppingCartActivity : AppCompatActivity() {privatevar cartItems = mutableListOf<String>()privatevar totalPrice = 0.0privatevar userName = ""overridefunonCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_shopping_cart)// Restore saved state if available savedInstanceState?.let { cartItems = it.getStringArrayList("CART_ITEMS")?.toMutableList() ?: mutableListOf() totalPrice = it.getDouble("TOTAL_PRICE", 0.0) userName = it.getString("USER_NAME", "") Log.d("ProcessDeath", "Restored ${cartItems.size} items") }updateUI() }overridefunonSaveInstanceState(outState: Bundle) {super.onSaveInstanceState(outState)// Save critical data before potential process death outState.putStringArrayList("CART_ITEMS", ArrayList(cartItems)) outState.putDouble("TOTAL_PRICE", totalPrice) outState.putString("USER_NAME", userName) Log.d("ProcessDeath", "Saved ${cartItems.size} items to bundle") }privatefunupdateUI() {// Update your UI with restored datafindViewById<TextView>(R.id.itemCount).text = "Items: ${cartItems.size}"findViewById<TextView>(R.id.totalPrice).text = "Total: $$totalPrice" }}
Here, we override two key methods:
onSaveInstanceState(): This is where we save our data to a Bundle. Think of a Bundle as a container that Android keeps safe even during Process Death. We use methods like putStringArrayList(), putDouble(), and putString() to store different data types.
onCreate(): We check if savedInstanceState exists. If it does, we restore our data using corresponding get methods like getStringArrayList() and getDouble().
The ?.let syntax is Kotlin’s safe call operator, ensuring we only access the bundle if it’s not null.
What Data Can You Save in a Bundle?
Bundles support many common data types, but there are limitations. Here’s what you can save:
Important Limitation: Bundles have a size limit (typically around 500KB to 1MB). Don’t try to save large images, videos, or extensive datasets. For large data, use other persistence methods like databases or files.
Using ViewModel to Survive Configuration Changes
While onSaveInstanceState() handles Process Death in Android, ViewModels help with configuration changes like screen rotation. However, ViewModels alone don’t survive process death.
Here’s how to combine both approaches:
Kotlin
classUserProfileViewModel : ViewModel() {// This survives configuration changes but NOT process deathvar userName = MutableLiveData<String>()var userAge = MutableLiveData<Int>()var profileImageUrl = MutableLiveData<String>()funupdateUserData(name: String, age: Int, imageUrl: String) { userName.value = name userAge.value = age profileImageUrl.value = imageUrl }}classUserProfileActivity : AppCompatActivity() {privatelateinitvar viewModel: UserProfileViewModeloverridefunonCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_user_profile) viewModel = ViewModelProvider(this).get(UserProfileViewModel::class.java)// If recovering from process death, restore to ViewModel savedInstanceState?.let {val name = it.getString("USER_NAME", "")val age = it.getInt("USER_AGE", 0)val imageUrl = it.getString("PROFILE_IMAGE_URL", "") viewModel.updateUserData(name, age, imageUrl) }observeViewModel() }overridefunonSaveInstanceState(outState: Bundle) {super.onSaveInstanceState(outState)// Save ViewModel data to survive process death outState.putString("USER_NAME", viewModel.userName.value ?: "") outState.putInt("USER_AGE", viewModel.userAge.value ?: 0) outState.putString("PROFILE_IMAGE_URL", viewModel.profileImageUrl.value ?: "") }privatefunobserveViewModel() { viewModel.userName.observe(this) { name ->findViewById<TextView>(R.id.nameText).text = name } viewModel.userAge.observe(this) { age ->findViewById<TextView>(R.id.ageText).text = "Age: $age" } }}
The ViewModel holds our UI data and survives configuration changes automatically. However, to survive Process Death in Android, we still need to:
Save ViewModel data in onSaveInstanceState()
Restore that data back to the ViewModel in onCreate()
This gives us the best of both worlds: automatic configuration change handling from ViewModel, plus process death recovery from saved instance state.
SavedStateHandle: The Modern Approach
Android Jetpack provides SavedStateHandle, which combines ViewModel benefits with automatic state saving. This is the recommended approach for handling Process Death in Android:
Kotlin
classShoppingViewModel(privateval savedStateHandle: SavedStateHandle) : ViewModel() {// Automatically saved and restored across process deathvar cartItems: MutableLiveData<List<String>> = savedStateHandle.getLiveData("cart_items", emptyList())var totalPrice: MutableLiveData<Double> = savedStateHandle.getLiveData("total_price", 0.0)funaddItem(item: String, price: Double) {val currentItems = cartItems.value?.toMutableList() ?: mutableListOf() currentItems.add(item) cartItems.value = currentItemsval currentTotal = totalPrice.value ?: 0.0 totalPrice.value = currentTotal + price// Automatically saved to SavedStateHandle }funclearCart() { cartItems.value = emptyList() totalPrice.value = 0.0 }}classShoppingActivity : AppCompatActivity() {privateval viewModel: ShoppingViewModelbyviewModels()overridefunonCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_shopping)// No manual state restoration needed!// SavedStateHandle does it automatically viewModel.cartItems.observe(this) { items ->updateCartUI(items) } viewModel.totalPrice.observe(this) { total ->findViewById<TextView>(R.id.totalText).text = "Total: $$total" }findViewById<Button>(R.id.addButton).setOnClickListener { viewModel.addItem("Product ${System.currentTimeMillis()}", 29.99) } }privatefunupdateCartUI(items: List<String>) {// Update RecyclerView or ListView with items }}
SavedStateHandle is magical for handling Process Death in Android. Here’s why:
getLiveData(): This method creates LiveData that’s automatically backed by saved state. When process death occurs, the data is saved. When the process restarts, the data is restored automatically.
No manual saving: Unlike the previous examples, we don’t need to override onSaveInstanceState(). The SavedStateHandle does it for us.
Type-safe: We can store various types, and they’re automatically serialized and deserialized.
The by viewModels() delegate is a Kotlin property delegate that creates or retrieves the ViewModel with SavedStateHandle automatically injected.
Testing Process Death in Android
Testing is crucial to ensure your app handles Process Death in Android correctly. Here are practical ways to test:
Method 1: Developer Options
Go to Settings → Developer Options
Enable “Don’t keep activities”
Navigate through your app, switching between activities
Every time an activity goes to the background, it’s destroyed
Method 2: Using ADB Command
Force your app’s process to be killed using Android Debug Bridge:
adb shell am kill com.yourapp.package
Then return to your app from the recent apps menu.
Method 3: Memory Pressure Testing
Use Android Studio’s Profiler to simulate low memory conditions:
Open Android Profiler
Select Memory
Click “Force garbage collection” multiple times
Android may kill your app’s process naturally
Common Mistakes to Avoid
When dealing with Process Death in Android, developers often make these mistakes:
Mistake 1: Saving Large Objects
Kotlin
// DON'T DO THISoverridefunonSaveInstanceState(outState: Bundle) {super.onSaveInstanceState(outState) outState.putSerializable("LARGE_IMAGE", largeImageBitmap) // Too big!}
Why it’s wrong: Bundles have size limits. Saving large objects causes TransactionTooLargeException.
Better approach: Save only a reference or ID, then reload the data from a persistent source.
Kotlin
// DO THIS INSTEADoverridefunonSaveInstanceState(outState: Bundle) {super.onSaveInstanceState(outState) outState.putString("IMAGE_URL", imageUrl) // Save URL, not bitmap}
Mistake 2: Assuming ViewModel Survives Process Death
Kotlin
// INCORRECT ASSUMPTIONclassMyActivity : AppCompatActivity() {privatelateinitvar viewModel: MyViewModeloverridefunonCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState) viewModel = ViewModelProvider(this).get(MyViewModel::class.java)// Assuming viewModel.userData is always available after process death// This is WRONG - ViewModel doesn't survive process death without SavedStateHandle }}
Fix: Use SavedStateHandle or manually save/restore ViewModel data.
Mistake 3: Not Testing Thoroughly
Many developers never test Process Death scenarios until users report bugs. Always enable “Don’t keep activities” during development.
Best Practices for Handling Process Death in Android
1. Use SavedStateHandle for Simple Data
For primitive types and small objects, SavedStateHandle is your best friend:
For critical data that users can’t afford to lose, use Room database or other persistent storage:
Kotlin
classFormActivity : AppCompatActivity() {privatelateinitvar database: AppDatabaseoverridefunonCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState) database = Room.databaseBuilder( applicationContext, AppDatabase::class.java,"form-database" ).build()// Restore form from database if exists lifecycleScope.launch {val savedForm = database.formDao().getUnsubmittedForm() savedForm?.let { restoreForm(it) } } }overridefunonPause() {super.onPause()// Save form to database when leaving activity lifecycleScope.launch {val formData = collectFormData() database.formDao().saveForm(formData) } }}
3. Combine Multiple Approaches
Use the right tool for each type of data:
SavedStateHandle: UI state (scroll position, selected tab, form inputs)
ViewModel: Temporary runtime data that survives configuration changes
Database/SharedPreferences: Persistent user data
Memory cache: Easily re-fetchable data
4. Keep Bundles Small
Only save essential state information. Calculate or reload other data:
Kotlin
overridefunonSaveInstanceState(outState: Bundle) {super.onSaveInstanceState(outState)// Save only IDs, not entire objects outState.putInt("SELECTED_ITEM_ID", selectedItem.id) outState.putString("SEARCH_QUERY", searchQuery)// Don't save the entire search results list// Reload it using the search query instead}
Advanced: Handling Process Death with Compose
If you’re using Jetpack Compose, handling Process Death in Android looks a bit different:
With Compose, you still use ViewModel with SavedStateHandle. The difference is:
observeAsState(): Converts LiveData from ViewModel into Compose State
Automatic recomposition: When the ViewModel data changes (including after process death restoration), Compose automatically updates the UI
No manual lifecycle management: Compose handles the observation lifecycle for you
The underlying Process Death handling still uses SavedStateHandle in the ViewModel, but the UI layer becomes much simpler.
Monitoring Process Death in Production
To understand how often Process Death in Android affects your users, implement analytics:
Kotlin
classAnalyticsHelper(privateval context: Context) {funtrackProcessDeath(activityName: String) {// Log to your analytics service FirebaseAnalytics.getInstance(context).logEvent("process_death_recovery") {param("activity_name", activityName)param("timestamp", System.currentTimeMillis()) } }}classMainActivity : AppCompatActivity() {overridefunonCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)setContentView(R.layout.activity_main)if (savedInstanceState != null) {AnalyticsHelper(this).trackProcessDeath("MainActivity") } }}
This helps you understand:
How frequently users experience process death
Which activities are most affected
Whether users successfully recover their state
Conclusion
Process Death in Android is an essential concept every Android developer must master. It’s not just about preventing crashes — it’s about creating a seamless user experience that feels reliable and polished.
Remember these key takeaways:
For simple UI state: Use SavedStateHandle with ViewModel. It automatically handles Process Death in Android with minimal code.
For important user data: Persist to a database. Don’t rely solely on saved instance state for data users can’t afford to lose.
Always test: Enable “Don’t keep activities” during development. Test your app thoroughly by simulating process death scenarios.
Keep bundles small: Only save essential state information. Reload complex data when the activity restores.
By properly handling Process Death in Android, you’ll build apps that feel professional, reliable, and respectful of your users’ time and data. Your users might never know about the complexity you’ve handled behind the scenes — and that’s exactly the point.
Start implementing these patterns in your next project, and you’ll be amazed at how much more robust your apps become.
If you’ve been writing object-oriented code for a while, you’ve probably used inheritance a lot. It feels natural. You create a base class, extend it, override a few methods, and move on. But as projects grow, inheritance often becomes hard to manage. Classes get tightly coupled. Changes ripple through the codebase. Small tweaks break unexpected...
Alarms, reminders, and notifications are some of the most deceptively complex features in Android development. On the surface, they look simple: “Schedule something at a time and show a notification.” In reality, you’re fighting with: Doze mode OEM background restrictions Process death App restarts Device reboots Android 12+ background execution limits Android 13+ notification permissions...
If you’ve ever used Hilt in a Jetpack Compose app, you’ve probably written code like this: Kotlinclass MyRepository @Inject constructor( @ApplicationContext private val context: Context ) And then paused for a second and thought: “Okay… but where is this @ApplicationContext coming from?” “Who creates it?” “And how does Hilt magically inject it at runtime?” This article answers...