Android Architecture
Background work on Android — Services, WorkManager, and Doze restrictions.
10 questions
JuniorTheoryVery commonWhat are viewModelScope and lifecycleScope, and when is each one cancelled?
What are viewModelScope and lifecycleScope, and when is each one cancelled?
viewModelScope is tied to a ViewModel and cancelled in onCleared(). lifecycleScope belongs to an Activity or Fragment, cancelled on onDestroy. Both exist so no coroutine outlives the component that started it — hence not GlobalScope.
Common mistakes
- ✗Launching a screen's coroutine in
GlobalScope, so it outlives the destroyed component - ✗Thinking
viewModelScopeis cancelled on a configuration change rather than inonCleared() - ✗Assuming a cancelled scope's coroutines resume by themselves when the screen returns
Follow-up questions
- →Which dispatcher does
viewModelScopeuse by default, and can you change it? - →What happens to a coroutine already suspended inside a scope that gets cancelled?
JuniorTheoryVery commonDoes a ViewModel survive a screen rotation, and does it survive process death?
Does a ViewModel survive a screen rotation, and does it survive process death?
A ViewModel is scoped to the ViewModelStoreOwner, so it survives a configuration change: on rotation the Activity is recreated, the ViewModel is not. It does not survive process death — the OS kills the process and its memory. Hence SavedStateHandle.
Common mistakes
- ✗Believing a
ViewModelalso survives process death, so no state ever needs saving - ✗Thinking a
ViewModelis recreated on every rotation just like itsActivity - ✗Confusing
ViewModelretention with the saved-instanceBundlemechanism
Follow-up questions
- →What does the framework actually retain across a rotation to hand back the same
ViewModel? - →Which state belongs in
SavedStateHandlerather than in a plainViewModelfield?
JuniorTheoryCommonWhen do you pick WorkManager over a foreground Service for background work?
When do you pick WorkManager over a foreground Service for background work?
Use WorkManager for deferrable, guaranteed work that must survive process death and app restarts — it persists and reschedules. Use a foreground Service for user-visible work that must run right now and continuously, like media playback or a watched upload.
Common mistakes
- ✗Thinking a
Serviceruns off the main thread by default - ✗Believing
WorkManagercannot defer or persist work across reboots - ✗Using a foreground
Servicefor deferrable work that does not need to run now
Follow-up questions
- →How does Doze mode restrict a
ServiceversusWorkManager? - →Why must a foreground
Servicepost an ongoing notification?
JuniorTheoryCommonWhat is the repository's job in a layered Android app, and what must it hide?
What is the repository's job in a layered Android app, and what must it hide?
The repository is the single entry point to a feature's data: it owns the network and database sources, decides which answers a request, and exposes domain models upward. The ViewModel never sees Retrofit or Room — so you can test it with a fake.
Common mistakes
- ✗Injecting Retrofit or Room straight into the
ViewModeland still calling it a repository - ✗Leaking network DTOs or Room entities upward instead of exposing domain models
- ✗Treating the repository as a cache rather than the owner of the data sources
Follow-up questions
- →How does a repository decide between a cached local answer and a fresh network fetch?
- →What does the repository return so the
ViewModelstays testable with a fake?
MiddleTheoryCommonHow does structured concurrency map onto the Android lifecycle, and what is cancelled when?
How does structured concurrency map onto the Android lifecycle, and what is cancelled when?
Each scope owns a parent Job, so cancelling the scope cancels every child coroutine, nested ones included. lifecycleScope is cancelled on every Activity/Fragment destruction, so a rotation kills it; viewModelScope survives that and is cancelled only in onCleared(). Work that must outlive a rotation belongs in viewModelScope.
Common mistakes
- ✗Thinking a nested
launchkeeps running when its parent scope is cancelled - ✗Believing a rotation cancels
viewModelScopethe way it cancelslifecycleScope - ✗Putting rotation-surviving work in
lifecycleScopeand losing it on every turn
Follow-up questions
- →What happens to a coroutine suspended at a cancellation point when its scope is cancelled?
- →Why does a
FragmentneedviewLifecycleOwner.lifecycleScoperather than its ownlifecycleScope?
MiddleDesignCommonYou are building a Compose screen for a product catalogue. Constraints:
- The screen holds several interacting pieces of state — a search query, active filters, a sort order, a paged list, a loading flag and an error banner — and one user action often changes several of them at once.
- Every change must produce a consistent frame: the list, the filter chips and the loading flag must never disagree with each other on screen.
- QA must be able to reproduce a reported bug from a recorded sequence of user actions.
- The team is small and does not want to hand-write large amounts of boilerplate.
Compare the architectural pattern MVVM (the ViewModel exposes several observable state holders and public methods the view calls) with the architectural pattern MVI (every user action becomes an intent handled by a reducer that produces one immutable state object). Say who owns the screen state, how a user action reaches it, and which pattern you would choose here and why.
You are building a Compose screen for a product catalogue. Constraints:
- The screen holds several interacting pieces of state — a search query, active filters, a sort order, a paged list, a loading flag and an error banner — and one user action often changes several of them at once.
- Every change must produce a consistent frame: the list, the filter chips and the loading flag must never disagree with each other on screen.
- QA must be able to reproduce a reported bug from a recorded sequence of user actions.
- The team is small and does not want to hand-write large amounts of boilerplate.
Compare the architectural pattern MVVM (the ViewModel exposes several observable state holders and public methods the view calls) with the architectural pattern MVI (every user action becomes an intent handled by a reducer that produces one immutable state object). Say who owns the screen state, how a user action reaches it, and which pattern you would choose here and why.
In MVVM the ViewModel owns several independent state holders and the view calls its methods directly — less ceremony, but fields can drift out of sync mid-update. MVI routes every action through one intent stream into a reducer emitting a single immutable state, so each frame is consistent and a bug replays from the log; the price is boilerplate. With these interacting fields, choose MVI.
Common mistakes
- ✗Treating a single immutable state as pure ceremony with no consistency benefit
- ✗Splitting a screen into many independent state holders, then fighting the frames where they disagree
- ✗Believing Compose recomposition alone guarantees the screen's fields agree with each other
Follow-up questions
- →How does a reducer in the architectural pattern
MVImake a reported bug reproducible from a log? - →When is the boilerplate of the architectural pattern
MVInot worth it on a simple screen?
SeniorDebuggingCommonWhy does this Fragment leak its whole view hierarchy on every navigation?
Why does this Fragment leak its whole view hierarchy on every navigation?
The hand-made scope is never cancelled, so its collecting coroutine outlives onDestroyView and keeps holding binding — one dead view hierarchy is retained per visit. Collect from viewLifecycleOwner.lifecycleScope inside repeatOnLifecycle(STARTED), which is cancelled when the view dies, or cancel the scope yourself in onDestroyView.
Common mistakes
- ✗Creating a
CoroutineScopeby hand in aFragmentand never cancelling it - ✗Collecting a view-bound flow in
lifecycleScoperather thanviewLifecycleOwner.lifecycleScope - ✗Blaming the dispatcher or the binding field instead of the uncancelled scope
Follow-up questions
- →Why is
viewLifecycleOwnerthe right owner for aFragment's view-bound collection? - →What does
repeatOnLifecycle(STARTED)do that a barelaunchin the same scope does not?
MiddleTheoryOccasionalWhat does SavedStateHandle preserve across process death that a ViewModel field does not?
What does SavedStateHandle preserve across process death that a ViewModel field does not?
SavedStateHandle is a key-value map backed by the saved-instance Bundle the system writes before killing the process, so its entries are restored into the recreated ViewModel. A plain ViewModel field lives only in RAM and is gone. It is Bundle-sized: keep small identifiers there — a query, a selected id — and re-load the data from the repository.
Common mistakes
- ✗Believing a plain
ViewModelfield survives process death, so nothing needs saving - ✗Stuffing a whole loaded list into
SavedStateHandleinstead of a small identifier - ✗Treating
SavedStateHandleas a persistent cache that outlives a swipe-away
Follow-up questions
- →How would you expose a
SavedStateHandleentry to the UI as observable state? - →Why does a large value in
SavedStateHandlerisk aTransactionTooLargeException?
SeniorDesignOccasionalDesign the background sync for an Android note-taking app: it pulls changes from the server and writes them into the local database. Constraints:
- The sync must eventually run even if the user never opens the app again, and it must survive process death and a device reboot.
- It may run only on an unmetered network and only while charging, and it must stop as soon as those conditions stop holding.
- It runs roughly every hour; enqueuing it twice must not start two parallel syncs.
- A server 503 or a dropped connection is transient and must be retried with a growing delay; a 401 is permanent and must not be retried forever.
- The sync body is written with coroutines and must be cancellable at every step.
Describe the mechanism you would use, how it survives a reboot, how the device conditions are applied, how a duplicate enqueue is handled, and how a retryable failure is distinguished from a permanent one.
Design the background sync for an Android note-taking app: it pulls changes from the server and writes them into the local database. Constraints: - The sync must eventually run even if the user never opens the app again, and it must survive process death and a device reboot. - It may run only on an unmetered network and only while charging, and it must stop as soon as those conditions stop holding. - It runs roughly every hour; enqueuing it twice must not start two parallel syncs. - A server 503 or a dropped connection is transient and must be retried with a growing delay; a 401 is permanent and must not be retried forever. - The sync body is written with coroutines and must be cancellable at every step. Describe the mechanism you would use, how it survives a reboot, how the device conditions are applied, how a duplicate enqueue is handled, and how a retryable failure is distinguished from a permanent one.
Enqueue a PeriodicWorkRequest for a CoroutineWorker carrying Constraints for an unmetered network and charging; WorkManager persists the request in its own database, so it survives process death and reboot. Dedupe with enqueueUniquePeriodicWork(KEEP). Return Result.retry() on a 503 to get exponential backoff and Result.failure() on a 401. doWork is suspend, so it is cancelled when a constraint stops holding.
Common mistakes
- ✗Assuming constraints are checked only at enqueue time and not while the worker runs
- ✗Returning
Result.retry()for a permanent failure such as a 401, so the job retries forever - ✗Relying on
AlarmManageror aGlobalScopeloop for work that must survive a reboot
Follow-up questions
- →What backoff policy does the job scheduler
WorkManagerapply to aResult.retry(), and how would you tune it? - →When is
REPLACEthe right unique-work policy instead ofKEEP?
MiddleDesignRareDesign an upload manager for an Android app that uploads photos and videos to a server. Requirements:
- It must not block the UI thread; uploads run in the background while the user keeps using the app.
- Files are large (hundreds of MB), so an upload may run for many minutes.
- The upload must continue if the user backgrounds the app, and ideally survive process death and resume.
- The user must be notified of the result (success or failure).
Specify which background-execution mechanism you choose (a foreground Service, the WorkManager job scheduler, or both), how it behaves under Doze power-saving restrictions, how the background worker communicates progress and the final result back to the UI, and what happens to an in-flight upload if the app process is killed mid-transfer.
Design an upload manager for an Android app that uploads photos and videos to a server. Requirements:
- It must not block the UI thread; uploads run in the background while the user keeps using the app.
- Files are large (hundreds of MB), so an upload may run for many minutes.
- The upload must continue if the user backgrounds the app, and ideally survive process death and resume.
- The user must be notified of the result (success or failure).
Specify which background-execution mechanism you choose (a foreground Service, the WorkManager job scheduler, or both), how it behaves under Doze power-saving restrictions, how the background worker communicates progress and the final result back to the UI, and what happens to an in-flight upload if the app process is killed mid-transfer.
Use WorkManager with a long-running worker promoted via setForeground (which posts the required ongoing notification). It persists the job, so it survives process death and reschedules to resume after a kill. Doze allows a foreground worker to keep running. Report progress with setProgress, observed via a WorkInfo LiveData/Flow in the UI.
Common mistakes
- ✗Running a multi-minute upload on a raw
Threadwith no persistence across process death - ✗Assuming a started
Serviceis exempt from Doze without going foreground - ✗Believing
START_STICKYre-delivers the original payload rather than a nullIntent
Follow-up questions
- →Why does a long-running
WorkManagerworker have to callsetForeground? - →How would you resume a partially uploaded large file rather than restarting it?