From 31d82f8ecf9639bdac8f30eaa064a66aab162442 Mon Sep 17 00:00:00 2001 From: vishnu Date: Sat, 11 Jul 2026 07:27:41 +0530 Subject: [PATCH] Rewrite Android study curriculum --- .../study/junior/01-kotlin-foundations.md | 20 +-- src/content/study/junior/02-activities.md | 18 +-- src/content/study/junior/03-fragments.md | 20 ++- src/content/study/junior/04-lifecycle.md | 18 +-- src/content/study/junior/05-ui-state.md | 20 ++- .../06-context-resources-and-process.md | 20 +-- .../07-ui-rendering-lists-accessibility.md | 16 +- .../08-storage-networking-permissions.md | 16 +- .../junior/09-debugging-and-platform-tools.md | 153 ++++++++++++++++++ .../study/mid/01-coroutines-and-flow.md | 38 +++-- .../study/mid/02-compose-state-and-effects.md | 38 ++--- .../study/mid/03-architecture-and-data.md | 23 +-- .../study/mid/04-testing-and-performance.md | 20 +-- .../05-persistence-networking-and-paging.md | 14 +- ...-background-work-services-notifications.md | 16 +- .../07-navigation-deep-links-adaptive-ui.md | 14 +- .../08-dependency-injection-gradle-release.md | 18 ++- .../01-modularization-and-boundaries.md | 16 +- .../02-offline-sync-and-system-design.md | 20 +-- .../03-reliability-and-technical-decisions.md | 20 +-- .../04-platform-internals-performance.md | 12 +- .../study/senior/05-security-privacy-trust.md | 14 +- .../06-mobile-system-design-interview.md | 16 +- .../senior/07-senior-interview-execution.md | 16 +- src/pages/study/[...id].astro | 4 +- src/pages/study/index.astro | 92 +++++++---- 26 files changed, 453 insertions(+), 239 deletions(-) create mode 100644 src/content/study/junior/09-debugging-and-platform-tools.md diff --git a/src/content/study/junior/01-kotlin-foundations.md b/src/content/study/junior/01-kotlin-foundations.md index f53ad5b..7bfec22 100644 --- a/src/content/study/junior/01-kotlin-foundations.md +++ b/src/content/study/junior/01-kotlin-foundations.md @@ -1,6 +1,6 @@ --- -title: Kotlin foundations for Android -description: Learn the Kotlin language features you will use before Android framework code can make sense. +title: "Kotlin: types, state, and program structure" +description: Establish the Kotlin type-system, state-modeling, and collection skills required to reason accurately about Android code. level: junior order: 1 duration: 50 min @@ -8,19 +8,19 @@ quizHref: /practice/?test=kotlin-test quizLabel: Take the Kotlin quiz --- -Kotlin is not a warm-up before “real Android.” It is how you express state, handle missing data, and prevent entire categories of mistakes in Android code. Before learning Activities or Compose, become comfortable reading and writing small Kotlin models and functions until they feel automatic. +Kotlin is the language in which an Android program states its invariants. Its type system distinguishes an absent value from a present one, its immutable bindings clarify ownership, and its algebraic types make a finite set of outcomes visible in code. These are correctness tools, not stylistic preferences. -This lesson focuses on the language surface you will touch daily: null safety, `val`/`var`, data and sealed types, functions, collections, and scope functions. Internals like variance and reified generics matter later; they are not the first gate. +This chapter establishes the language surface used throughout the curriculum: nullability, immutable state, data and sealed types, functions, collection transformations, and scope functions. Read each construct as a statement of intent. The important question is not whether code compiles, but whether another engineer can infer the permitted states and failure conditions from the type. ## Learning goals By the end of this lesson you should be able to: -- Choose between `val` and `var`, and explain why immutability-by-default helps Android UI code. -- Handle nullable types with `?.`, `?:`, and smart casts - and know when `!!` is a smell. -- Model a screen’s data with `data class` and mutually exclusive outcomes with `sealed interface` / `sealed class`. -- Transform collections with `map`, `filter`, `firstOrNull`, and `associateBy` without hiding business rules. -- Pick a scope function (`let`, `also`, `apply`, `run`, `with`) for a clear purpose, or refuse one when it hurts readability. +- Choose between `val` and `var` by considering reference reassignment and mutation separately. +- Handle nullable values with `?.`, `?:`, and smart casts, and identify the exact failure condition introduced by `!!`. +- Model screen data with `data class` and mutually exclusive outcomes with `sealed interface` or `sealed class`. +- Transform collections with `map`, `filter`, `firstOrNull`, and `associateBy` without concealing a business rule in a chain. +- Select a scope function only when its receiver, return value, and purpose remain clear to the reader. ## Values, variables, and nullability @@ -239,7 +239,7 @@ val label = when (val code = response.code) { 4. **Boolean soup** for UI (`isLoading && !hasError && data != null`) instead of a sealed state. 5. **Scope-function noise** that makes a simple block unreadable under interview pressure. -## How interviewers probe this +## Examination prompts Expect: diff --git a/src/content/study/junior/02-activities.md b/src/content/study/junior/02-activities.md index 13afd82..340a0af 100644 --- a/src/content/study/junior/02-activities.md +++ b/src/content/study/junior/02-activities.md @@ -1,6 +1,6 @@ --- -title: "Activities: an Android screen's entry point" -description: Understand what an Activity owns, how Android creates it, and why it should stay thin. +title: Activities, intents, tasks, and window ownership +description: Understand how the system creates an Activity, delivers input, manages tasks, and defines the boundary of a screen host. level: junior order: 2 duration: 45 min @@ -8,16 +8,16 @@ quizHref: /practice/?test=android-fundamentals-test quizLabel: Take the fundamentals quiz --- -An **Activity** is an Android component that provides a window where your app can show a screen. Android creates it in response to an explicit intent, a launcher tap, a deep link, or task restoration. Your code does not call an Activity constructor as the normal entry path. The operating system controls its lifecycle and may create a new instance when you did not expect it - after rotation, locale change, or process death recovery. +An **Activity** is an Android application component that owns a window and participates in the system task model. The operating system, not ordinary application code, creates it in response to a launcher action, an explicit intent, a verified link, or restoration of a task. That ownership explains why an Activity instance is replaceable and why it is an unsuitable home for durable business state. -Modern apps often use a **single-Activity** architecture: one Activity hosts many Compose destinations or Fragments. Even then, you must understand what that Activity owns, how intents deliver data, and how tasks and the back stack work. +Many modern applications use one Activity to host a navigation system. The architecture does not remove the Activity contract. A screen host must still accept and validate external input, configure the window, coordinate system callbacks, and leave data loading and business decisions to longer-lived collaborators. ## Learning goals -- Explain what an Activity is responsible for - and what it should not own. -- Trace how an intent starts an Activity and how extras are read safely. -- Describe tasks, the back stack, and common launch-mode pitfalls at a junior level. -- Keep Activities thin: theme, content, navigation host, system callbacks - not network or business rules. +- Define the window-scoped responsibilities of an Activity and identify work that belongs elsewhere. +- Trace an intent from component resolution to validated arguments. +- Describe a task, its back stack, and the consequences of common launch modes. +- Keep the Activity limited to theme, content, navigation host, and system callbacks rather than network or business rules. ## What an Activity is @@ -222,7 +222,7 @@ You may still use multiple Activities for separate tasks (e.g. a picture-in-pict 4. **Assuming `onCreate` runs for every intent** - false for `onNewIntent` cases. 5. **Putting business logic in the Activity** so unit tests require Robolectric or instrumentation for no good reason. -## How interviewers probe this +## Examination prompts - “What is an Activity?” - component + window + system-managed lifecycle. - “Difference between explicit and implicit intents?” diff --git a/src/content/study/junior/03-fragments.md b/src/content/study/junior/03-fragments.md index 8bac721..1d7714d 100644 --- a/src/content/study/junior/03-fragments.md +++ b/src/content/study/junior/03-fragments.md @@ -1,6 +1,6 @@ --- -title: Fragments and view lifecycle -description: Learn why a Fragment can outlive its view and how that changes safe UI code. +title: Fragments and the view-lifecycle boundary +description: Learn the separate lifetimes of a Fragment and its view, then apply that distinction to bindings, observers, and navigation. level: junior order: 3 duration: 45 min @@ -8,18 +8,16 @@ quizHref: /practice/?test=android-fundamentals-test quizLabel: Take the fundamentals quiz --- -A **Fragment** represents a reusable portion of UI or navigation within an Activity. It has its own lifecycle, but the critical detail juniors miss is that its **view has a different lifecycle**. A Fragment can remain on the back stack while Android destroys its view to free memory. Later, the same Fragment instance can create a **new** view. +A **Fragment** is a controller that can contribute UI and navigation to an Activity. Its instance lifecycle and its view lifecycle are deliberately different. A Fragment may remain in the FragmentManager while its view hierarchy is destroyed, then create a replacement hierarchy later. Every safe Fragment implementation begins by treating those two lifetimes as separate ownership domains. -If you hold a `View` / View Binding / RecyclerView reference past `onDestroyView`, you leak memory and can crash when you touch a dead view hierarchy. - -Even if your app is Compose-first and “never uses Fragments,” interviewers still ask about them. Navigation, ViewPager, and many production codebases still depend on Fragment semantics - especially the view-lifecycle gap. +A `View`, View Binding, adapter connected to views, or observer that updates views belongs to the view lifecycle. Retaining one after `onDestroyView` keeps the obsolete hierarchy reachable and can produce a memory leak or a stale UI update. This distinction remains important in Compose-first applications because established applications, ViewPager integrations, and navigation hosts commonly retain Fragment boundaries. ## Learning goals -- Separate **Fragment lifecycle** from **view lifecycle**. -- Create and clear view bindings correctly. -- Observe LiveData / collect flows with `viewLifecycleOwner`, not `this` (the Fragment). -- Explain when Fragments still matter in a Compose world. +- Separate the Fragment instance lifecycle from the lifecycle of its current view hierarchy. +- Create and clear a View Binding at the correct boundary. +- Observe LiveData and collect UI streams with `viewLifecycleOwner`, not the Fragment instance. +- Explain when Fragment semantics remain relevant in a Compose-based application. ## Fragment vs view lifecycle @@ -217,7 +215,7 @@ Do not store UI state only in view fields (`binding.title.text`) if it must retu 5. **Retaining large objects (bitmaps, adapters) on the Fragment instance** across `onDestroyView`. 6. **Nested Fragments with the wrong FragmentManager** (`parent` vs `child`). -## How interviewers probe this +## Examination prompts - “Difference between Fragment lifecycle and view lifecycle?” - “Why `viewLifecycleOwner`?” diff --git a/src/content/study/junior/04-lifecycle.md b/src/content/study/junior/04-lifecycle.md index 1646e6a..61d130e 100644 --- a/src/content/study/junior/04-lifecycle.md +++ b/src/content/study/junior/04-lifecycle.md @@ -1,6 +1,6 @@ --- -title: Lifecycle, configuration change, and process death -description: Know what Android preserves, what it recreates, and where important state belongs. +title: Lifecycle, recreation, and durable state +description: Classify configuration change and process death correctly, then place UI state and durable data in the owner that can recover it. level: junior order: 4 duration: 50 min @@ -8,16 +8,16 @@ quizHref: /practice/?test=android-fundamentals-test quizLabel: Take the fundamentals quiz --- -**Lifecycle** is the contract between your UI and Android. The system can pause your app when another window appears, stop it when it is no longer visible, destroy an Activity for a configuration change, or kill the whole process to reclaim memory. Correct Android code treats these as **normal events**, not exceptional failures. +Lifecycle is Android's contract for changing ownership. The system can obscure a window, stop a component, recreate an Activity for a configuration change, or reclaim the entire process. These are ordinary operating conditions. An application that treats them as rare errors eventually loses state, leaks resources, or repeats work. -Juniors who only memorize callback names struggle. Juniors who ask “what ownership changes here?” and “what state still exists?” do well in interviews and in production. +Do not memorize callback names in isolation. At each lifecycle boundary, determine what remains visible, which object is being replaced, which work must stop, and which data must survive without relying on a final callback. That model provides a reliable answer for both View and Compose applications. ## Learning goals -- Trace Activity lifecycle callbacks and what each implies for resources and UI. -- Distinguish **configuration change** from **process death**. -- Place state in the right store: UI widget, ViewModel, SavedStateHandle, or durable storage. -- Collect flows and start work in a lifecycle-aware way (`repeatOnLifecycle`, `WhenStarted`, etc.). +- Trace Activity lifecycle callbacks and the resource or UI obligation each one implies. +- Distinguish configuration recreation from process death. +- Place state in a widget, ViewModel, `SavedStateHandle`, or durable storage according to its required lifetime. +- Collect streams and start UI work with lifecycle-aware APIs such as `repeatOnLifecycle`. ## The core Activity callbacks @@ -222,7 +222,7 @@ Never rely on `onDestroy` as the only place to persist user data. 4. **Updating UI after `onDestroyView` / destroyed lifecycle.** 5. **Assuming `init` in ViewModel runs once per app** - it runs once per ViewModel instance; process death creates a new one. -## How interviewers probe this +## Examination prompts - Walk through rotation with a form draft in different storage locations. - “Does ViewModel survive process death?” - **No** (in-memory); SavedStateHandle can restore small keys. diff --git a/src/content/study/junior/05-ui-state.md b/src/content/study/junior/05-ui-state.md index add4181..2f4dca3 100644 --- a/src/content/study/junior/05-ui-state.md +++ b/src/content/study/junior/05-ui-state.md @@ -1,6 +1,6 @@ --- -title: UI state and UI models -description: Model what a screen can show before wiring network calls or rendering components. +title: UI state, events, and rendering models +description: Model the valid states of a screen, distinguish state from one-time work, and map application data into renderable UI models. level: junior order: 5 duration: 50 min @@ -8,18 +8,16 @@ quizHref: /practice/?test=architecture-test quizLabel: Take the architecture quiz --- -A screen is easiest to build when you can describe its **state** before writing widgets or firing network calls. Avoid several unrelated flags such as `isLoading`, `hasError`, and `showContent`. Those flags can contradict one another: is the screen loading *and* showing content? Is the error blocking, or a small refresh warning over cached data? +A screen should be specified as a set of states before it is rendered as widgets. Independent flags such as `isLoading`, `hasError`, and `showContent` permit contradictory combinations and leave the renderer to invent product policy. A state model should instead make every meaningful condition explicit, including cached content that is refreshing and an error that does not block existing data. -**UI models** are the data the UI needs to render - not the Retrofit DTO, not the Room entity, not the domain object with twenty unused fields. Mapping into UI models forces you to decide what the user actually sees. - -This is the last junior lesson before mid-level async and architecture work. Master it here; everything later assumes it. +**UI models** describe the information and actions required to render a particular surface. They are neither transport DTOs nor database entities. Mapping to a UI model is a design step: it decides what the user can see, what is unavailable, and what action is possible. This model is the foundation for the asynchronous and architectural work that follows. ## Learning goals -- Replace boolean soup with explicit, valid states. -- Separate **state** (what is true now) from **events** (one-off things that must happen once). -- Design `*UiModel` / `*UiState` types the UI can render without guessing. -- Place state in the right owner (composable memory, ViewModel, disk). +- Replace unrelated flags with a state model that represents only valid combinations. +- Separate present state from an action that must occur once. +- Design `*UiModel` and `*UiState` types that the renderer can consume without inference. +- Place state in composable memory, a ViewModel, or durable storage according to its required lifetime. ## Make valid states explicit @@ -306,7 +304,7 @@ list.add(newItem) _state.update { it.copy(items = it.items + newItem) } ``` -## How interviewers probe this +## Examination prompts - “How do you model loading / content / error?” - “Difference between UI state and one-off events?” diff --git a/src/content/study/junior/06-context-resources-and-process.md b/src/content/study/junior/06-context-resources-and-process.md index f1ed48d..ee2fab4 100644 --- a/src/content/study/junior/06-context-resources-and-process.md +++ b/src/content/study/junior/06-context-resources-and-process.md @@ -1,6 +1,6 @@ --- -title: Context, resources, and the Android process -description: Understand what Context gives you, how Android selects resources, and which objects must never be kept alive accidentally. +title: Context, resources, process lifetime, and memory ownership +description: Choose the correct Android Context, predict resource resolution, and prevent leaks by matching every reference to its proper lifetime. level: junior order: 6 duration: 55 min @@ -8,17 +8,17 @@ quizHref: /practice/?test=android-fundamentals-test quizLabel: Take the Android fundamentals quiz --- -`Context` is one of the most frequently passed objects in Android and one of the least precisely explained. It is not simply "the app." A `Context` is an interface to environment-specific services and resources. The environment may be an `Activity`, the application, a service, or a wrapped context with a different theme or configuration. +`Context` is an interface to services, resources, package operations, and configuration that belong to a particular Android environment. It is not a synonym for the application. An Activity, an application, a service, and a themed wrapper provide different capabilities, themes, configurations, and lifetimes. -This distinction matters because the object you choose controls what is available, which theme is used, and how long a reference can safely live. +Selecting the wrong Context can be a functional defect as well as a memory defect. A dialog needs an Activity-backed themed context; a long-lived cache should not retain that Activity. Resource selection and process lifetime rely on the same discipline: identify the environment and lifetime before retaining or resolving anything. ## Learning goals -- Explain what `Context` provides without calling it a global variable. -- Choose an `Activity` context or application context based on lifetime and capability. -- Read resource qualifiers and predict which resource Android selects. -- Distinguish an app process from an Activity, task, and application object. -- Find common memory leaks caused by long-lived references. +- Explain the capabilities supplied by a Context without treating it as a global variable. +- Choose an Activity or application Context from the operation's capability and lifetime requirements. +- Read resource qualifiers and predict the selected resource. +- Distinguish a process from an Activity, task, and Application object. +- Identify memory leaks caused by retaining a reference beyond the owner's lifetime. ## What `Context` actually provides @@ -228,7 +228,7 @@ The useful interview rule is: **compare the lifetime of the owner with the lifet 6. Assuming a hardware feature exists because an API exists. 7. Doing heavy initialization in `Application.onCreate()`. -## How interviewers probe this +## Examination prompts - "What is Context?" - "Application context vs Activity context?" diff --git a/src/content/study/junior/07-ui-rendering-lists-accessibility.md b/src/content/study/junior/07-ui-rendering-lists-accessibility.md index 697eb3d..e20bbed 100644 --- a/src/content/study/junior/07-ui-rendering-lists-accessibility.md +++ b/src/content/study/junior/07-ui-rendering-lists-accessibility.md @@ -1,6 +1,6 @@ --- -title: UI rendering, lists, input, and accessibility -description: Learn how Views and Compose measure and draw UI, how lists preserve identity, and how to build controls every user can operate. +title: UI rendering, lists, and accessible interaction +description: Build responsive Android interfaces that preserve item identity, expose semantic meaning, and remain usable across input methods. level: junior order: 7 duration: 65 min @@ -8,15 +8,17 @@ quizHref: /practice/?test=jetpack-compose-test quizLabel: Take the UI and Compose quiz --- -Android has two major UI toolkits: the View system and Jetpack Compose. Interviews may focus on one, but strong developers understand the shared constraints underneath both: a finite window, measurement, placement, drawing, input, semantics, and state. +Android provides the View system and Jetpack Compose, but both operate under the same constraints: a finite window, a rendering deadline, input dispatch, layout, drawing, semantic meaning, and changing state. Toolkit syntax differs; user-visible correctness does not. An interface that is smooth but unreadable by a screen reader, or accessible but unstable during list changes, is incomplete. + +This chapter connects the rendering pipeline to practical UI decisions. It examines how layout work reaches a frame, why lazy collections require identity, how text and focus interact with the keyboard, and how semantics allow assistive technology to operate the same feature. ## Learning goals -- Describe measurement, layout, and drawing in both UI toolkits. +- Describe measurement, layout, and drawing in both Android UI toolkits. - Build efficient lists with stable item identity. -- Explain why modifier and listener order changes behavior. +- Explain why modifier and listener ordering changes behavior. - Handle text input, focus, and the on-screen keyboard. -- Test accessibility semantics, touch targets, and large-text layouts. +- Test semantic labels, touch targets, and large-text layouts. ## The frame pipeline @@ -253,7 +255,7 @@ When hosting Compose in a Fragment View, disposal must align with the Fragment's 6. Hard-coding heights around one font size. 7. Applying status-bar or IME insets twice. -## How interviewers probe this +## Examination prompts - "Explain measure, layout, and draw." - "`requestLayout()` vs `invalidate()`?" diff --git a/src/content/study/junior/08-storage-networking-permissions.md b/src/content/study/junior/08-storage-networking-permissions.md index 7ee590e..7ad1e23 100644 --- a/src/content/study/junior/08-storage-networking-permissions.md +++ b/src/content/study/junior/08-storage-networking-permissions.md @@ -1,6 +1,6 @@ --- -title: Storage, networking, and runtime permissions -description: Build a reliable data path with Room, DataStore, HTTP clients, scoped storage, and permission flows that handle denial correctly. +title: Storage, networking, files, and runtime permissions +description: Select a durable data boundary, interpret network outcomes, handle user-owned files, and request sensitive access only when it is needed. level: junior order: 8 duration: 70 min @@ -8,14 +8,16 @@ quizHref: /practice/?test=android-fundamentals-test quizLabel: Take the Android fundamentals quiz --- -Most Android screens are projections of data from memory, disk, a remote service, or a device capability. The important junior-level skill is not memorizing library annotations. It is tracing where data comes from, where it becomes durable, which thread does the work, and what the UI shows when access fails. +Most Android screens are projections of data held in memory, stored on disk, received from a remote service, or supplied by a protected device capability. The essential skill is to trace the complete path: where data originates, which owner makes it durable, which work may block, and how the interface represents denial or failure. + +The APIs in this chapter are boundaries, not conveniences. Room protects a schema and transactions, DataStore protects small persistent state, HTTP represents a fallible request-response exchange, and a runtime permission is a user-controlled grant that may be denied at any time. ## Learning goals -- Choose Room, DataStore, files, or cache based on the shape and lifetime of data. -- Explain an HTTP request from UI action to parsed response. +- Choose Room, DataStore, files, or cache from the shape and lifetime of the data. +- Explain an HTTP request from a UI action to a parsed response or classified failure. - Keep disk and network work off the main thread. -- Request runtime permissions as part of a user action and handle every result. +- Request runtime permissions in response to a user action and handle every result. - Distinguish private app storage, shared media, and document-provider access. ## Choose storage by data shape @@ -235,7 +237,7 @@ Encrypted local storage reduces exposure in some device-compromise and backup sc 7. Treating `shouldShowRequestPermissionRationale()` as a permanent-denial detector. 8. Embedding a secret in `BuildConfig` and assuming obfuscation protects it. -## How interviewers probe this +## Examination prompts - "Room vs DataStore?" - "Why use a transaction?" diff --git a/src/content/study/junior/09-debugging-and-platform-tools.md b/src/content/study/junior/09-debugging-and-platform-tools.md new file mode 100644 index 0000000..7c78263 --- /dev/null +++ b/src/content/study/junior/09-debugging-and-platform-tools.md @@ -0,0 +1,153 @@ +--- +title: Debugging, inspection, and Android platform tools +description: Build a repeatable method for reproducing failures, reading evidence, inspecting application state, and verifying a fix on a real device. +level: junior +order: 9 +duration: 55 min +quizHref: /practice/?test=android-fundamentals-test +quizLabel: Take the Android fundamentals quiz +--- + +Debugging is the discipline of reducing uncertainty with evidence. A crash log, a visual defect, a failed request, and an unexpected lifecycle callback are not separate kinds of problem. In each case, the engineer must establish the observed behavior, reproduce it under controlled conditions, collect the smallest useful evidence, form a falsifiable explanation, and verify the correction without creating a new regression. + +Android adds important sources of variation: operating-system version, window size, locale, process lifetime, network state, permission state, and device manufacturer behavior. A correct fix therefore names the environment in which the fault occurred. “It works on my phone” is an observation, not a conclusion. + +## Learning goals + +By the end of this lesson, you should be able to: + +- Write a minimal reproduction that distinguishes observation from assumption. +- Read a stack trace from the exception type to the first relevant application frame. +- Use Logcat, the debugger, and Android Debug Bridge to inspect a running application. +- Inspect the lifecycle, permissions, storage, and process state that influence a defect. +- Verify a fix across the state transition that originally failed. + +## Start with a reproducible observation + +A useful bug report describes a behavior that another person can attempt to reproduce: + +| Weak report | Reproducible report | +|---|---| +| “The screen is broken.” | “On Android 14, open a shared PDF, rotate while the picker is visible, then return. The attachment row remains loading until the screen is reopened.” | +| “Login sometimes fails.” | “With an expired access token and valid refresh token, tapping Retry twice creates two refresh requests. The second request overwrites the first response.” | + +Record the smallest set of facts that change the result: + +1. Application version, device or emulator image, Android version, and account state. +2. Starting condition, exact actions, and expected versus actual result. +3. Frequency and whether the behavior depends on timing, connectivity, configuration, or a permission. +4. Evidence such as a stack trace, screenshot, network response class, or trace. + +Do not begin by editing code. First make the failure predictable enough that a later change can be tested against the same condition. + +## Read a stack trace as a causal path + +An exception provides a type, message, and call stack. Start with the exception type and message, then find the first frame in your application package. Framework frames explain how Android reached your code, but the first relevant application frame usually identifies the violated assumption. + +```text +java.lang.IllegalStateException: Fragment ProfileFragment did not return a View + at androidx.fragment.app.FragmentStateManager.createView(...) + at com.example.profile.ProfileFragment.onCreateView(ProfileFragment.kt:42) +``` + +This is not evidence that FragmentManager is defective. It says the `onCreateView` contract was violated at `ProfileFragment.kt:42`. Open that line, identify the condition that returned no view or used the wrong constructor, and write a test or manual reproduction for that condition. + +For a crash caused by a nullable value, do not stop at “null pointer.” Ask why the value could be absent. It may be a missing deep-link argument, a process recreation path, a repository race, or an invalid assumption about a permission grant. The root cause is the earlier contract failure, not the final dereference. + +## Use logs as structured evidence + +Logcat contains application logs, system logs, crashes, and process events. Filter by the application process and a stable tag while reproducing a fault. A log line should state an event and the non-sensitive facts needed to interpret it. + +```kotlin +private const val TAG = "AttachmentUpload" + +Log.i(TAG, "enqueue id=$attachmentId source=$source") +Log.w(TAG, "upload failed id=$attachmentId category=${error.category}", error) +``` + +Avoid logging credentials, complete URLs containing tokens, document contents, email addresses, or raw server responses. Logging is a diagnostic channel with privacy and retention consequences. For production telemetry, prefer a classified outcome such as `timeout`, `unauthorized`, or `parse_error` over unbounded exception text. + +Use log levels consistently: + +- `VERBOSE` and `DEBUG` for local diagnostic detail that is normally disabled in release builds. +- `INFO` for meaningful lifecycle or business milestones during development. +- `WARN` for an unexpected but recoverable condition. +- `ERROR` for a failed operation that needs investigation. Include the throwable when it preserves the causal stack. + +## Pause execution and inspect state + +The debugger is most valuable when it answers a specific question. Set a breakpoint immediately before the suspected decision, reproduce the issue, then inspect values and the call stack. Step over a line to observe its result; step into only when the callee is relevant to the hypothesis. + +For an incorrect screen state, inspect the state holder rather than only the composable or View. Check the input event, repository result, mapper output, and rendered model in order. This follows the data flow and prevents a visual symptom from hiding an upstream defect. + +Use conditional breakpoints sparingly when a loop or callback fires frequently. For example, pause only when `item.id == expectedId`. A breakpoint that halts every frame can change scheduling enough to hide a race condition. + +## Inspect the device deliberately + +Android Debug Bridge, commonly invoked as `adb`, exposes information that the application UI cannot show. The following commands are useful during local investigation: + +```bash +adb logcat --pid=$(adb shell pidof com.example.app) +adb shell dumpsys activity activities +adb shell dumpsys package com.example.app +adb shell am force-stop com.example.app +adb shell pm clear com.example.app +``` + +The first command narrows Logcat to the running process. `dumpsys activity activities` helps inspect the foreground task and Activity stack. `dumpsys package` shows package information, declared permissions, and granted runtime permissions. Force-stopping or clearing app data changes the test precondition. Use those commands intentionally and say which state has been reset. + +Android Studio's App Inspection tools can inspect databases, network traffic where supported, and layout state. They complement, rather than replace, a written reproduction. The inspection view shows one moment in time; the reproduction explains why that moment occurred. + +## Exercise state transitions that hide defects + +Many Android failures occur between steady states. Test the transition, not only the final screen: + +| Transition | Questions to ask | +|---|---| +| Rotation or size change | Does the new UI render from state without repeating a request or retaining the old view? | +| Background then foreground | Does visible work pause and resume correctly? Did a token or permission change? | +| Process death and restore | Can durable data reconstruct the screen? Is transient input restored only when appropriate? | +| Offline then online | Are requests retried according to policy? Does cached content remain coherent? | +| Permission denied or revoked | Does the feature explain the next action without assuming a grant? | +| Deep link into a feature | Are arguments validated and is required data reloaded safely? | + +The developer options process limit and “Don't keep activities” setting can help expose recreation assumptions during development. They are stress tools, not substitutes for correct state ownership. + +## Verify the fix and guard the contract + +A fix is complete when the original reproduction no longer fails, nearby states still work, and the change has a durable guard. The guard may be a unit test for a state mapper, an instrumented test for a permission flow, a regression test for a parsing edge case, or a concise manual test recorded in the pull request. + +Use this sequence: + +1. Reproduce the defect on an unchanged build. +2. State the proposed cause in one sentence. +3. Make the smallest change that addresses that cause. +4. Re-run the original reproduction and one neighboring state. +5. Add a test when the contract can be expressed reliably. +6. Review logs, cleanup, and error messages for information or privacy leaks. + +If the change only removes the visible symptom while the state model remains contradictory, the defect will return through another entry point. + +## Common mistakes + +1. Treating the last stack frame as the root cause without examining the violated earlier assumption. +2. Using a release-only report as an excuse not to reproduce the environment locally. +3. Leaving sensitive values in logs after the investigation. +4. Testing a happy path after a fix but not the state transition that exposed the defect. +5. Clearing application data without recording that the reproduction now starts from a different state. + +## Practice checklist + +1. Introduce a harmless state-mapping defect in a sample screen and write a five-step reproduction before correcting it. +2. Trigger and read a stack trace. Identify the exception type, first application frame, and violated assumption. +3. Use Logcat filtering and one `adb dumpsys` command to inspect an application you are running locally. +4. Rotate a stateful screen, force-stop the process, and compare what restores from the ViewModel, `SavedStateHandle`, and durable storage. +5. Add one behavior-focused regression test for a defect you have diagnosed. + +## What you should be able to explain + +- Why reproducibility is the first debugging deliverable. +- How to read a stack trace from failure symptom to the first relevant application frame. +- The difference between inspecting a value and proving the cause of a value. +- Which Android state transitions are likely to reveal lifecycle and ownership defects. +- How a regression test or documented manual check protects the corrected contract. diff --git a/src/content/study/mid/01-coroutines-and-flow.md b/src/content/study/mid/01-coroutines-and-flow.md index 85771ed..1f2bf6b 100644 --- a/src/content/study/mid/01-coroutines-and-flow.md +++ b/src/content/study/mid/01-coroutines-and-flow.md @@ -1,6 +1,6 @@ --- -title: Coroutines and Flow for screen data -description: Run async work with structured concurrency and expose changing values as Flow without leaking jobs. +title: Coroutines, Flow, cancellation, and state streams +description: Use structured concurrency and observable streams to perform asynchronous work without losing cancellation, ownership, or failure semantics. level: mid order: 1 duration: 55 min @@ -8,15 +8,17 @@ quizHref: /practice/?test=coroutines-test quizLabel: Take the Coroutines quiz --- -Coroutines are how modern Android code waits for network, disk, and timers **without blocking the main thread**. Flow is how you model **values that change over time**. Mid-level engineers are expected to choose scopes, handle cancellation, switch dispatchers, and collect safely in the UI - not merely call `launch` until something works. +Coroutines express asynchronous work without blocking a thread, and Flow expresses a sequence of values that changes over time. Neither API grants safety by itself. Correct code must assign each job to an owner, preserve cancellation, classify exceptions, and stop collection when the UI that consumes a value is no longer active. + +This chapter treats concurrency as a lifetime problem. A scope defines who may cancel work, a dispatcher defines where a block runs, and a stream defines what the consumer receives when it starts or stops observing. Those choices determine whether a screen shows current state, repeats a request, or leaks work beyond its purpose. ## Learning goals -- Own work with the right **scope** (`viewModelScope`, `lifecycleScope`, `supervisorScope`). -- Explain **structured concurrency**, cancellation, and why `GlobalScope` is a red flag. -- Choose `withContext`, `async`/`await`, and dispatchers deliberately. +- Select a scope such as `viewModelScope`, `lifecycleScope`, or `supervisorScope` from the lifetime of the result. +- Explain structured concurrency, cooperative cancellation, and the ownership problem created by `GlobalScope`. +- Choose `withContext`, `async` and `await`, and dispatchers deliberately. - Build cold Flows, expose hot state with `StateFlow`, and collect with lifecycle awareness. -- Handle exceptions without silently swallowing failures. +- Handle exceptions without converting a real failure into silence. ## Structured concurrency in one picture @@ -186,7 +188,7 @@ val results: StateFlow> = query | Type | Behavior | Typical use | |------|----------|-------------| | `StateFlow` | Hot, conflated, always has value | Screen UI state | -| `SharedFlow` | Hot, configurable replay/buffer | One-off events (careful design) | +| `SharedFlow` | Hot, configurable replay/buffer | Multicast signals where every active collector should observe the value | | Cold `Flow` | Starts on collect | Repository streams, Room | | `LiveData` | Lifecycle-aware holder | Legacy / simple UI | @@ -223,14 +225,24 @@ lifecycleScope.launch { } ``` -## Channels for events (optional pattern) +## UI consequences belong in state + +Do not treat a `Channel` or `SharedFlow` emitted by a ViewModel as a default delivery mechanism for navigation or snackbars. A collector can be absent while the UI is stopped, and buffering policy then becomes accidental product behavior. An event that changes what the user should be able to see or do should be processed in the ViewModel and represented by the resulting state. ```kotlin -private val _events = Channel(Channel.BUFFERED) -val events = _events.receiveAsFlow() +data class CheckoutUiState( + val submission: Submission = Submission.Editing, +) + +sealed interface Submission { + data object Editing : Submission + data object Submitting : Submission + data class Confirmed(val orderId: String) : Submission + data class Failed(val message: String) : Submission +} ``` -Use for navigation / Snackbar. Do not overload `StateFlow` with sticky “show message” booleans. +The UI renders `Confirmed` as a confirmation destination and `Failed` as a retryable message. If the effect is truly transient, define its acknowledgement and replay behavior explicitly. Never rely on an incidental buffer to decide whether a user sees an important outcome. ## Work that must survive the screen @@ -251,7 +263,7 @@ Mid-level judgment: *does cancellation on leave match product expectations?* 5. **Collecting StateFlow without lifecycle** - wasted work, subtle bugs. 6. **`stateIn` with `Eagerly` everywhere** - work runs with no subscribers; know `WhileSubscribed`. -## How interviewers probe this +## Examination prompts - “Why not GlobalScope?” - “Difference between `launch` and `async`?” diff --git a/src/content/study/mid/02-compose-state-and-effects.md b/src/content/study/mid/02-compose-state-and-effects.md index 390779a..2089c92 100644 --- a/src/content/study/mid/02-compose-state-and-effects.md +++ b/src/content/study/mid/02-compose-state-and-effects.md @@ -1,6 +1,6 @@ --- -title: Compose state, recomposition, and side effects -description: Write Compose UI that reacts predictably when values change, items move, or effects restart. +title: Compose state, recomposition, identity, and effects +description: Make declarative UI predictable by choosing state ownership, stable identity, and effect scope from the composition lifecycle. level: mid order: 2 duration: 55 min @@ -8,17 +8,17 @@ quizHref: /practice/?test=jetpack-compose-test quizLabel: Take the Compose quiz --- -Jetpack Compose describes UI from **state**. When observable state that a composable **read** changes, Compose schedules **recomposition** of the parts of the tree that depend on it. Recomposition is normal and may run often. A composable body must remain a **description of UI**, not a place for network calls, analytics floods, or navigation storms. +Jetpack Compose renders a description of the current UI state. When an observable value read by a composable changes, Compose may recompose the relevant part of the tree. Recomposition is routine, may occur more than once, and may be discarded. A composable body must therefore describe UI without performing network work, duplicating analytics, or issuing navigation commands. -Mid-level Compose skill is less about knowing every widget and more about **state lifetime**, **stability**, **effect keys**, and **list identity**. +Reliable Compose code follows four questions: who owns this state, how long must it survive, what identifies this item, and what lifecycle should own this side effect? Answering those questions gives recomposition a predictable role instead of treating it as a mysterious callback. ## Learning goals -- Choose `remember`, `rememberSaveable`, ViewModel state, and disk for the right lifetime. -- Explain recomposition and why side effects need effect APIs. -- Key `LaunchedEffect` / `DisposableEffect` correctly. -- Provide stable keys in Lazy lists. -- Hoist state so UIs stay testable and reusable. +- Choose `remember`, `rememberSaveable`, ViewModel state, and disk from the required lifetime. +- Explain recomposition and why side effects require effect APIs. +- Key `LaunchedEffect` and `DisposableEffect` from the resources and inputs that define their lifetime. +- Provide stable keys in lazy collections. +- Hoist state so reusable interfaces remain testable and controllable. ## Mental model: composition phases @@ -206,19 +206,21 @@ You do not need to memorize compiler stability inference tables on day one; you ## Navigation and effects -Navigation is a one-off effect. Trigger it from events, not sticky state: +Navigation must not be invoked directly from the composable body, because composition can run repeatedly. The ViewModel should process the user action and expose a state change that represents the resulting destination or route. The UI then performs the navigation in a controlled effect whose key is the route identity. ```kotlin -LaunchedEffect(Unit) { - viewModel.events.collect { event -> - when (event) { - is Event.OpenDetail -> navController.navigate("detail/${event.id}") - } - } +data class ArticlesUiState( + val destination: Destination? = null, +) + +LaunchedEffect(state.destination) { + val destination = state.destination ?: return@LaunchedEffect + navController.navigate(destination.route) + viewModel.onDestinationHandled(destination) } ``` -Avoid navigating during composition based on a flag that remains true across recompositions without consumption. +The acknowledgement makes the delivery policy visible. For flows such as authentication, a state-driven navigation host can instead render the destination directly from `isAuthenticated`. In both cases, avoid a hidden event buffer and avoid navigating from a flag that remains true across recompositions. ## Interop notes @@ -235,7 +237,7 @@ Avoid navigating during composition based on a flag that remains true across rec 5. Reading a high-frequency state at the root of a giant screen. 6. Using `rememberCoroutineScope` for load-on-enter instead of `LaunchedEffect`. -## How interviewers probe this +## Examination prompts - “What is recomposition?” - “`remember` vs `rememberSaveable` vs ViewModel?” diff --git a/src/content/study/mid/03-architecture-and-data.md b/src/content/study/mid/03-architecture-and-data.md index 44e7252..fe806bb 100644 --- a/src/content/study/mid/03-architecture-and-data.md +++ b/src/content/study/mid/03-architecture-and-data.md @@ -1,6 +1,6 @@ --- -title: Architecture and the data layer -description: Give UI, business decisions, local storage, and remote data clear responsibilities. +title: Android architecture, repositories, and unidirectional data flow +description: Structure a feature around explicit data ownership, repositories, UI state, and boundaries that improve correctness and testability. level: mid order: 3 duration: 55 min @@ -8,15 +8,17 @@ quizHref: /practice/?test=architecture-test quizLabel: Take the architecture quiz --- -Architecture is useful when it makes change **safer**, not when it maximizes folder count. Start with a feature boundary, not a diagram. The UI renders state and sends user intent. A ViewModel turns that intent into screen state. A repository coordinates the data sources a feature needs. Each layer should have a reason to change that differs from the others. +Architecture is useful when it makes a change safer to reason about, test, and ship. It is not a contest for the most folders. Begin with a feature boundary and assign each collaborator a distinct reason to change: the UI renders state and receives intent, a state holder coordinates screen behavior, and a repository exposes application data while coordinating its sources. + +The resulting flow is deliberately one-directional. Input enters at the UI boundary, work reaches the data layer through explicit dependencies, and observable state returns for rendering. This prevents the screen from becoming an untraceable mixture of database calls, HTTP callbacks, and transient view state. ## Learning goals -- Apply recommended Android layering without cargo-cult Clean Architecture theater. -- Keep DTOs and entities out of the UI. -- Use the local database as a **source of truth** for offline-friendly screens. -- Inject dependencies with clear scopes. -- Explain MVVM / UDF trade-offs in interview language. +- Apply Android layering as a response to responsibilities rather than a prescribed folder hierarchy. +- Keep transport and persistence representations out of the UI. +- Use a local database as a source of truth when a feature needs durable observable data. +- Inject dependencies with explicit lifetimes and replacement points. +- Explain MVVM and unidirectional data flow in terms of data ownership and testability. ## A practical default shape @@ -185,7 +187,8 @@ Skip use cases when they are 1:1 wrappers around a single repository method - th - Navigation arguments should be **IDs**, not giant objects. - Shared ViewModels can be scoped to a navigation graph for multi-step flows (checkout). -- Keep NavController out of ViewModels when possible; emit events the UI navigates on. +- Keep `NavController` at the UI boundary. A ViewModel processes the user action and exposes the resulting destination or screen state; the UI performs navigation through a controlled, acknowledged effect. +- Treat deep-link arguments as external input and reload the required data through the feature boundary. ## Common pitfalls @@ -195,7 +198,7 @@ Skip use cases when they are 1:1 wrappers around a single repository method - th 4. **Over-modularizing** early (senior topic) without ownership pain. 5. **Two sources of truth** - memory list and database list that diverge. -## How interviewers probe this +## Examination prompts - “Draw the layers for a notes app with offline support.” - “Why repository?” diff --git a/src/content/study/mid/04-testing-and-performance.md b/src/content/study/mid/04-testing-and-performance.md index a7adf44..80a8c36 100644 --- a/src/content/study/mid/04-testing-and-performance.md +++ b/src/content/study/mid/04-testing-and-performance.md @@ -1,6 +1,6 @@ --- -title: Test behavior and diagnose performance -description: Use tests and traces to protect user behavior rather than implementation details or coverage numbers. +title: Testing strategy, performance measurement, and diagnosis +description: Test observable behavior at the appropriate layer and use evidence to diagnose rendering, startup, memory, and responsiveness problems. level: mid order: 4 duration: 50 min @@ -8,17 +8,17 @@ quizHref: /practice/?test=testing-quality-test quizLabel: Take the testing quiz --- -Good Android tests describe an **observable behavior**: given a repository result, the screen exposes content; given an I/O failure, the screen exposes a retryable error. A test should not fail only because you renamed a private function or reordered internal calls. +Good Android tests describe an observable contract. Given a repository result, a screen exposes content; given an I/O failure, it exposes a retryable error. A test that fails because a private method was renamed or an internal call moved has tested an implementation accident rather than behavior. -Performance work follows the same honesty: **measure**, classify the bottleneck, then fix. Guessing “maybe Compose is slow” wastes days. +Performance work follows the same standard of evidence. Measure a user journey in a representative build, classify the limiting resource, and change one cause at a time. A claim that a toolkit is slow without a trace, benchmark, or reproducible symptom is not a diagnosis. ## Learning goals -- Pyramid your tests: many fast unit tests, fewer instrumented tests. -- Prefer **fakes** over heavy mocks for repositories. -- Test coroutines and Flows with controlled time. -- Classify jank (main-thread work, overdraw, allocation, startup) before optimizing. -- Know the basic tooling: unit test runner, Espresso/Compose tests, Systrace/Macrobenchmark awareness. +- Use a test portfolio with many fast unit tests and a smaller number of instrumented checks. +- Prefer fakes when they make a collaborator's behavior clearer than interaction-based mocks. +- Test coroutines and Flow with controlled time and deterministic dispatchers. +- Classify jank, startup delay, allocation pressure, and overdraw before optimizing. +- Select an appropriate tool, including unit tests, Compose or Espresso tests, traces, and benchmarks. ## Start with fast, controlled tests @@ -206,7 +206,7 @@ You are not expected to recite every API, but you should say: *I measure, then f 4. Optimizing without a trace (“added caches everywhere”). 5. Ignoring flaky CI. -## How interviewers probe this +## Examination prompts - “How would you test this ViewModel?” - “Fake vs mock?” diff --git a/src/content/study/mid/05-persistence-networking-and-paging.md b/src/content/study/mid/05-persistence-networking-and-paging.md index ed5ab2d..ccff976 100644 --- a/src/content/study/mid/05-persistence-networking-and-paging.md +++ b/src/content/study/mid/05-persistence-networking-and-paging.md @@ -1,6 +1,6 @@ --- -title: Production data flows, caching, and Paging -description: Turn Room and HTTP calls into an observable source of truth with transactions, freshness rules, pagination, and recoverable failures. +title: Persistence, HTTP, caching, and Paging +description: Build a data flow that remains correct through refresh, pagination, cache invalidation, duplicate work, and credential changes. level: mid order: 5 duration: 75 min @@ -8,12 +8,14 @@ quizHref: /practice/?test=system-design-test quizLabel: Take the system design quiz --- -At mid level, "call the API and show the response" is no longer an architecture. You are expected to define the source of truth, concurrency rules, cache freshness, pagination keys, transactional boundaries, and the user-visible behavior of each failure. +At production scale, “call the API and show the response” is not an architecture. A data feature needs an explicit source of truth, freshness policy, concurrency rule, pagination key strategy, transaction boundary, and user-visible response to failure. Without those decisions, refreshes race, list items duplicate, and the interface tells conflicting stories about the same data. + +This chapter follows data across the local database and a remote HTTP service. The central design question is not which library performs a request. It is which representation the UI observes, how it is updated atomically, and how it remains coherent when the request is late, repeated, rejected, or only partially complete. ## Learning goals -- Design a repository around an explicit source of truth. -- Coordinate remote refresh and local observation without duplicate state. +- Design a repository around one explicit source of truth. +- Coordinate remote refresh and local observation without duplicating screen state. - Use Room transactions for atomic cache updates. - Explain Paging 3, load states, keys, and `RemoteMediator`. - Make retries safe through idempotence and request identity. @@ -270,7 +272,7 @@ The ViewModel maps these to actions and copy. The repository should not decide w 7. Sharing remote keys across different filters or accounts. 8. Refreshing tokens independently for every concurrent 401. -## How interviewers probe this +## Examination prompts - "What is the source of truth?" - "How do you show cached data while refreshing?" diff --git a/src/content/study/mid/06-background-work-services-notifications.md b/src/content/study/mid/06-background-work-services-notifications.md index acfe61c..3554156 100644 --- a/src/content/study/mid/06-background-work-services-notifications.md +++ b/src/content/study/mid/06-background-work-services-notifications.md @@ -1,6 +1,6 @@ --- -title: Background work, services, and notifications -description: Choose coroutines, WorkManager, foreground services, and alarms by guarantee instead of reaching for whichever API is familiar. +title: Background execution, services, and notifications +description: Choose an Android execution model from requirements for durability, urgency, user visibility, constraints, and cancellation. level: mid order: 6 duration: 75 min @@ -8,15 +8,17 @@ quizHref: /practice/?test=android-fundamentals-test quizLabel: Take the Android fundamentals quiz --- -Android limits background execution to protect battery, memory, privacy, and performance. "Run this in the background" is therefore incomplete. You must define when work should start, how long it runs, whether it must survive process death, whether timing is exact, and how visible it is to the user. +Android constrains background execution to protect battery, memory, privacy, and foreground responsiveness. “Run this in the background” is therefore an incomplete requirement. Before selecting an API, define the work's urgency, durability, constraints, execution duration, cancellation behavior, timing precision, and user visibility. + +WorkManager, a foreground service, a coroutine, an alarm, and a push message make different promises. This chapter turns those promises into a decision procedure and shows why idempotence, checkpointing, and notification design are part of the same operational contract. ## Learning goals -- Select a scheduling API from product requirements. -- Explain the guarantees and limits of WorkManager. +- Select a scheduling API from the product's required guarantee. +- Explain both the guarantees and limits of WorkManager. - Make workers idempotent, cancellable, and observable. - Distinguish started, bound, and foreground services. -- Build notification channels and `PendingIntent` objects safely. +- Build notification channels and `PendingIntent` objects with explicit identity and mutability. ## Make the decision from requirements @@ -243,7 +245,7 @@ The server remains the source of truth. A dropped push should not permanently pr 7. Treating `START_STICKY` as durable recovery. 8. Creating notification `PendingIntent` objects whose identities collide. -## How interviewers probe this +## Examination prompts - "Coroutine vs WorkManager?" - "What does WorkManager guarantee?" diff --git a/src/content/study/mid/07-navigation-deep-links-adaptive-ui.md b/src/content/study/mid/07-navigation-deep-links-adaptive-ui.md index a58a452..2c6375b 100644 --- a/src/content/study/mid/07-navigation-deep-links-adaptive-ui.md +++ b/src/content/study/mid/07-navigation-deep-links-adaptive-ui.md @@ -1,6 +1,6 @@ --- -title: Navigation, deep links, and adaptive UI -description: Model destinations and back stacks, validate external entry points, preserve state, and adapt one feature across changing window sizes. +title: Navigation, deep links, adaptive layouts, and window state +description: Preserve user intent across navigation and deep links while adapting information architecture to available window space. level: mid order: 7 duration: 70 min @@ -8,14 +8,16 @@ quizHref: /practice/?test=jetpack-compose-test quizLabel: Take the UI and Compose quiz --- -Navigation is more than changing screens. It defines how destinations are identified, which arguments cross boundaries, how back behaves, how external links enter the app, and how state survives when one window becomes two panes. +Navigation is a model of destinations, history, and user intent. It defines the identifier that crosses a screen boundary, the behavior of Back and Up, the treatment of an external link, and the way one feature reorganizes itself when a window gains enough space for two panes. A call that merely changes the visible screen is only the final step of this model. + +This chapter treats deep links as untrusted public input and adaptive layout as an information-architecture decision. The implementation should preserve the user's intent through authentication, recreation, resizing, and navigation history without passing unstable object graphs between destinations. ## Learning goals - Model navigation as destination state plus user actions. -- Explain back stack operations and state restoration. +- Explain back-stack operations and state restoration. - Treat deep links as untrusted external input. -- Keep navigation types out of lower layers. +- Keep navigation types at the UI boundary rather than in lower layers. - Design list-detail and primary-secondary layouts for adaptive windows. ## Destinations carry identifiers, not object graphs @@ -211,7 +213,7 @@ Avoid using `LocalConfiguration.current.screenWidthDp` as a complete window stra 7. Branching only on tablet vs phone. 8. Losing selection when a list-detail layout changes width. -## How interviewers probe this +## Examination prompts - "What should a route argument contain?" - "How do you keep NavController out of the ViewModel?" diff --git a/src/content/study/mid/08-dependency-injection-gradle-release.md b/src/content/study/mid/08-dependency-injection-gradle-release.md index 93c20d7..b899f7a 100644 --- a/src/content/study/mid/08-dependency-injection-gradle-release.md +++ b/src/content/study/mid/08-dependency-injection-gradle-release.md @@ -1,6 +1,6 @@ --- -title: Dependency injection, Gradle, and release variants -description: Build replaceable object graphs, control scope, understand Android build variants, and explain what changes between source code and a shipped app bundle. +title: Dependency injection, Gradle, artifacts, and release safety +description: Understand object graphs, build variants, packaging, shrinking, and release verification as one delivery system. level: mid order: 8 duration: 75 min @@ -8,15 +8,17 @@ quizHref: /practice/?test=architecture-test quizLabel: Take the architecture quiz --- -Mid-level Android engineers should be able to trace two graphs: the runtime object graph and the Gradle dependency graph. Dependency injection controls how objects are created and shared. Gradle controls how source, resources, manifests, generated code, and dependencies become an APK or app bundle. +Mid-level Android engineers should be able to trace two graphs. The runtime object graph describes how dependencies are created, configured, shared, and replaced. The Gradle dependency graph describes how source, resources, manifests, generated code, and external libraries become an APK or Android App Bundle. Delivery failures often occur when either graph is misunderstood. + +This chapter treats dependency injection and builds as engineering systems rather than framework vocabulary. A scope is an ownership decision, a variant is a product configuration, and a shrinker rule is a compatibility contract with code that is discovered dynamically at runtime. ## Learning goals -- Apply constructor injection and scope only when shared lifetime is required. -- Explain Hilt component lifetimes and common scope mistakes. +- Apply constructor injection and scope an object only when a shared lifetime is required. +- Explain Hilt component lifetimes and common scope errors. - Distinguish Gradle projects, modules, plugins, configurations, and variants. -- Predict build type and product flavor source-set merging. -- Explain D8, R8, resources shrinking, signing, APKs, and app bundles. +- Predict build-type and product-flavor source-set merging. +- Explain D8, R8, resource shrinking, signing, APKs, and Android App Bundles. ## Dependency injection is explicit construction @@ -249,7 +251,7 @@ More modules can improve parallelism and isolation, but too many tiny modules ad 7. Solving one missing R8 rule with a global keep rule. 8. Adding product-flavor dimensions without a product need. -## How interviewers probe this +## Examination prompts - "Constructor injection vs service locator?" - "What does a Hilt scope guarantee?" diff --git a/src/content/study/senior/01-modularization-and-boundaries.md b/src/content/study/senior/01-modularization-and-boundaries.md index e6cd314..75ba0bb 100644 --- a/src/content/study/senior/01-modularization-and-boundaries.md +++ b/src/content/study/senior/01-modularization-and-boundaries.md @@ -1,6 +1,6 @@ --- -title: Modularization and feature boundaries -description: Use module boundaries to reduce real coupling, protect ownership, and improve change speed. +title: Modularization, ownership, and architectural boundaries +description: Define Android modules from ownership and change patterns, then enforce dependency direction without creating ceremonial abstraction. level: senior order: 1 duration: 55 min @@ -8,17 +8,17 @@ quizHref: /practice/?test=architecture-test quizLabel: Take the architecture quiz --- -Modularization is not a goal by itself. A Gradle module adds build configuration, dependency management, navigation complexity, and cognitive overhead. It earns that cost when it reduces a **concrete pain**: teams collide in one feature, changes recompile too much of the app, internal implementations leak into many callers, or you need a clear ownership boundary for scale. +Modularization is a means of controlling coupling, not an architectural achievement by itself. A Gradle module adds configuration, dependency management, navigation decisions, and cognitive overhead. It earns that cost when it removes a specific problem: teams collide in the same feature, changes invalidate too much of the build, an implementation leaks through many callers, or ownership is unclear. -Senior engineers justify modules with **problems and metrics**, not fashion. +A senior design begins with change and ownership rather than package names. The proposed boundary should state what may depend on it, what it deliberately hides, which team owns it, and which measurable problem it improves. A module count is not evidence of a healthy architecture. ## Learning goals -- Find module boundaries from **change frequency and ownership**, not package names alone. -- Keep dependency arrows healthy (features depend on APIs, not on each other’s internals). -- Choose between monolith, package-by-feature, and multi-module with eyes open. +- Find module boundaries from change frequency and ownership rather than package names alone. +- Keep dependency direction healthy: features depend on stable APIs, not on each other's internals. +- Choose among a monolith, package-by-feature, and multiple modules from the current problem. - Migrate incrementally with measurable checkpoints. -- Discuss modularization in system-design / architecture interviews without dogma. +- Discuss modularization as a tradeoff rather than a universal rule. ## Find a boundary from change and ownership diff --git a/src/content/study/senior/02-offline-sync-and-system-design.md b/src/content/study/senior/02-offline-sync-and-system-design.md index c3f75f9..4c96d6c 100644 --- a/src/content/study/senior/02-offline-sync-and-system-design.md +++ b/src/content/study/senior/02-offline-sync-and-system-design.md @@ -1,6 +1,6 @@ --- -title: Offline sync and mobile system design -description: Design a mobile feature around unreliable networks, retries, conflicts, and user-visible truth. +title: Offline synchronization and mobile source-of-truth design +description: Design mobile features around unreliable transport, durable intent, retries, conflicts, and user-visible truth. level: senior order: 2 duration: 60 min @@ -8,17 +8,17 @@ quizHref: /practice/?test=system-design-test quizLabel: Take the system design quiz --- -Mobile system design starts with constraints the server cannot fully see: network availability changes, process lifetime is uncertain, storage is finite, radios are expensive, and the user can leave halfway through any request. A senior answer makes those constraints **explicit** before proposing endpoints or libraries. +Mobile system design begins with constraints that a server cannot control: networks change quality or disappear, process lifetime is uncertain, storage is finite, radio use has a cost, and a user may leave during any operation. An offline-capable design must therefore distinguish what a user has asked the device to do from what the server has durably acknowledged. -This lesson focuses on offline-capable product features and the design vocabulary used in mobile system design interviews. +This chapter establishes a vocabulary for that distinction. It treats local intent, queued work, attempted transport, server acknowledgement, conflict resolution, and visible state as separate concepts. A design is credible only when it names the behavior at each of those boundaries. ## Learning goals -- Separate **local intent** from **remote acknowledgement**. -- Design idempotent writes, outboxes, and conflict policy. -- Choose sync styles (fetch, push, periodic, push-notification triggered) deliberately. -- Define **product truth** the UI can render consistently. -- Structure a 30–40 minute system design answer for Android. +- Separate local intent from remote acknowledgement. +- Design idempotent writes, outboxes, and an explicit conflict policy. +- Choose among fetch, push, periodic, and push-triggered synchronization deliberately. +- Define product truth that the interface can render consistently. +- Structure a time-bounded Android system-design answer around the riskiest data path. ## Separate local intent from remote acknowledgement @@ -166,7 +166,7 @@ Each layer needs size limits and invalidation rules. “Cache everything forever 4. Ignoring auth expiry mid-sync. 5. Designing server-only truth while product promised airplane-mode editing. -## How interviewers probe this +## Examination prompts - “Design offline notes / queue / chat / photo backup.” - “What if the response is lost after success?” diff --git a/src/content/study/senior/03-reliability-and-technical-decisions.md b/src/content/study/senior/03-reliability-and-technical-decisions.md index 5005b5a..b4ed5bd 100644 --- a/src/content/study/senior/03-reliability-and-technical-decisions.md +++ b/src/content/study/senior/03-reliability-and-technical-decisions.md @@ -1,6 +1,6 @@ --- -title: Reliability, observability, and technical decisions -description: Connect architecture choices to rollout safety, production signals, and an explicit decision record. +title: Reliability, observability, rollouts, and technical decisions +description: Connect an engineering decision to measurable signals, controlled rollout, incident response, and explicit consequences. level: senior order: 3 duration: 55 min @@ -8,15 +8,17 @@ quizHref: /practice/?test=senior-full-mock quizLabel: Take the senior full mock --- -Senior engineering is not only selecting a pattern. It is making a choice the team can **ship safely**, **observe in production**, and **revisit when evidence changes**. Interviews for senior Android roles increasingly probe judgment: rollouts, migrations, incidents, and trade-offs under incomplete information. +Senior engineering is not limited to selecting a pattern. It is making a decision that can be delivered safely, measured in production, and revised when evidence changes. This requires an operational view of architecture: a migration has a rollback, a release has a kill switch, a metric has an owner, and an incident has a recovery path. + +The chapter connects technical judgment to operational evidence. It covers the signals that matter, the conditions that trigger action, the staged delivery practices that limit blast radius, and the decision record that allows a team to revisit an assumption without reconstructing history. ## Learning goals -- Instrument outcomes that answer product and reliability questions. -- Plan rollouts with flags, staged delivery, and kill switches. -- Write decision records that capture context, options, and consequences. -- Reason about failure modes end-to-end (client, network, backend, store). -- Communicate trade-offs without false certainty. +- Instrument outcomes that answer a product or reliability question without collecting unnecessary personal data. +- Plan rollout with flags, staged delivery, thresholds, and a kill path. +- Write decision records that preserve context, options, and consequences. +- Reason about failures across client, transport, backend, storage, and store distribution. +- Communicate a tradeoff with evidence and residual risk rather than false certainty. ## Instrument outcomes, not private data @@ -198,7 +200,7 @@ Speak in **failures and users**, not only library names. 4. Migrations without upgrade testing. 5. Overconfidence: “that can’t happen” without proof. -## How interviewers probe this +## Examination prompts - “Tell me about a technical decision you made and how you validated it.” - “How do you roll out a risky change?” diff --git a/src/content/study/senior/04-platform-internals-performance.md b/src/content/study/senior/04-platform-internals-performance.md index a5192b8..a6f1f1e 100644 --- a/src/content/study/senior/04-platform-internals-performance.md +++ b/src/content/study/senior/04-platform-internals-performance.md @@ -1,6 +1,6 @@ --- -title: Platform internals and performance engineering -description: Trace process startup, the main thread, Binder, ART, rendering, memory, and ANRs so performance answers begin with evidence. +title: Android runtime internals and performance engineering +description: Trace startup, the main thread, Binder, rendering, memory, ANRs, and compilation so performance work begins with evidence. level: senior order: 4 duration: 85 min @@ -8,11 +8,13 @@ quizHref: /practice/?test=platform-internals-test quizLabel: Take the platform internals quiz --- -Senior performance work is not a bag of micro-optimizations. It starts with a causal model of the platform, a user-visible metric, and a trace that connects the two. If a screen is slow, you should be able to ask whether the time is spent starting a process, loading code, blocking the main thread, waiting for Binder, decoding images, allocating memory, or missing rendering deadlines. +Senior performance work begins with a causal model, a user-visible metric, and evidence that connects them. A slow screen may be waiting for process startup, code loading, main-thread work, a Binder reply, image decoding, allocation or garbage collection, or a missed frame deadline. A micro-optimization that cannot name its measured cause is a guess. + +This chapter follows the runtime from process creation to a presented frame. It then uses that model to diagnose ANRs, jank, memory pressure, startup latency, and compilation behavior. The objective is not to memorize internals; it is to choose the next diagnostic tool from a plausible failure mechanism. ## Learning goals -- Trace a cold launch from process creation to first useful content. +- Trace a cold launch from process creation to useful content. - Explain the main thread, `Looper`, `MessageQueue`, Choreographer, and Binder boundaries. - Distinguish cold, warm, and hot startup. - Diagnose jank, ANRs, memory pressure, and leaks with appropriate tools. @@ -229,7 +231,7 @@ Compare percentiles, not only averages. A good median can hide a painful p95 or 7. Generating a Baseline Profile without measuring representative journeys. 8. Reporting initial display before the screen is useful. -## How interviewers probe this +## Examination prompts - "Walk through a cold app start." - "How do Handler, Looper, and MessageQueue relate?" diff --git a/src/content/study/senior/05-security-privacy-trust.md b/src/content/study/senior/05-security-privacy-trust.md index bf06213..97a191a 100644 --- a/src/content/study/senior/05-security-privacy-trust.md +++ b/src/content/study/senior/05-security-privacy-trust.md @@ -1,6 +1,6 @@ --- -title: Security, privacy, and client trust boundaries -description: Threat-model Android entry points, credentials, storage, networking, WebView, logs, and dependencies without pretending the client can keep server secrets. +title: Android security, privacy, and client trust boundaries +description: Threat-model entry points, credentials, storage, network behavior, WebView, telemetry, and dependencies without trusting the client as authority. level: senior order: 5 duration: 85 min @@ -8,15 +8,17 @@ quizHref: /practice/?test=system-design-test quizLabel: Take the system design quiz --- -Security answers become credible when they name an asset, attacker, entry point, control, and residual risk. "We encrypt it" is not enough. You need to say what is encrypted, where the key lives, which attack becomes harder, and which attacks remain possible. +Security analysis becomes credible when it names an asset, an attacker, an entry point, a control, and residual risk. “We encrypt it” is not a complete control description. A useful answer identifies what is encrypted, where the key resides, which attack is made harder, and which authority the client can never possess. + +The installed application is an execution environment controlled by the user, not a trusted extension of the server. The server must therefore remain authoritative for protected operations. Android-side controls reduce exposure, validate input, protect local material, and provide defense in depth; they do not convert a client-supplied fact into proof. ## Learning goals -- Threat-model exported components, intents, URIs, and pending intents. +- Threat-model exported components, intents, URIs, and `PendingIntent` objects. - Design authentication and authorization with the server as authority. - Protect local keys and sensitive data with realistic boundaries. - Configure network and WebView behavior deliberately. -- Reduce privacy risk in telemetry, backups, screenshots, and logs. +- Reduce privacy exposure in telemetry, backups, screenshots, and logs. ## Start with assets and trust boundaries @@ -245,7 +247,7 @@ For each feature: 8. Logging credentials while debugging an auth problem. 9. Treating root detection as authorization. -## How interviewers probe this +## Examination prompts - "Can you hide a secret in an APK?" - "How would you secure an exported Activity?" diff --git a/src/content/study/senior/06-mobile-system-design-interview.md b/src/content/study/senior/06-mobile-system-design-interview.md index 8f6539f..05c74f3 100644 --- a/src/content/study/senior/06-mobile-system-design-interview.md +++ b/src/content/study/senior/06-mobile-system-design-interview.md @@ -1,6 +1,6 @@ --- -title: The mobile system design interview -description: Turn an ambiguous feature into requirements, state ownership, APIs, storage, synchronization, failure handling, security, and measurable tradeoffs. +title: "Mobile system design: requirements, state, recovery, and scale" +description: Turn an ambiguous mobile feature into requirements, ownership, APIs, persistence, synchronization, security, recovery, and measurable tradeoffs. level: senior order: 6 duration: 95 min @@ -8,13 +8,15 @@ quizHref: /practice/?test=system-design-test quizLabel: Take the system design quiz --- -A strong mobile system design answer is not a diagram with repository boxes. It is a sequence of decisions tied to user behavior and failure conditions. The interviewer should hear what the product guarantees, where truth lives, how the client recovers, and what you would measure after release. +A strong mobile system-design answer is a sequence of decisions tied to user behavior, constraints, and failure conditions. A diagram containing repositories is insufficient. The interviewer should hear the product guarantee, the location of truth, the recovery behavior when transport fails, and the signals that would validate the design after release. + +The structure in this chapter is deliberately portable. It can be applied to chat, feeds, media, commerce, mapping, or collaboration because it begins with a contract and follows data through state ownership, APIs, persistence, synchronization, security, and operations. ## Learning goals -- Drive a system design interview from requirements to tradeoffs. -- Estimate the constraints that materially affect a mobile design. -- Define client state, server APIs, local schema, sync, and conflict policy. +- Drive a system-design interview from requirements to explicit tradeoffs. +- Identify the constraints that materially affect a mobile design. +- Define client state, server APIs, local schema, synchronization, and conflict policy. - Cover security, performance, testing, rollout, and observability. - Adapt the framework to chat, feed, media, checkout, maps, and collaboration. @@ -304,7 +306,7 @@ Deep dive on operation log, versions, conflict algorithm, presence, reconnect ga 8. Ignoring rollout compatibility between old clients and new APIs. 9. Ending without tradeoffs or metrics. -## How interviewers probe this +## Examination prompts - "Design WhatsApp messages on Android." - "Design an offline-first notes app." diff --git a/src/content/study/senior/07-senior-interview-execution.md b/src/content/study/senior/07-senior-interview-execution.md index 9ff088c..4a2b94a 100644 --- a/src/content/study/senior/07-senior-interview-execution.md +++ b/src/content/study/senior/07-senior-interview-execution.md @@ -1,6 +1,6 @@ --- -title: Senior interview execution and leadership signals -description: Communicate scope, tradeoffs, incidents, migrations, disagreement, mentoring, and impact with the specificity expected in senior and staff interviews. +title: Senior interview execution and engineering leadership +description: Communicate scope, technical judgment, incidents, migrations, disagreement, mentoring, and impact with evidence and precision. level: senior order: 7 duration: 80 min @@ -8,14 +8,16 @@ quizHref: /tests/ quizLabel: Take a full mock test --- -Senior interviews evaluate how you reduce ambiguity and move a system and team toward a better outcome. Technical knowledge is necessary, but the signal comes from decisions: what you noticed, which constraints mattered, what you changed, how you handled disagreement, and what measurable result followed. +Senior interviews evaluate how you reduce ambiguity and move a system and team toward a better outcome. Technical knowledge is necessary, but the evidence comes from decisions: what you observed, which constraints mattered, the options considered, the action taken, how disagreement was handled, and the measurable result. + +This chapter turns experience into evidence rather than chronology. A strong answer names the initial condition, personal ownership, decision mechanism, safeguards, outcome, and retrospective learning. It neither claims individual credit for team work nor hides responsibility behind vague collective language. ## Learning goals -- Structure project, incident, migration, and conflict stories around decisions. -- Separate personal contribution from team outcome. -- Demonstrate technical depth without drowning the answer in chronology. -- Discuss failures with ownership and evidence. +- Structure project, incident, migration, and conflict stories around a decision. +- Separate personal contribution from the team's outcome. +- Demonstrate technical depth without replacing reasoning with chronology. +- Discuss failures with ownership, evidence, and corrective action. - Ask senior-level questions that reveal engineering context. ## Build a story bank before the interview diff --git a/src/pages/study/[...id].astro b/src/pages/study/[...id].astro index 4f835d4..c8cb59d 100644 --- a/src/pages/study/[...id].astro +++ b/src/pages/study/[...id].astro @@ -67,8 +67,8 @@ const pathLabel = Check your understanding

Ready for a short quiz?

- Optional - reinforce this topic with a timed set, or skip ahead if - you already feel solid. + Use a timed set to test recall. If you continue without it, explain + the lesson's core contract and failure boundary without notes first.

diff --git a/src/pages/study/index.astro b/src/pages/study/index.astro index 157bb51..bc5a331 100644 --- a/src/pages/study/index.astro +++ b/src/pages/study/index.astro @@ -11,28 +11,41 @@ const lessons = (await getCollection('study')).sort( const levelMeta: Record< string, - { title: string; summary: string; audience: string; outcome: string } + { + title: string; + summary: string; + audience: string; + outcome: string; + evidence: string; + entry: string; + } > = { junior: { - title: 'Junior path', + title: 'Foundation curriculum', summary: - 'Build Android fundamentals in the order they connect in a real app: language, components, lifecycle, UI, storage, and network boundaries.', - audience: 'New to Android or preparing for your first mobile role', - outcome: 'Explain the platform and ship a complete, lifecycle-safe feature', + 'Build a precise model of the Android runtime before adding architecture patterns. The sequence moves from Kotlin types to components, ownership, rendering, data, and platform permissions.', + audience: 'For developers new to Android and candidates preparing for an entry-level role.', + outcome: 'Implement a small feature that survives recreation, renders explicit state, and handles data and permission boundaries correctly.', + evidence: 'Completion evidence: a lifecycle-safe detail screen with loading, error, empty, and content states.', + entry: 'Begin here unless you can explain configuration change, process death, and Context without notes.', }, mid: { - title: 'Mid-level path', + title: 'Production engineering curriculum', summary: - 'Turn fundamentals into production systems: async state, Compose, architecture, tests, data flows, background work, navigation, DI, and release builds.', - audience: '1–4 years of Android experience', - outcome: 'Own a production feature from UI state through release behavior', + 'Turn platform knowledge into a reliable production feature. These lessons connect asynchronous state, Compose, data ownership, tests, background execution, navigation, dependency graphs, and release artifacts.', + audience: 'For Android engineers who can already build screens and want to own features end to end.', + outcome: 'Design, implement, test, and release a feature with a clear source of truth and known failure behavior.', + evidence: 'Completion evidence: an offline-capable paged screen with tests, deep links, and a release-ready build.', + entry: 'Start here only after the foundation curriculum feels routine in both Views and Compose.', }, senior: { - title: 'Senior path', + title: 'Technical leadership curriculum', summary: - 'Own larger systems: boundaries, offline sync, reliability, platform performance, security, system design, and engineering leadership.', - audience: 'Leading features, tech design, or senior+ interviews', - outcome: 'Defend tradeoffs, failure behavior, rollout, and measurable impact', + 'Make decisions for systems rather than isolated screens. The focus is on architecture boundaries, synchronization, operational safety, platform behavior, security, and the communication expected from a technical lead.', + audience: 'For engineers designing cross-team work, leading delivery, or preparing for senior and staff interviews.', + outcome: 'Defend a design in terms of constraints, recovery paths, observability, rollout safety, and user impact.', + evidence: 'Completion evidence: a written design for a synchronizing mobile feature and an incident-aware release plan.', + entry: 'Begin after you can explain the mid-level choices as tradeoffs rather than library preferences.', }, }; @@ -49,29 +62,28 @@ const totalMinutes = lessons.reduce((sum, lesson) => { const totalHours = Math.floor(totalMinutes / 60); ---
Study plan -

Master Android from first principles to senior system design.

+

Learn Android as an engineering discipline.

- This is a complete interview curriculum, not a list of definitions. - Follow the paths in order to learn the runtime, UI, data, architecture, - performance, security, and design decisions behind production Android - apps. Every lesson includes exact APIs, code, failure modes, interview - probes, and exercises. + This curriculum teaches the contracts that govern a production Android + application: lifecycle ownership, state, concurrency, storage, process + lifetime, rendering, security, and delivery. It is designed for + deliberate study and interview preparation, not passive browsing.

-
{lessons.length}deep lessons
-
{totalHours}+study hours
-
3career paths
+
{lessons.length}sequenced lessons
+
{totalHours}+guided study hours
+
3competency stages
@@ -82,8 +94,10 @@ const totalHours = Math.floor(totalMinutes / 60); {DIFFICULTY_LABEL[group.level]}

{group.title}

{group.summary}

-

{group.audience}

+

Who this is for: {group.audience}

+

Entry check: {group.entry}

Outcome: {group.outcome}

+

{group.evidence}

{group.items.length} topic{group.items.length === 1 ? '' : 's'}
@@ -109,12 +123,13 @@ const totalHours = Math.floor(totalMinutes / 60); ))} @@ -209,6 +224,17 @@ const totalHours = Math.floor(totalMinutes / 60); color: var(--ink-faint); font-size: 0.82rem; } + .level-entry, + .level-evidence { + margin: 0.35rem 0 0; + max-width: 690px; + color: var(--ink-soft); + font-size: 0.86rem; + line-height: 1.5; + } + .level-evidence { + color: var(--ink-faint); + } .level-outcome { margin: 0.5rem 0 0; color: var(--ink-soft);