A Practical Checklist for Resilient Android Features
A practical checklist for Android developers to make features resilient to process death, unreliable networks, large datasets, database changes, and system-level integration failures.
Table of Contents6 sections

A Ml Kit Mobile Cloud Dependency development workspace where lifecycle, data, and device behavior meet.
A feature is not production-ready because it works on a fast emulator. It is production-ready when it keeps behaving predictably after rotation, process death, a flaky connection, a stale cache, a database upgrade, or a system callback that arrives at an inconvenient time.
That changes the implementation question from “does the happy path work?” to “what happens when one layer stops cooperating?”
This checklist reviews resilience across four places where Android features commonly break: UI state, the data layer, system interactions, and verification. The goal is not to prescribe one architecture for every screen. It is to make failure behavior an explicit engineering decision.
1. Decide Which UI State Must Survive
A ViewModel is useful for keeping screen state through configuration changes, but process death is a different failure mode. Small pieces of state that are necessary to reconstruct a screen, such as a search query, selected filter, or item identifier, can be stored with SavedStateHandle.
class SearchViewModel(
private val savedStateHandle: SavedStateHandle
) : ViewModel() {
val searchQuery = savedStateHandle.getStateFlow("query", "")
fun updateQuery(newQuery: String) {
savedStateHandle["query"] = newQuery
}
}
The important design decision is not “put everything in saved state.” Persist the minimum state needed to reconstruct the experience, then reload heavier data from the appropriate repository or local source.
For a RecyclerView, restoration also needs to happen at the right moment. If an adapter restores its state while it is still empty, the user can return to the top of a list instead of the previous position.
adapter.stateRestorationPolicy =
RecyclerView.Adapter.StateRestorationPolicy.PREVENT_WHEN_EMPTY
A useful review question is simple: if Android kills this process now, what information is required to put the user back in a sensible state?
2. Choose the Data Strategy Before Adding Room
Room is not automatically the correct answer for every networked screen. The strategy should follow the product requirement.
| Strategy | Useful when | Main trade-off |
|---|---|---|
| Network-first | Freshness matters more than offline access | Weak experience during connectivity failures |
| Cache-first reads | Previously loaded content remains useful | Requires an explicit freshness policy |
| Database-backed Architecting Paging 2 With Room And Kotlin Flow For Offline | Large lists should remain browsable from local data | Adds synchronization and invalidation complexity |
| Offline-first writes | User changes must survive without connectivity | Requires retry, conflict, and sync semantics |
For screens that genuinely need offline-first reads, a local database can become the source the UI observes while the repository synchronizes remote changes into it. That keeps network availability from becoming a direct UI dependency.
Remote API -> Repository / sync policy -> Room -> Flow / Paging -> UI
The distinction matters. “We use Room” describes a library choice. “The UI reads persisted data while synchronization happens independently” describes failure behavior.
3. Treat Paging as a Contract, Not a Memory Optimization
Paging 3 is useful for large datasets, but production behavior depends on more than choosing a page size. When Room is the source of truth and remote data extends that local dataset, RemoteMediator can coordinate fetching and database writes.
Before shipping, define what happens when:
- refresh succeeds but append fails;
- cached rows exist while the network is unavailable;
- the backend returns duplicate or reordered records;
- an item changes while the user is several pages deep;
- the remote dataset is exhausted;
- a refresh invalidates keys used by the previous paging window.
These are data-contract questions, not UI polish.
RayLabs has a deeper walkthrough of the Room + Flow pagination boundary in Architecting Paging with Room and Kotlin Flow for Offline Use. The useful connection here is that pagination becomes more predictable when the UI observes one persisted source rather than trying to merge network and database state itself.
Keep network DTOs separate from Room entities as well. API representation and persistence schema evolve for different reasons; coupling them makes a backend change unnecessarily risky for local storage.
4. Make System Entry Points Explicit
Notifications, alarms, widgets, and other system interactions can fail even when the feature’s data architecture is sound.
For PendingIntent, explicitly choose mutability. On modern Android versions, immutable should be the default unless the receiving workflow genuinely needs another component to modify the intent.
val intent = Intent(context, DetailActivity::class.java).apply {
putExtra("item_id", itemId)
}
val pendingIntent = PendingIntent.getActivity(
context,
REQUEST_CODE,
intent,
PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
)
Do not treat this as a flag added only to satisfy the SDK. The mutability choice is part of the security boundary of the feature.
The same principle applies to background work: define whether the task must survive process death, whether it may be deferred, what constraints it needs, and what retry behavior is acceptable before choosing the scheduling mechanism.
5. Verify Failure Paths Deliberately
A production checklist becomes useful when each architectural promise has a corresponding test or manual verification scenario.
Lifecycle
- rotate or recreate the screen while a request is in flight;
- restore after process death with a saved query or selected item;
- return to a paged list and verify position restoration.
Database
- open the feature with an empty database;
- upgrade from the previous schema with real migration data;
- insert duplicate server records and verify the intended conflict policy;
- confirm that destructive migration is never an accidental fallback for user data.
Network
- start with cached data and remove connectivity;
- introduce high latency during refresh;
- fail append after earlier pages have loaded;
- return an empty or partially malformed payload and verify the error boundary.
System integration
- trigger notification or alarm entry points on supported OS versions;
- verify
PendingIntentmutability choices; - confirm background work retries without creating duplicate side effects.
The point is not to collect edge cases indefinitely. Each test should correspond to a failure the architecture claims to tolerate.
A Smaller Definition of “Production-Ready”
You do not need maximum architecture on every feature. A static settings screen and an offline field-work application should not carry the same synchronization machinery.
A better standard is proportional resilience:
- preserve only the UI state that users actually need restored;
- choose a cache or offline strategy based on product behavior, not architecture fashion;
- define paging and synchronization failure semantics before wiring the UI;
- make system security choices explicit;
- test the failure paths your design promises to survive.
That is the useful version of a production-ready Android checklist: not more layers, but fewer undefined behaviors.
Before marking a feature done, ask one final question: which failure can still leave the user in a state we have not designed for?
Continue Exploring
You Might Also Like

Mastering List to String Conversion in Mobile Development
An in-depth guide on handling list to string conversion, managing Android lifecycles, and avoiding memory leaks during state transformation.

Android Date and Time: Model Instants, Local Dates, and Time Zones Correctly
Learn how to model and format date and time in Android with Kotlin by separating absolute instants, local calendar values, time zones, localization, and testable presentation logic.

FCM Delivery Monitoring: Know What Sent, Delivered, and Opened Actually Mean
A practical guide to Firebase Cloud Messaging observability that separates send acceptance, aggregated delivery, app processing, and user interaction instead of treating one success response as proof of delivery.