Everything the adaptive question bank can teach and test in Android Development, from foundations through advanced practice. Work through it in order, or start practicing and let the questions find your level.
These are the only things the system knows how to launch, which means every way into an app is one of them. Anything else — a class, a function, a coroutine — runs only because a component started it, so the entry points are a short and complete list.
The framework constructs your class and calls back into it, so control flow is inverted relative to an ordinary program. Every lifecycle rule follows from that: you are responding to the system's decisions rather than driving your own sequence.
Separate user identity and process per app means isolation is enforced by the operating system rather than by convention. Sharing anything requires an explicit mechanism, which is why file paths from another app are useless without a grant.
An undeclared component cannot be started at all, and the failure surfaces at runtime rather than at build time. That timing is the trap — the code compiles, the navigation looks right, and the crash arrives on the device.
It decides whether other applications can reach the component, and recent platform versions refuse to build when the value is ambiguous. That build failure is deliberate: the previous implicit default was a frequent source of accidental exposure.
Three numbers answering three different questions — who can install, what compiles, and which behavior changes apply. Raising the target is the one that changes runtime behavior, which is why it is the one that breaks working apps.
B. The activity lifecycle, configuration changes and process death
Three mirrored pairs, always entered in one order and unwound in the reverse, which makes the correct place for an acquisition and its release a matching pair rather than a judgment. Most lifecycle bugs are an acquire and a release in mismatched pairs.
Visible and focused are different states, and a dialog over your activity leaves it visible without focus. Treating them as one is why work stops when the screen is still showing something the user is looking at.
The incoming screen cannot proceed until this callback returns, so anything slow there is a visible stall during a transition the user is watching. It is the callback with the tightest budget and the one most often used for saving.
Hardware and animations pair with the visibility callbacks rather than the focus ones, because a merely unfocused activity may still be on screen. Releasing the camera too early produces a blank preview behind a dialog.
The argument is null on a genuine first creation and non-null on a recreation, which is the only reliable way to distinguish the two inside the callback. Branching on it is how one-time setup stays one-time.
It runs when the activity might be killed rather than when the user is finished with it, so it fires on rotation and on backgrounding. It is sized for small transient state, which is why it is the wrong place for anything that belongs on disk.
Rotation is the familiar case and the list is longer — locale, font scale, theme — so any of them destroys and rebuilds the screen. Claiming them in the manifest to avoid the recreation means handling every one of them by hand.
It survives the recreation and dies with the process, which makes it the right holder for screen state and the wrong one for anything that must outlive the app being killed in the background. Two different survival requirements need two different mechanisms.
C. Tasks, the back stack and launch modes
The stack is a history rather than a structure you design, so Back returns to whatever was actually visited. That is what makes the up-versus-back distinction later in this chapter necessary rather than pedantic.
A task can contain activities from several applications, so the back stack describes a user's journey rather than one app's screens. That is why Back can leave your app mid-flow and why the stack is not yours to assume.
One always creates a new instance and the other reuses an existing one only when it is already on top. The narrowness of that condition is the point — a matching instance further down the stack is not reused.
A reused instance receives the new intent through a different callback, and the original intent remains what the getter returns until it is explicitly replaced. Reading the stale one is a classic source of a screen showing the previous item.
One follows the app's declared hierarchy and the other follows history, so they diverge whenever the user arrived from somewhere unexpected. Implementing Up as Back is the shortcut that breaks deep links into the middle of an app.
D. Intents and intent filters
One names the exact class and the other describes what you want done and lets the system find a handler. The second is how apps interoperate and is also where another application's code ends up running, which is why the resolution rules matter.
Three tests must all pass — the action, every category, and the data specification — so a filter that matches two of three delivers nothing. Debugging a non-resolving intent means checking all three rather than the obvious one.
An implicit intent started as an activity is automatically given a default category, so a filter that does not declare it can never match. It is a single missing line producing a component that is unreachable for no visible reason.
Anything arriving at an exported component came from another process and is under someone else's control, which makes it external input regardless of how it looks. Validating it is the same discipline as validating a network response.
Recent platform versions hide most other applications unless you declare what you need to see, so an intent that used to resolve now finds nothing. The failure is silent — resolution simply returns no match — which makes it hard to attribute.
The replacement requires registration during initialization, before the lifecycle reaches a started state, because the result can arrive during recreation. Registering conditionally or late is the mistake, and it produces a result that is never delivered.
A pending intent executes with your application's identity in another process, which is why the platform now requires the mutability to be stated explicitly. A mutable one handed to another app is a capability, not a message.
E. Fragments and the View system
The fragment outlives its view, so an observer registered against the fragment keeps firing after the view it updates has been destroyed. Using the view's own lifecycle owner is what binds the subscription to the thing it touches.
A binding reference held past view destruction pins the entire detached hierarchy in memory, which is a leak proportional to the screen's complexity. Clearing it is a one-line habit that prevents a whole class of retention.
Adding a transaction to the back stack is what makes Back reverse it; without that, the transaction is permanent and the system button does something else entirely. It is an opt-in, which surprises people expecting Back to undo.
Three passes where nesting multiplies the measurement cost, which is why deep hierarchies are expensive in a way that looks linear in the markup. Flattening is the standard remedy and the reason the constraint-based layout exists.
It hosts a traditional view inside the declarative system, with an update block that runs whenever the surrounding state changes. That block is the bridge, and doing the work in the factory instead means it never updates.
It embeds declarative UI inside an existing traditional layout, which is what makes incremental migration possible screen by screen. The two interop directions together mean a codebase never has to convert all at once.
F. Compose: composition, recomposition and stability
You never mutate a widget — you change state and let the description re-run, which removes an entire category of bugs where the view and the model disagree. Every other rule in this chapter is a consequence of that one.
Only the scopes that actually read a changed value re-run, so the read site determines the cost of a write. That makes where you read state a performance decision, which is the subject of deferring reads later in this chapter.
Functions may run in any order, in parallel, be skipped entirely, or re-run every frame, so a side effect in the body fires an unpredictable number of times. That is why effects have their own dedicated constructs rather than being written inline.
A function is skipped when its parameters are stable types and compare equal to last time, which is what makes recomposition cheap in practice. Losing stability on one parameter silently removes the optimization for that whole subtree.
The interface can be backed by a mutable implementation, so the compiler cannot assume equality means unchanged. Passing one defeats skipping across the whole call site, which is why immutable collection types matter more here than elsewhere.
A lambda capturing an unstable value is itself unstable, so a child recomposes even when the data it displays has not changed. It is the least visible cause of unnecessary recomposition, because the parameter list looks fine.
Reading state in the lowest function that needs it keeps every ancestor out of the recomposition, which converts a whole-screen update into a single text change. It is the highest-leverage habit in this area.
It passes a value implicitly down a subtree rather than through every intermediate parameter, which suits genuinely ambient things like theme and locale. Using it for ordinary data hides the dependency, which is the trade being made.
G. Compose state and state hoisting
It caches across re-runs of the same composition and nothing more, so it is gone on recreation. Knowing its exact lifetime is what separates it from the saveable variant and from a view model, which cover longer horizons.
Without remembering it, a fresh state object is allocated on each recomposition and every write is immediately discarded. The symptom is a value that appears not to change, which sends people looking at the writer rather than the declaration.
Subscription happens at the read, not at the declaration, so a state written but never read notifies nobody. That is also why moving a read changes which scopes recompose — the dependency is established dynamically.
It writes into the saved instance state, so it survives rotation and process death that plain remembering does not. The cost is that the value must be storable in a bundle, which is the subject of the next concept.
Anything not already bundle-compatible needs an explicit converter, and an ordinary data class is not compatible by default. Discovering that at runtime rather than at compile time is the usual first encounter.
Passing the value down and the event up leaves exactly one owner of the state, which makes the component reusable and testable. It is the same unidirectional principle the architecture chapter states at a larger scale.
It recomputes whenever a source changes and notifies only when the result differs, which throttles a rapidly-changing input into occasional updates. It is the fix for a scroll position driving a boolean that changes twice.
H. Compose side effects
The coroutine starts on entering composition and is canceled and restarted whenever a key changes, which makes the key list the control over the effect's lifetime. Choosing keys carelessly either restarts too often or never restarts when it should.
A constant key means the effect runs once and never again, which is right for genuine one-shot work and wrong whenever the effect depends on data that can change. It is the most common effect bug and it looks correct in review.
Anything registered must be unregistered, and this construct pairs the two so the cleanup runs on leaving composition or before a keyed restart. Registering inside a launched effect leaves the cleanup unpaired.
An effect cannot be started from a click handler, because handlers are not composition, so launching from a callback needs a scope remembered for the purpose. Using the wrong one produces work canceled or leaked at the wrong moment.
It captures the latest value inside a long-lived effect that must not restart, which resolves the tension between a stable key list and fresh data. It exists precisely because the two requirements otherwise conflict.
It publishes to non-declarative code after each successful composition and cannot suspend, which makes it the bridge outward rather than a way to do work. Reaching for it to perform asynchronous work is a category error.
I. Compose layout, modifiers and the three phases
Composition decides what exists, layout decides where and how big, drawing decides appearance — and a frame can skip the earlier phases entirely. That skipping is why an animation driven at the right phase costs almost nothing.
Each child is measured exactly once, which is what makes layout cost linear rather than exponential in depth. Arbitrary multi-pass measurement is rejected for that reason, which is why some traditional layout techniques have no direct equivalent.
The chain applies outside-in, so the same two modifiers in opposite orders produce visibly different results. It is the most common source of surprise for newcomers and it is entirely predictable once the direction is known.
Padding before the click handler is inside the touch target and padding after is outside it, which changes whether the spacing is tappable. It is the ordering rule with the clearest usability consequence.
Parents pass constraints downward and children report their resolved sizes upward, which is the same protocol the traditional system uses. Knowing it explains why a child cannot exceed what it was given and why some sizing requests are ignored.
The block forms read state during layout or drawing rather than during composition, so an animation through them skips recomposition entirely. Choosing the block form over the value form is a large and easily-missed performance difference.
Three containers covering vertical, horizontal and overlapping arrangement, which compose to express most layouts without anything more specialized. Reaching for a constraint-based layout before these is usually premature.
One distributes children along the container's main axis and the other positions them across it, which is why the two parameter names are not interchangeable. Getting them the wrong way round is the commonest layout confusion.
J. Lazy lists and scrolling
Only visible items are composed, which is the difference between a list that scrolls smoothly at a thousand items and one that does not exist until every row is built. A scrollable column of the same content composes all of it.
Without a key, remembered state is bound to the position rather than the item, so inserting a row shifts every subsequent row's state onto the wrong data. The symptom is a checkbox or an animation attached to the wrong entry after an update.
Declaring the shape of an item lets compositions be reused between items of the same type in a mixed list, which avoids rebuilding from scratch on every scroll. It matters in proportion to how heterogeneous the list is.
A scrollable child inside a scrollable parent on the same axis is measured with unbounded constraints and throws, because neither can decide how large the other should be. The remedy is one scrolling container with the content as items.
K. Architecture: layers, ViewModel and unidirectional data flow
Dependencies point one way only, which is what lets a lower layer be tested and replaced without the layers above knowing. A reference pointing back up is the violation that turns a layered design into a graph.
One component owns the reconciliation of remote and cached data so the screen observes a single authoritative stream. Without it, each screen invents its own merge and they disagree about what is current.
State moves down and events move up, so no component writes state it does not own. It is the same hoisting principle applied at application scale, and it is what makes a state change traceable to one writer.
It is tied to a lifecycle owner and cleared only when that owner is genuinely finishing, which is why it survives a rotation and not a back press. Confusing those two moments is why work is sometimes canceled unexpectedly.
Holding an activity or a view leaks it across the recreation the component exists to survive, which is precisely the opposite of its purpose. It is the most common leak in this architecture and it is caught by a static check.
It provides key-value state that survives the process being killed, which an ordinary field does not. That makes it the piece covering the gap between a rotation and a cold restart from the background.
A single immutable state object cannot produce an inconsistent intermediate frame, which several independent streams can — one updates, the screen renders, the second arrives. It trades a little verbosity for removing a class of flicker.
Supplying dependencies rather than constructing them in place is what makes a layer substitutable in a test. The framework generates the wiring, so the benefit is the inversion rather than the tooling.
L. Coroutines, flows and lifecycle-aware collection
Work launched in it is canceled when the holder is cleared, which means the cancellation is structural rather than something to remember. Launching in a scope that outlives the screen is how work continues against a destroyed UI.
A suspending function moves itself off the main thread, so callers never need to know where it runs. That convention is what allows a repository to change its threading without every call site changing.
Collection suspends until the flow completes, so two collections written one after another in a single coroutine means the second never starts. Each needs its own coroutine, which is the fix that looks redundant until you hit it.
It cancels collection when the app is backgrounded and restarts it on return, whereas the older helper merely suspends and leaves the upstream producer running. That difference is the whole reason the newer one exists.
The lifecycle-aware collector stops consuming while the app is in the background; the plain one keeps going. Using the plain one is a silent battery and data cost that nothing in the code makes visible.
It always holds a value, drops intermediate emissions under load, and suppresses an emission equal to the current one. All three behaviors suit state and none of them suits events, which is why events need a different type.
M. Navigation
One activity hosts many destinations, so navigating manipulates a back stack rather than launching components. That is what makes shared state between screens straightforward and what makes the task rules mostly irrelevant within an app.
Each graph has a fixed starting point, and it is the last screen Back reaches before leaving the app. Choosing it is therefore a decision about what the app's home is, not merely about what shows first.
Popping up to a destination optionally removes that destination too, which is how a login screen is erased so Back cannot return to it. The inclusive flag is the difference between hiding and removing.
It prevents a second copy of the destination already on top, which is what stops a repeated tap building a stack of identical screens. It is the navigation equivalent of the task launch mode.
An argument should carry an identifier the destination uses to re-read from its own layer, because a serialized object is a snapshot that can be stale by the time the screen renders. It also keeps arguments small enough for saved state.
N. Persistence: Room, DataStore, files and scoped storage
One needs no permission and disappears on uninstall; the other outlives the app and is subject to the platform's media rules. Choosing the wrong one produces either an unnecessary permission prompt or data users cannot find.
Access to other applications' media goes through a mediated interface rather than raw paths, so code that builds a file path and opens it fails on modern versions. The remedy is a different API rather than a permission.
Queries are validated against the schema at build time, so a misspelled column is a compile error rather than a crash on the device. That single property is most of the argument for using it over raw database access.
A suspending function returns once; a reactive return type re-emits whenever the underlying tables change. The choice of return type is therefore a behavioral decision that looks like a typing preference.
Main-thread access throws by default rather than being permitted and slow, which converts a latent responsiveness problem into an immediate and obvious failure. It is a deliberate design choice rather than a limitation.
A schema version change without a migration throws on open, and the destructive fallback silently discards user data instead. Enabling that fallback to make a crash go away is how a release loses everyone's data.
One is asynchronous and transactional; the other exposes a synchronous commit that blocks its caller, including on the main thread. The migration path exists because the older API's convenience was the problem.
Key-value storage sits unencrypted inside the sandbox, so it is protected by the sandbox and nothing else. Tokens and keys therefore do not belong there, on a device where the sandbox may not hold.
O. Networking
The platform throws rather than allowing it, which makes the rule impossible to violate accidentally in production. It is one of the few places where a design mistake is prevented rather than documented.
It is granted at install with no runtime prompt, which means there is no denial path to handle for it. Writing permission-request code for it is a common and harmless waste that suggests a misunderstanding of the tiers.
Plain unencrypted traffic is refused unless a configuration explicitly permits that domain, which usually surfaces first when pointing a debug build at a local server. The fix is a configuration file rather than a code change.
A connected network says nothing about whether your server responds, so connectivity checks do not remove the need for timeouts and retries. Treating the check as a precondition produces an app that hangs on a captive portal.
One turns an interface declaration into typed HTTP calls and the other is the client that actually performs them. Knowing which layer does what is how an interceptor, a timeout or a certificate problem is attributed correctly.
P. Background work, services and execution limits
Work that can wait and must eventually happen uses the scheduler; work the user is actively watching uses a foreground service. Choosing by convenience rather than by that distinction is what produces both battery complaints and killed work.
A backgrounded app cannot start an ordinary background service and the call throws, which is a platform restriction rather than a deprecation. Code written before the change fails at runtime with an exception naming the restriction.
It must post a visible notification and, on recent versions, declare a type backed by a matching permission. The typing requirement exists to make the justification explicit rather than implied by the notification alone.
Constraints defer work until conditions are met rather than failing it, and the work survives a reboot. That persistence is the guarantee being bought, and it is why the scheduler is heavier than simply starting a coroutine.
Naming work and choosing what happens when it is enqueued again is what prevents a relaunch queueing duplicates. The three policies express three different intentions, and the default is rarely the one wanted.
Deferred work is batched into maintenance windows when the device is idle, so anything needing to happen at a specific moment requires an exact alarm and its own permission. Assuming a scheduled job fires on time is the mistake.
Most implicit broadcasts no longer wake a manifest-declared receiver, so a receiver that worked silently stops. Registering at runtime restores it for as long as the app is alive, which is the trade the restriction imposes.
Q. Permissions
One tier is granted on installation and the other must be requested and can be revoked afterwards. The revocability is what makes the check-at-use rule necessary rather than a formality.
A grant can be withdrawn while the app is not running, so a check performed at startup proves nothing later. The check belongs immediately before the use, which is a habit rather than an architecture.
The result arrives in a callback, so the statement after the request executes before the user has answered anything. Writing the dependent code inline is a sequencing error that appears to work when the permission was already granted.
The signal is true only in the window after one denial and before a permanent one, which is exactly when an explanation can still change the outcome. Showing a rationale outside that window is either premature or futile.
After a permanent denial the system shows no dialog and returns a refusal immediately, so the request appears to do nothing. The only remaining route is sending the user to settings, which requires detecting the state rather than retrying.
Several system-provided pickers return the same data with no permission requested at all, which is both better for the user and less code. Requesting broad access where a picker exists is the pattern platform reviews increasingly reject.
R. Notifications and channels
A notification posted without a channel is not displayed at all on modern versions, and nothing errors. It is a silent no-op, which is why the symptom is usually reported as notifications not working rather than as a missing channel.
Once a channel exists the user controls its importance, sound and vibration, and the app cannot raise them again. That is deliberate — it moves the decision permanently to the person being interrupted.
Posting is now a runtime permission the user can refuse, so an app that always showed notifications may now show none. It needs the same request-and-handle-denial treatment as any other runtime permission.
Reusing an identifier replaces the existing notification rather than adding another, which is how progress is updated in place. Using a fresh identifier each time is what produces a stack of near-identical notifications.
S. Resources and configuration qualifiers
The platform selects from qualified folders at runtime, so dark mode, orientation and language need no branching in code at all. Writing that branching by hand is reimplementing something the resource system already does better.
An unqualified default is required, because a configuration you did not anticipate must still resolve. Omitting it produces a lookup failure on a device you never tested rather than a build error.
Drawables are qualified by density and the system scales the nearest available one when an exact match is missing. Providing only one density means every other device is showing a scaled image, which is usually visible.
Density-independent units resolve to different pixel counts per device, which is what keeps physical size consistent. A hardcoded pixel value is a different physical size on every screen, which is the defect this unit exists to prevent.
The text unit additionally honors the user's font-size preference, which is an accessibility setting rather than a styling one. Sizing text in the other unit ignores that preference silently, which is why it is never correct for text.
Direction-aware attributes mirror automatically in right-to-left locales while the absolute ones stay put. Using the absolute pair is invisible until the app is localized, at which point the layout is wrong everywhere at once.
T. Build: Gradle, variants, R8 and delivery
Variants are the cross product of flavors and build types, so adding one flavor multiplies rather than adds. That multiplication is where an unmanageable matrix of build configurations comes from.
The development type is debuggable and signed with a shared key; shrinking and your own key belong to the release type. Most release-only failures trace to the differences between the two rather than to the code.
Unreachable code is removed and what remains is renamed, which is why anything resolved by name at runtime breaks only in release builds. Reflection and serialization are the two that always need attention.
A rule names what must survive shrinking and renaming, which is how reflectively-referenced code stays reachable. Writing the rules too broadly gives up the size benefit, and too narrowly produces a release-only crash.
An update must be signed by the same key as the installed version, which makes the key the app's identity rather than a build artifact. The platform's key custody exists because losing it once meant losing the ability to update.
The store generates device-specific packages from the bundle, so the bundle itself cannot be installed on a device. Attempting to sideload one is a common first confusion when moving to the newer format.
One is an integer the store orders releases by and must increase; the other is a display string with no constraints. Conflating them produces either a rejected upload or a version users cannot interpret.
The tracks trade audience size against the cost of a mistake reaching people, which makes the progression through them a risk decision. Skipping straight to the widest one removes the only cheap place to find a problem.
Releasing to a percentage bounds how many users a bad build reaches and allows halting before it reaches everyone. It only helps if something is watching the metrics during the window, which is the half people omit.
U. Performance: jank, startup, memory and ANRs
Jank is a missed deadline against the display's refresh interval, so it is a worst-case property — an app with excellent average frame times and occasional long ones feels bad. Measuring the tail is the only measurement that matches perception.
It fires because the main thread failed to return in time, regardless of how much total work the app is doing. That is why an app can be idle overall and still produce one, and why the fix is always about the main thread specifically.
A shipped profile lets the runtime precompile the hot startup and scrolling paths, improving cold start before any of your own code runs. It is one of the few improvements that requires no change to the application logic.
Three different starting points with three different costs, which is why a single startup number is ambiguous. Optimizing for the wrong one is common, because the one developers experience most is rarely the one users do.
A development build skips shrinking and adds instrumentation, so profiling it measures an application nobody will ever run. Every conclusion drawn from it is about a different program.
A static field, an unremoved listener or an inner class holding a reference keeps an entire view hierarchy alive past its recreation. The cost compounds with each rotation, which is why the symptom is often described as slow degradation.
V. Accessibility
An icon-only control needs a description, and a purely decorative image needs an explicit absence so it is skipped rather than announced. Both are required — omitting the second produces a reader announcing meaningless elements.
An interactive element needs a minimum touch area regardless of how small it is drawn, which means the visual size and the target size are separate decisions. Padding inside the clickable region is how the two are reconciled.
Meaning carried only by color is unavailable to a substantial number of users, and the text still needs sufficient contrast on top of that. They are two separate requirements that are frequently treated as one.
The declarative system publishes a separate tree describing meaning rather than appearance, and merging or replacing nodes controls what both assistive services and UI tests observe. That shared tree is why accessibility work also improves testability.
W. Testing
One runs fast on the development machine with no real framework and the other runs on a device and pays for it in time. Choosing the wrong level is what makes a suite both slow and unreliable.
Calling a framework class from a local test hits a stub that throws unless it is faked or a simulation library provides it. That is a deliberate signal that the test is reaching outside its level rather than an inconvenience.
A hardcoded scheduler cannot be replaced, so the test cannot control time and must resort to waiting. Injecting it is what makes virtual time possible, which turns a flaky timing test into a deterministic one.
The test harness matches nodes through the semantics tree and synchronizes with the frame clock, so an explicit sleep is both slower and less reliable than doing nothing. Adding one usually indicates a missing idling condition.
A hand-written fake exercises the real contract and fails when the contract changes; a mock replays the interactions the test already assumed and keeps passing. That difference is why a suite of mocks can be green against broken code.
X. Material theming and dynamic color
One provider supplies color, typography and shape to everything beneath it, which is what makes a theme change a single edit. Components read from it rather than receiving it, which is why the scope of the provider matters.
Reading the current values rather than hardcoding them is what makes dark mode and dynamic palettes work without touching each component. A hardcoded color is invisible until the theme changes, and then it is the only thing that does not.
Named roles rather than sizes keep text consistent and let the scale be adjusted centrally. Choosing a role by how it currently looks rather than by what the text is defeats that, because the appearance is what is allowed to change.
Recent platform versions can derive the palette from the user's wallpaper, which means the colors are not the designer's to fix. Building a brand-critical element on a theme color is what produces an unrecognizable logo on someone's device.
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All exam, test, and product names and trademarks are the property of their respective owners and are used here for identification and reference only. Keentune is independent study practice — not affiliated with, authorized, or endorsed by any of these organizations.