Prefer explicit state to boolean combinations
For a loading screen, a small sealed state can prevent impossible combinations such as “loading and failed and showing success”:
sealed interface LoadState {
data object Idle : LoadState
data object Loading : LoadState
data class Ready(val itemCount: Int) : LoadState
data class Failed(val message: String) : LoadState
}
This is an illustrative model. Avoid placing sensitive raw exception details in a user-visible message.
Make ownership visible
| Design choice | Question it should answer |
|---|---|
| Repository boundary | Who owns data retrieval and persistence? |
| Dependency injection | Who creates and disposes of long-lived dependencies? |
| Immutable state | Which component may change the displayed state? |
| Small interface | What behavior does a consumer actually need? |
| Structured asynchronous work | When is work canceled or allowed to outlive a screen? |
A global singleton holding an activity is still a lifecycle risk even if it is called a service locator. The Android memory guidance explains why retained references matter.
Use nullability as a contract
Follow Kotlin's null-safety model. A non-null type is useful only when values entering from Java, serialization or external data are validated appropriately. Replacing a failing safe call with !! often moves uncertainty into a runtime crash rather than resolving it.
Prefer explicit empty, unavailable and error states when they have different meanings to the feature.
Keep conventions boring
The Kotlin coding conventions help reduce unnecessary variation. Consistent names and small cohesive files improve review more than clever syntax that hides control flow.
Validate the design with a change
Ask how the code handles a retry, canceled request, offline state or replacement dependency. If a simple change requires editing many unrelated layers, the abstraction may be adding ceremony without protecting a useful boundary.
APKLint's Kotlin and static-code tools can flag patterns, but architecture quality requires reviewing the actual responsibilities and tests. Do not treat a zero-warning snippet as proof that lifecycle and state ownership are correct.
Use a state model that identifies the current search
Consider a search screen where query A is slow and query B finishes first. A sealed loading state makes each displayed condition explicit, but it does not automatically prevent A's late result from replacing B. Keep the request identity or cancellation ownership with the state transition, and accept a result only for the query that remains current.
Kotlin's sealed-class and interface documentation explains the language mechanism. Use it to make the supported states clear, such as loading, usable results and a recoverable error. Decide separately whether an empty successful search is a distinct state or a valid result with an empty collection.
Make the repository's result contract specific
A repository interface should explain what happens when the service is unavailable, the response is malformed or no matches exist. Returning null for all three loses information needed by the screen. Model the outcomes the feature actually distinguishes instead of adding abstractions only because a design-pattern name sounds familiar.
At Java or serialization boundaries, validate values before treating them as trusted non-null domain objects. The null-safety guide describes where runtime null failures can still arise. Replacing a compiler complaint with a not-null assertion does not supply missing input validation.
Test the state transitions using a controllable repository: complete B before A, fail the current query, cancel a request and retry with a second input. Assert the visible query and result together. Checking only that a Ready state exists can miss a stale result associated with the wrong query.
Review lifetime independently from state syntax. A long-lived dependency should not retain an activity merely to deliver search results. Choose an owner appropriate to the work and define whether navigation cancels it or permits a later result to be retained for the same feature.
Use these scenarios to evaluate the abstraction. If replacing the repository or adding an offline result requires touching unrelated UI and transport code, refine the responsibility boundary. The value of the pattern is that ownership and behavior remain understandable when the feature changes.
Sources and further reading
Reference review: 29 September 2026. Examples illustrate the workflow; check your installed versions, release artifact and account-specific Console requirements before applying them. This guide is not a claim that APKLint executed your project or verified your private account.


