Course outline

Android development / Advanced: reliable mobile systems and releases / Lesson 13

Architecture and process restoration

Design feature boundaries and recovery contracts so a recreated process can rebuild a coherent user task from small saved inputs and durable data.

About 4 min readContent reviewed
On this page

Lesson goals

  • Define sources of truth and dependency direction.
  • Reconstruct screen state after system-initiated process death.

Make responsibilities visible

A scalable architecture gives UI, data access, and business rules distinct responsibilities. The UI renders state and sends events; repositories coordinate data sources; optional domain operations encapsulate complex reusable rules. Introduce a domain layer when it clarifies behavior, not as an automatic wrapper around every getter. Dependencies should point toward contracts that express needs rather than toward activities or network implementations. Modules can enforce boundaries and ownership, but excessive splitting adds build and navigation cost. Start with a meaningful feature boundary and extract infrastructure only when its contract is understood.

Choose one authority per piece of state

A database may be the source of truth for confirmed records, while SavedStateHandle holds a transient search query needed to reconstruct a screen. A network response is an input to that truth, not a competing UI cache owned independently by each composable. Define how edits become authoritative and how failures are represented. Derived screen state can be rebuilt from durable records and small parameters. Keeping a second mutable list in the activity makes recovery and synchronization ambiguous because two copies can disagree without a policy explaining which one wins.

kotlin Reconstruct results from a small saved parameter
class SearchViewModel(
    private val saved: SavedStateHandle,
    private val repository: SearchRepository
) : ViewModel() {
    val query: StateFlow<String> = saved.getStateFlow("query", "")

    fun changeQuery(value: String) { saved["query"] = value }

    val results: StateFlow<List<Book>> = query
        .flatMapLatest { repository.observeMatches(it) }
        .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5_000), emptyList())
}

interface SearchRepository { fun observeMatches(query: String): Flow<List<Book>> }

Use lifecycle saved-state and Kotlin Flow APIs; flatMapLatest may require the opt-in specified by the selected coroutine version. Book is the domain model from the networking lesson. Supply a saved-state-aware ViewModel factory or DI integration so the handle participates in restoration.

Save reconstruction inputs

Saved state is a lightweight recovery aid tied to the system’s task-restoration contract. It is not permanent storage and does not guarantee restoration after force-stop, task dismissal, or every user-initiated exit. Save small ids, filters, and simple draft values; avoid complete datasets or large binary objects. A ViewModel instance itself does not survive process death. On reconstruction, validate saved parameters against current data and authentication state because a record may have been deleted or an account changed since the state was captured.

Inject dependencies at the composition root

A composition root creates application-scoped infrastructure and supplies smaller contracts to feature owners. This prevents feature code from looking up global mutable objects or constructing a new database on every screen. Use a factory or an established DI integration that understands ViewModel ownership and saved state. A manual factory is useful for a ViewModel without saved-state requirements, as shown below. Do not reuse it for the previous SavedStateHandle constructor without adding the supported creation-extras or saved-state-aware path; otherwise the code compiles in isolation while restoration silently disappears.

kotlin A manual factory for the earlier unsaved ShelfViewModel
class ShelfFactory(private val repository: ShelfRepository) : ViewModelProvider.Factory {
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        require(modelClass.isAssignableFrom(ShelfViewModel::class.java))
        @Suppress("UNCHECKED_CAST")
        return ShelfViewModel(repository) as T
    }
}

Import ViewModel and ViewModelProvider and use the repository contract from the Flow lesson. The checked cast is confined to the factory. Obtain the instance through the correct ViewModel owner; repeatedly calling this factory directly would bypass owner-managed retention.

Test recreation as a user journey

Rotation tests activity recreation while the process may stay alive; it cannot prove process-death recovery. Use a controlled Android test environment to background the task, recreate its process through supported tooling, and resume it without confusing that scenario with force-stop. Verify selection, draft policy, current database content, and pending-operation status. Repeat with a deleted record or expired session. Keep the expected fallback explicit so restoration never resurrects unauthorized content or pretends that an in-memory operation completed merely because a success banner had once been visible.

Common errors and fixes

Rotation passes but process restoration fails

Why it happens
All necessary inputs lived only in a ViewModel instance.
How to fix it
Save small reconstruction inputs and persist important user work.
Prevent it next time
Separate recreation tests from process-death tests.

Restored details show another account’s content

Why it happens
Saved identity was trusted without current authorization.
How to fix it
Revalidate account scope before loading the record.
Prevent it next time
Include logout and account-switch restoration cases.

Every screen creates another database instance

Why it happens
Infrastructure construction occurs inside feature rendering.
How to fix it
Create shared infrastructure at an appropriate application scope.
Prevent it next time
Review constructors and dependency ownership.

Check your understanding

1. A saved detail id exists but its row no longer does. Which source wins and what should recovery display?

Show worked solution

Current authorized storage wins. The id is a request to reconstruct a task, not proof the record remains valid. Show a not-found state and a safe navigation option, and remove stale selection if appropriate. Do not recreate a deleted record from an old UI snapshot.

2. A team proposes saving the entire search result list in SavedStateHandle. Replace that design.

Show worked solution

Save the small query and relevant filter or selected id. Rebuild results from the repository, whose durable source can supply current records. Preserve a loading or cached-state distinction during reconstruction. This reduces saved-state size and avoids restoring stale remote DTOs as if they were authoritative domain records.

Key takeaways

  • Sources of truth and lifetimes are architectural decisions.
  • Saved state reconstructs a task from small inputs.
  • Recovery must revalidate current data and account scope.

Official references