Keentune

Android Development curriculum

24 chapters
·
148 concepts
·
free
Everything the adaptive question bank can teach and test in Android Development, from foundations through advanced practice. Work through it in order, or start practising and let the questions find your level.
New here? Read the Android Development guide
A free 17-minute primer — the mental model, the mistakes beginners make, and what to practise first.
A. App fundamentals, components and the manifest
activity, service, broadcast receiver and content provider are the only entry points the system can start
the system constructs a component and calls its callbacks; an app owns no single main()
each app gets its own Linux user ID and process, so another app's files are unreachable without an explicit grant
an activity or service missing from the manifest cannot be started, and the failure is a runtime exception rather than a build error
android:exported decides whether other apps can start a component, and API 31+ refuses to build one that has an intent filter but no explicit value
minSdk is the oldest device that can install, compileSdk is which APIs compile, targetSdk is which behavior changes apply to you
B. The activity lifecycle, configuration changes and process death
create/destroy, start/stop and resume/pause are three mirrored pairs, always entered and unwound in that order
started means visible, resumed means focused; a dialog on top leaves the activity started but paused
the incoming activity cannot resume until onPause returns, so slow work there is a visible stall
camera, sensors and animations pair with onStart/onStop, because a merely paused activity can still be on screen
the Bundle argument is non-null only on recreation, which is how you tell a cold start from a restore
it runs when the activity *may* be killed, not when the user finishes it, and it is sized for small transient UI state only
rotation, locale, font scale and dark-mode switches destroy and recreate the activity unless configChanges claims them
a ViewModel survives a configuration change but dies with the process; only saved state or disk survives both
C. Tasks, the back stack and launch modes
a task holds activities last-in-first-out, and Back pops the top one
activities from different apps can share one task, so the back stack is a user journey rather than an app
standard always creates a new instance; singleTop reuses one only when it is already at the top of the stack
a reused instance receives the intent in onNewIntent, and getIntent() keeps returning the original until setIntent is called
Up walks the app's declared hierarchy and never leaves the app; Back walks the historical stack and can
D. Intents and intent filters
an explicit intent names the component class; an implicit one describes an action and lets the system resolve a handler
resolution tests action, category and data, and an implicit intent is delivered only if it passes all three
an implicit intent sent to startActivity is given CATEGORY_DEFAULT, so a filter lacking it never matches
an exported component's extras arrive from another process and must be validated like any external input
since API 30 an app must declare <queries> to see or resolve most other apps' components
registerForActivityResult replaces startActivityForResult and must be registered unconditionally during initialization, before STARTED
a PendingIntent runs with your app's identity in another process, and API 31+ requires FLAG_IMMUTABLE or FLAG_MUTABLE to be stated
E. Fragments and the View system
the fragment's view dies in onDestroyView while the fragment instance lives on, so observers must use viewLifecycleOwner
a view binding held past onDestroyView pins the whole detached view hierarchy in memory
addToBackStack makes Back reverse a fragment transaction; without it the transaction cannot be undone
the View system renders in three passes, and nesting multiplies measure cost — the reason ConstraintLayout exists to flatten hierarchies
AndroidView hosts a classic View inside Compose, with an update lambda that runs on recomposition
ComposeView embeds Compose UI inside an existing XML layout
F. Compose: composition, recomposition and stability
a composable describes the UI for a state; you never mutate a widget, you change state and let it re-run
only scopes that *read* a changed State recompose, so a state nobody reads costs nothing to write
composables may run in any order, in parallel, be skipped, and re-run every frame, so a side effect in the body fires an unpredictable number of times
Compose skips a composable whose parameters are all stable types and equal to the previous composition
List can hide a mutable implementation, so passing one defeats skipping; an immutable collection or @Immutable restores it
a lambda capturing an unstable value forces the child to recompose even when its visible data is unchanged
reading state in the lowest composable that needs it keeps every ancestor out of the recomposition
CompositionLocal passes a value down a subtree implicitly instead of threading it through every parameter
G. Compose state and state hoisting
remember caches a value across recompositions and nothing more; it dies with the composition
without remember a fresh state is allocated each recomposition, so writes look like they are being ignored
the read is what registers the scope; a State written but never read triggers no recomposition anywhere
rememberSaveable writes into saved instance state, surviving rotation and process death, which remember does not
rememberSaveable needs a Bundle-compatible type or an explicit Saver; an ordinary data class is neither
a stateless composable takes the value down and sends the event up, leaving exactly one source of truth
derivedStateOf recomputes on every source change but notifies readers only when the derived value differs, throttling a fast-changing input
H. Compose side effects
LaunchedEffect starts a coroutine on entering composition and cancels then restarts it whenever a key changes
LaunchedEffect(Unit) is right for one-shot work and wrong whenever the effect depends on data that can change
use DisposableEffect when something must be unregistered; onDispose runs on leaving composition or before a keyed restart
a click handler cannot start a LaunchedEffect, so launching from a callback needs rememberCoroutineScope
captures the latest value inside a long-lived effect that must not be restarted by a key change
SideEffect publishes state to non-Compose code after every successful composition and cannot suspend
I. Compose layout, modifiers and the three phases
composition decides what, layout decides where and how big, drawing decides how — and a frame can skip the earlier phases
a Compose layout measures each child exactly once, which is why arbitrary multi-pass measurement is rejected
a modifier chain applies outside-in, so padding before background insets the color and padding after does not
padding placed after clickable falls outside the touch target; placed before, it is inside it
a parent passes constraints down and each child reports its own resolved size back up
offset { } and graphicsLayer { } read state during layout or draw, so an animation through them skips recomposition entirely
Column stacks vertically, Row horizontally, Box overlays children in the same space
arrangement distributes children along the main axis; alignment positions them on the cross axis
J. Lazy lists and scrolling
LazyColumn composes only what is visible, unlike a Column inside verticalScroll, which composes every child
without a key an item's remembered state is bound to its index, so an insertion shifts that state onto the wrong row
declaring contentType lets Compose reuse compositions across items of the same shape in a heterogeneous list
a vertically scrollable child inside a LazyColumn is measured with infinite height constraints and throws
K. Architecture: layers, ViewModel and unidirectional data flow
dependencies point one way, UI → domain → data, and never back up
the repository owns the merge of network and cache so the UI observes one authoritative stream
state flows down and events flow up; the UI never writes state it does not own
a ViewModel is scoped to a lifecycle owner and onCleared runs only when that owner is finishing for good, not on rotation
an Activity, View or Context stored in a ViewModel leaks across the very configuration change the ViewModel exists to survive
SavedStateHandle gives a ViewModel key-value state that survives process death, which a plain field does not
exposing a single immutable UiState avoids the inconsistent intermediate frames that several independent streams produce
Hilt builds on Dagger to supply dependencies instead of constructing them inline, which is what makes a layer testable
L. Coroutines, flows and lifecycle-aware collection
work launched in viewModelScope is cancelled automatically when the ViewModel is cleared
a suspend function moves itself off the main thread with withContext, so no caller has to know where it runs
collect suspends until the flow finishes, so two collects written sequentially in one coroutine starve the second
repeatOnLifecycle(STARTED) cancels collection when the app backgrounds and restarts it on return, whereas launchWhenStarted only suspends and leaves the producer hot
the Compose equivalent; plain collectAsState keeps consuming while the app is in the background
StateFlow always holds a value, drops intermediate emissions under load, and skips an emission equal to the current one
M. Navigation
one activity hosts many destinations, so navigating is a back-stack operation rather than an activity launch
each graph has a fixed start destination, and it is the last screen Back reaches before leaving the app
popUpTo pops up to a destination, and inclusive pops that destination too — how a login screen is erased from history
prevents stacking a second copy of the destination already on top of the back stack
arguments should carry an identifier the destination re-reads from its own layer, not a serialized object that can go stale
N. Persistence: Room, DataStore, files and scoped storage
app-specific directories need no permission and are removed on uninstall; shared media storage is neither
since API 29 an app reaches other apps' media through MediaStore or the Storage Access Framework, not through raw file paths
Room validates @Query SQL against the schema at build time, so a typo is a compile error rather than a runtime crash
a suspend DAO function is one-shot; a Flow return re-emits whenever the queried tables change
Room refuses main-thread database access by default instead of letting it become an ANR
bumping the schema version without a Migration throws on open, and fallbackToDestructiveMigration silently drops the data instead
DataStore is asynchronous and transactional; SharedPreferences exposes a synchronous commit() that blocks its caller
key-value stores sit in the sandbox unencrypted, so tokens and keys do not belong in them
O. Networking
a network call on the main thread throws NetworkOnMainThreadException by design, not as a warning
INTERNET is a normal permission granted at install, so there is no runtime prompt to handle for it
plain HTTP is refused since API 28 unless a network security configuration opts that domain in
a connected network says nothing about your server, so timeouts and retries still have to exist
Retrofit turns an interface into typed HTTP calls; OkHttp is the client underneath it that actually moves bytes
P. Background work, services and execution limits
WorkManager is for deferrable guaranteed work; a foreground service is for immediate work the user is actively aware of
since API 26 a backgrounded app cannot start a plain background service; the call throws
a foreground service must post a notification and, since API 34, declare a foregroundServiceType backed by a matching permission
network, charging and idle constraints defer work rather than fail it, and the work still runs after a reboot
enqueueUniqueWork with KEEP, REPLACE or APPEND is what stops a relaunch from queueing duplicate jobs
Doze batches deferred work into maintenance windows, so a guaranteed wake-up needs an exact alarm and its own permission
since API 26 most implicit broadcasts no longer wake a manifest-declared receiver, so register at runtime instead
Q. Permissions
normal permissions are granted at install; dangerous ones must be requested at runtime and can be revoked afterwards
a grant can be revoked while the app is not running, so the check belongs at the point of use, not at startup
the result arrives in a callback, so the line after the request executes before the user has answered
shouldShowRequestPermissionRationale is true only after one denial and before a permanent one, which is the only moment an explanation helps
once denied for good the system shows no dialog and returns denied immediately; the only route left is Settings
the photo picker, the Storage Access Framework and a capture intent return the same data with no permission requested at all
R. Notifications and channels
since API 26 a notification posted without a channel is not shown at all
importance, sound and vibration belong to the user once a channel exists; the app cannot raise them again in code
since API 33 POST_NOTIFICATIONS is a runtime permission the user can deny
posting with an id already in use replaces that notification instead of adding a second one
S. Resources and configuration qualifiers
the system selects from qualified folders at runtime, so -night, -land and -es need no branching in code
an unqualified default resource is required, or a configuration you did not anticipate fails at lookup
drawables are qualified mdpi through xxxhdpi, and Android scales the nearest available bucket when one is missing
dp is density-independent and resolves to pixels at runtime, so a hardcoded px is a different physical size on every device
sp additionally honors the user's font-size setting, which is why text must never be sized in dp
layout-direction-aware attributes mirror automatically in RTL locales; left and right stay put
T. Build: Gradle, variants, R8 and delivery
a build variant is the cross product of product flavor and build type, which is where a variant explosion comes from
the debug type is debuggable and auto-signed with a shared debug key; shrinking and your own signing key belong to release
R8 strips unreachable code and renames what remains, which is why reflection and serialization break in release builds only
a keep rule names the reflectively-referenced classes and members R8 must not remove or rename
an update must be signed by the same key, and Play App Signing holds that key so a lost upload key stays recoverable
Play generates per-device APKs from an App Bundle; a device installs an APK, never the .aab itself
versionCode is the integer Play orders releases by and must increase; versionName is a display string with no rules
internal, closed, open and production tracks trade audience size against feedback risk
a percentage rollout limits blast radius and can be halted before it reaches everyone
U. Performance: jank, startup, memory and ANRs
jank is a missed frame deadline against the display's refresh interval, so it is a worst-case problem, not an average one
an ANR fires because the main thread did not return in time, regardless of how much total work the app is doing
a shipped profile lets ART precompile the hot startup and scroll paths, improving cold start before any of your code runs
a cold start creates the process, a warm one recreates the activity, a hot one only brings it forward
a debug build skips R8 and adds instrumentation, so profiling it measures an app you never ship
a static field, an unremoved listener or an inner-class Handler holding an Activity keeps its entire view tree alive past recreation
V. Accessibility
an icon-only control needs a description a screen reader can announce, and a purely decorative image must be explicitly null so it is skipped
an interactive element needs roughly 48dp of touch target no matter how small it is drawn
meaning carried only by color is invisible to many users, and text still needs sufficient contrast against its background
Compose publishes a separate semantics tree, and mergeDescendants and clearAndSetSemantics control what services and UI tests see
W. Testing
local JVM tests are fast but have no real framework; instrumented tests run on a device and pay for it in wall-clock time
calling an Android framework class from a local unit test hits a stub that throws unless it is faked or Robolectric provides it
a hardcoded dispatcher is untestable; injecting it is what lets a test substitute a scheduler and control virtual time
the Compose test rule matches nodes through the semantics tree and synchronizes with the clock, so an explicit sleep is both slow and wrong
a hand-written fake exercises the real contract, where a mock only replays the interactions the test already assumed
X. Material theming and dynamic color
MaterialTheme supplies color scheme, typography and shapes to everything composed beneath it
read the current values from MaterialTheme.colorScheme rather than hard-coding them
MaterialTheme.typography names the roles (display, headline, body, label) so text stays consistent
Android 12+ can derive the scheme from the user wallpaper, so the palette is not yours to fix
Keentune is not affiliated with or endorsed by the organizations whose documentation informs these maps.
All about Android Development practice
Also on your phone
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.
© 2026 SportaApp LLC