Free Android Tools Interview Questions & Answers

Gradle, R8, build performance, and profiling.

All 50 questions and detailed answers are free. No account or sign-in required.

  1. What are the three phases of a Gradle build and why does the configuration phase deserve special attention?

    Gradle runs initialization (deciding which projects participate and creating their Project objects), configuration (evaluating every build script and building the task graph), and execution (running the requested tasks in dependency order). The subtle trap is that configuration runs on every single invocation regardless of which task you asked for, so any expensive work placed directly in a build script body, like reading files, querying git, or eagerly creating tasks, taxes even a trivial build. Keeping configuration lean and deferring work into task actions is the foundation of fast builds.

  2. What is the difference between a build type and a product flavor, and what is a variant?

    A build type describes how you build the same code, such as debug versus release, controlling things like debuggability, minification, and signing. A product flavor describes different versions of the app, such as free versus paid or two branded builds, and lives in a flavor dimension. A build variant is the cross product of one build type and one flavor from each dimension, so two build types and two flavors in a single dimension yield four variants. The gotcha is that every extra flavor dimension multiplies the variant count and sync time, so people often add dimensions carelessly and pay for it in tooling speed.

  3. What is buildConfigField and how does BuildConfig end up in your code?

    buildConfigField declares a constant that the Android Gradle Plugin bakes into a generated BuildConfig class per variant, letting you branch on values like an API base URL or a feature flag known at build time. Each field is a type, name, and literal string that gets emitted verbatim into generated Java, so string values must include their own escaped quotes. A common surprise in newer AGP is that BuildConfig generation is off by default and must be enabled with the buildConfig build feature flag, otherwise the class simply does not exist and your import fails to resolve.

  4. Why do version catalogs and libs.versions.toml help a multi-module project?

    A version catalog centralizes dependency coordinates and versions in a single libs.versions.toml file, so every module references the same typed accessor like libs.retrofit rather than repeating group, name, and version strings. This guarantees one version across modules, makes coordinated upgrades a one-line change, and gives you compile-time-checked accessors with IDE completion instead of stringly-typed dependencies. It also supports bundles for grouping related libraries and plugin aliases, which removes drift where two modules accidentally pull different versions of the same library and cause duplicate-class or resolution headaches.

  5. What is the practical difference between api and implementation dependency configurations, and how does implementation speed builds?

    With implementation, a dependency is on your compile and runtime classpath but is not exposed to modules that depend on you, whereas api leaks the dependency onto consumers' compile classpath so they can use its types transitively. The build-speed payoff is that changing an implementation dependency only recompiles that module, not everyone downstream, because the ABI seen by consumers did not change. The trap is overusing api out of convenience, which recreates a monolithic classpath where a single library bump triggers a wide recompilation cascade.

  6. Why is KSP generally faster than kapt for annotation processing?

    Kapt runs Java annotation processors against Kotlin code by first generating Java stubs for all your Kotlin sources so a javac-based processor can see them, and that stub generation step is expensive and defeats some incrementality. KSP instead exposes a Kotlin-native symbol-processing API, reading the Kotlin program model directly without any stub round trip, which is why libraries that offer a KSP processor build noticeably faster. The nuance is that KSP is only faster if the library actually ships a KSP processor; forcing a kapt-only processor through KSP is not possible, so migration depends on ecosystem support.

  7. What replaced ProGuard in modern Android builds and what does it do?

    R8 replaced ProGuard as the default code shrinker and is integrated directly into the Android Gradle Plugin, performing shrinking to remove unused code, obfuscation to rename classes and members, and optimization such as inlining and class merging in a single pass. It consumes the same keep-rule syntax as ProGuard so existing configuration mostly carries over, but it is generally more aggressive and produces smaller, faster output. Because it does all three jobs together, misunderstanding it as only a renamer leads people to forget that it also deletes code paths it cannot prove are reachable.

  8. Why do reflection-based or serialization libraries sometimes crash only in release builds, and how do keep rules fix it?

    In release builds R8 shrinks and renames anything it cannot statically prove is used, and reflection or reflective serialization accesses classes and fields by name at runtime, which R8 cannot see, so it may strip or rename those members and produce ClassNotFoundException, NoSuchMethodError, or nulled-out fields. The fix is a keep rule that tells R8 to preserve the affected classes and their members, either via a hand-written keep directive in the rules file or, better, via consumer rules the library ships. The gotcha is that debug builds do not minify, so the bug hides until the release build and looks like a data or network problem rather than a shrinking issue.

  9. What is mapping.txt and how does turning off obfuscation relate to it?

    When R8 obfuscates, it renames symbols and writes a mapping.txt that records the original-to-renamed correspondence, which you feed to the retrace tool to turn an obfuscated production stack trace back into readable names. Using the dontobfuscate option disables renaming so stack traces stay readable without retracing, but you lose the size and tamper-resistance benefits while still keeping shrinking and optimization. The correct production practice is to keep obfuscation on and archive the mapping.txt for every release, since it is version-specific and a mismatched mapping deobfuscates a crash incorrectly.

  10. What is configuration avoidance and how do tasks.register and tasks.create differ?

    Configuration avoidance means declaring tasks lazily so Gradle only configures a task if it actually ends up in the execution graph, which trims the configuration phase. tasks.register returns a lazy provider and does not configure the task until something realizes it, whereas tasks.create eagerly instantiates and configures it immediately during configuration, whether or not it will run. The same idea extends to using providers and configureEach instead of eager getters and forEach, and the subtle failure is that calling get or iterating the container prematurely realizes everything and quietly undoes the avoidance.

  11. What is the difference between the Gradle build cache and the configuration cache?

    The build cache stores the outputs of individual tasks keyed by their inputs so an identical task invocation, even on another machine with a remote cache, can be skipped and its outputs reused. The configuration cache is different in kind: it caches the result of the entire configuration phase, the serialized task graph, so subsequent builds skip re-running your build scripts entirely. They are complementary, and confusing them is common; one avoids re-executing work while the other avoids re-computing what work to do.

  12. What kinds of code break the configuration cache?

    The configuration cache requires that configuration not depend on live external state and that tasks not reach into the Project object at execution time. Reading system properties, environment variables, the system clock, or running arbitrary processes during configuration invalidates or forbids caching because that state is not captured deterministically, and referencing Project or other non-serializable objects inside a task action throws. The fix is to capture needed values into serializable inputs or Provider values at configuration time and use the provider APIs like providers.environmentVariable, so the cached graph stays valid and reads are tracked.

  13. What is the difference between an APK and an AAB, and why did Google Play move to AAB?

    An APK is the installable artifact a device runs, while an Android App Bundle is a publishing format you upload to Play that contains all your compiled code and resources but is not itself installed. Play uses the bundle to generate and sign optimized split APKs tailored to each device, so a phone downloads only the density, ABI, and language resources it needs rather than a universal APK carrying everything. The consequence is smaller downloads, but because Play now does the final signing and splitting, you must test with bundletool or an internal track rather than assuming your locally built universal APK matches what users receive.

  14. What is a baseline profile and how does it improve startup?

    A baseline profile is a list of classes and methods, shipped in the AAB, that Android Runtime uses to ahead-of-time compile the hottest code paths at install time instead of relying purely on interpreted or JIT execution as the app warms up. This removes JIT compilation and interpretation overhead from critical journeys like cold startup and first scroll, commonly cutting startup time noticeably. The nuance is that the profile must be generated from a representative user journey, typically via a Macrobenchmark test with BaselineProfileRule, and a stale or empty profile gives little benefit, so it should be regenerated as the app changes.

  15. What is the difference between Macrobenchmark and Microbenchmark?

    Microbenchmark measures the performance of small pieces of code in a tight loop within the same process, ideal for hot functions, and it warms up the JIT and reports nanosecond-level timings. Macrobenchmark measures whole user-facing interactions like app startup and scrolling by launching the real app in a separate process, controlling compilation mode, and reading frame and timing metrics. The trap is using the wrong tool: Microbenchmark cannot tell you about startup or jank, and Macrobenchmark is too coarse for a single algorithm, and both must run on a physical device in a release-like build to produce trustworthy numbers.

  16. What does StrictMode detect and why should it never gate production behavior?

    StrictMode is a developer tool that flags accidental main-thread disk and network I/O under its thread policy and object leaks like unclosed cursors or leaked SQLite objects under its VM policy, surfacing them via log, dialog, or crash during development. It is meant to catch mistakes early, not to run in production, and enabling penaltyDeath in a shipped build would crash real users on issues that are merely warnings. The correct pattern is to enable it only in debug builds, usually early in Application onCreate, and treat its reports as a to-do list rather than as runtime enforcement.

  17. How does LeakCanary detect memory leaks and what does it actually report?

    LeakCanary watches objects that should be garbage collected, such as destroyed activities and fragments, using weak references, and when one is not collected after a forced GC it dumps the heap and analyzes it to find the shortest strong reference chain keeping the object alive. It reports that leak trace so you can see exactly which field or static reference is the culprit, rather than just telling you memory is growing. The important caveat is that it is a debug-only dependency and its heap dumps pause the app, so it must never ship in release builds, and it finds retained-instance leaks, not general high memory usage.

  18. What is Lint's baseline file and what problem does it solve?

    Android Lint statically analyzes code and resources for correctness, performance, accessibility, and API issues, and on a large legacy codebase it can report thousands of preexisting warnings. A Lint baseline snapshots all current issues into an XML file so the build treats only new issues as failures, letting a team enforce zero new problems without first fixing the entire backlog. The gotcha is that a baseline can silently hide regressions if issues drift, and it should be periodically regenerated and shrunk, not treated as a permanent excuse, since anything recorded in it is effectively suppressed.

  19. Why did multidex exist and is it still relevant?

    A single DEX file can reference at most 65,536 method references because the method index is a 16-bit field, and large apps exceeded this, so multidex splits the app across multiple DEX files and, on very old devices, adds a support library to load the secondary DEX at startup. On Android 5.0 and above the ART runtime natively supports multiple DEX files, so multidex is essentially automatic and the startup cost is gone once your minSdk is 21 or higher. The remaining relevance is mostly historical, though the 64K reference limit itself still exists and shrinking with R8 is the better remedy for method count than relying on the split.

  20. What is core library desugaring and what does it let you use?

    Core library desugaring lets you call newer Java APIs, such as the java.time date-time classes and some java.util.stream features, on older Android versions whose bundled runtime lacks them, by bundling a backported implementation and rewriting bytecode to target it. You enable it with the coreLibraryDesugaring dependency and the isCoreLibraryDesugaringEnabled flag. The distinction people miss is that this is about library APIs, separate from ordinary language desugaring of features like lambdas, and it adds a small amount to app size and requires D8 desugaring to be enabled.

  21. How does resolutionStrategy help with dependency version conflicts, and what does Gradle do by default?

    When two dependencies transitively pull different versions of the same library, Gradle's default conflict resolution picks the highest version that satisfies the constraints, which usually works but can silently upgrade you past a breaking change. resolutionStrategy lets you take control by forcing a specific version, failing the build on any version conflict so nothing is silently upgraded, or substituting one module for another. The nuance is that forcing a version can create runtime incompatibilities if a transitive consumer needed a different API, so forcing should be paired with testing rather than used to blindly silence a resolution warning.

  22. What are convention plugins and why prefer them over allprojects or subprojects blocks?

    Convention plugins are custom Gradle plugins, typically written in a buildSrc or an included build-logic module, that encapsulate shared configuration like Kotlin options, Compose setup, and common dependencies so each module simply applies the plugin. They are preferred over cross-project configuration blocks like allprojects and subprojects because those blocks reach into other projects during configuration, which hurts configuration-cache compatibility and creates implicit coupling. The payoff is consistency and one place to change build conventions, while keeping each module's build script small and each project configuring only itself.

  23. What are signing configs, and how does Play App Signing change who holds the key?

    A signing config bundles the keystore, key alias, and passwords the build uses to sign an APK or bundle, and traditionally you held the single app signing key that must never change across updates. With Play App Signing, you upload with an upload key while Google holds and manages the actual app signing key, re-signing the artifacts Play distributes, which protects the master key and enables key rotation. The subtlety is that the upload key and the app signing key are different, and losing the upload key is recoverable through support whereas, in the legacy model, losing the sole signing key permanently blocks updating that app.

  24. How do dynamic feature modules and on-demand delivery work?

    A dynamic feature module is packaged in the app bundle but can be delivered conditionally, either installed at first download, on demand at runtime through the Play Feature Delivery APIs, or based on device conditions, keeping the base install smaller. The runtime code must handle the module not being present yet, requesting and monitoring its install, and the fact that its code and resources appear only after install completes. The common mistake is assuming a dynamic feature's classes and resources are always accessible; before installation they are absent, so direct references must go through the delivery API and split-compat handling.

  25. What is JankStats and how does it define jank differently from a simple frame counter?

    JankStats is a Jetpack library that reports frame timing on a per-frame basis, computing each frame's duration against a dynamically adjusted target rather than a fixed threshold, and it lets you attach application state so you know which screen or interaction was janky. It leverages the platform frame metrics on newer versions and a fallback on older ones, giving you aggregated jank data you can log to analytics. The insight it captures is that jank is a frame exceeding the deadline for the current refresh rate, so a naive count against a hardcoded 16 millisecond budget is wrong on 90 or 120 hertz displays where the budget is smaller.

  26. How can you count Compose recompositions and what does an unexpectedly high count usually indicate?

    Android Studio's Layout Inspector shows recomposition and skip counts per composable while the app runs, and the Compose compiler can also emit metrics and reports about which functions are restartable and skippable. A surprisingly high recomposition count usually points to unstable parameters, since Compose can only skip a composable when its inputs are stable and unchanged, so passing an unstable type like a plain List, a lambda capturing changing state, or a class the compiler cannot prove stable forces recomposition. The fix is to make state reads as narrow as possible and use stable or immutable types so Compose can skip work rather than repeat it.

  27. What is the difference between the Android Studio Profiler and Perfetto, and when do you reach for each?

    The Android Studio Profiler gives interactive, in-IDE views of CPU, memory, network, and energy for your app, convenient for quick investigation during development. Perfetto, and the older systrace it succeeded, captures a full system-wide trace across all processes, the kernel scheduler, binder transactions, and frame lifecycle, which you analyze in the Perfetto UI to understand contention, thread scheduling, and where time actually goes during a slow interaction. The rule of thumb is to use the Profiler for a first look at your own process and Perfetto when you need whole-system context, precise timing, or to correlate your app with system services.

  28. Why is it a mistake to profile or benchmark a debuggable build, and what should you use instead?

    Debug builds are not representative because they are not minified, disable ahead-of-time optimizations, carry extra debugging instrumentation, and the debuggable flag itself slows ART, so any timing measured on them is systematically pessimistic and misleading. Benchmarks and profiling should run against a release-like build that is minified and non-debuggable, which is exactly why Macrobenchmark encourages a dedicated benchmark build type based on release. Measuring on debug is one of the most common ways teams draw false conclusions about performance and chase problems that vanish in production.

  29. How do incremental and parallel compilation differ, and what defeats incrementality?

    Parallel compilation runs independent modules or tasks concurrently across CPU cores, so a well-decomposed multi-module project builds faster, whereas incremental compilation recompiles only the sources affected by a change instead of the whole module. Incrementality is defeated when a change touches something with wide impact, such as modifying a public ABI that many files depend on, using api dependencies that ripple downstream, or annotation processors that are not incremental-aware. The practical guidance is to keep module boundaries clean, prefer implementation over api, and prefer KSP or incremental processors so a one-line edit does not trigger a full rebuild.

  30. How does Play split an app bundle, and what do configuration splits contain?

    From one uploaded bundle Play generates a base APK plus configuration split APKs along three axes: screen density so only the needed drawable buckets ship, ABI so only the matching native libraries ship, and language so only the device's locales ship. A device downloads the base plus exactly the configuration splits it needs, which is why bundle downloads are smaller than a universal APK. The gotcha appears when you access resources for a configuration the device did not receive, for example loading a locale the user has not installed; you must account for language splits or disable language splitting if you switch locales in-app.

  31. What goes wrong if you write a keep rule that is too broad, like keeping an entire package?

    A keep rule instructs R8 to preserve matching classes and members from shrinking, renaming, or optimization, so a broad rule like keeping an entire package or all classes with all members prevents R8 from removing dead code, renaming symbols, and inlining across that region. The result is a larger APK, weaker obfuscation, and forfeited optimizations, quietly eroding the benefits you enabled R8 for. The disciplined approach is to keep the narrowest surface actually accessed reflectively, ideally through library-provided consumer rules and annotations like Keep, rather than blanket wildcards that silently opt large parts of the app out of shrinking.

  32. Why does reading a system property or running git during configuration hurt every build, even with caching?

    Any logic in the configuration phase executes on every invocation before Gradle knows which task you want, so shelling out to git or reading system state at configuration time adds latency to trivial commands like listing tasks. Worse, it makes the build non-deterministic from Gradle's perspective and is incompatible with the configuration cache, which needs configuration inputs to be declared and stable, so it either forbids caching or forces frequent invalidation. The correct pattern is to defer such reads into task execution using tracked provider APIs or a ValueSource, so the value is captured as a declared input rather than an uncontrolled side effect.

  33. What is a Microbenchmark's warmup and allocation tracking really protecting you from?

    Microbenchmark repeatedly runs your code, discarding initial iterations as warmup so the JIT has compiled the hot path and one-time costs like class loading are excluded, then reports stabilized timings, and it also warns or fails if the build is debuggable or the device is thermally throttled. It can track allocations to catch a regression where a change is timing-neutral but starts allocating in a hot loop, which would later show as GC pressure and jank. The point it enforces is that a naive single-run timing measures JIT warmup and noise, not steady-state performance, so trusting one unwarmed run leads to wrong conclusions.

  34. In CI, why run assemble, test, and lint as distinct steps, and how does the mapping file fit into release CI?

    assemble verifies the app actually compiles and packages, test runs unit and instrumentation suites to catch behavioral regressions, and lint catches correctness, accessibility, and resource issues static analysis can find without executing code, so treating them as separate gates gives precise failure signals and lets you fail fast on the cheapest check. For release pipelines, CI must also archive or upload the R8 mapping.txt and native symbols to your crash reporter, because without them production stack traces stay obfuscated and unsymbolicated. Forgetting the mapping upload is a classic omission that leaves you unable to read the very crashes you shipped.

  35. Why can enabling the configuration cache surface bugs that a plugin had hidden for years?

    The configuration cache enforces strict rules that well-behaved builds should already follow: tasks must not reference the Project at execution time, must declare their inputs and outputs, and must not share mutable state across the boundary, so turning it on makes latent violations throw instead of silently working. A plugin that reached into a project's build directory inside a doLast action, or captured a Task reference, previously happened to work only because configuration ran every time. Enabling the cache is therefore both a speed win and a correctness audit, and the migration effort is largely fixing plugins that were quietly violating Gradle's execution model.

  36. How does a custom Lint rule work, and what is an Issue versus a Detector?

    A custom Lint rule is packaged as a Detector, the class that scans the code, resource, or manifest model via scopes and UAST or XML visitors, paired with one or more Issue objects that define the id, severity, category, and explanation reported to users. You register your issues through an IssueRegistry so the Lint infrastructure discovers them, and you can ship rules with a library so consumers automatically get project-specific checks like banning an internal API. The subtlety is choosing the right scope and analysis mode so the detector runs efficiently and, ideally, participates in incremental and partial analysis rather than forcing full scans.

  37. When R8 optimizes across your code, why can a stack trace line number be misleading even after retracing?

    R8 does more than rename; it inlines methods, merges classes, and removes or reorders code, so a frame you retrace may point into a method that was inlined into its caller, making the original call site ambiguous or the line appear to come from unexpected code. Modern mapping files record inlining information so retrace can reconstruct the inline stack, but this only works if you use a compatible retrace version and the exact mapping for that build. The lesson is that optimization changes the shape of the code, so deobfuscation is not a simple one-to-one rename and requires the full mapping to reconstruct what really happened.

  38. How do you generate a baseline profile in a way that actually reflects real usage, and what is the failure mode of a bad one?

    You write a Macrobenchmark test using BaselineProfileRule that drives the critical user journeys, startup, first navigation, initial scroll, on a physical device, and the tooling records which classes and methods are exercised into a profile that is then packaged in the release bundle. The failure mode is a profile that covers only launch and an empty screen, or one that has gone stale after significant code changes, so ART ahead-of-time compiles the wrong methods and users see little startup improvement while you believe you optimized it. Treating the profile as generated build output that must be regenerated and verified with a before-and-after Macrobenchmark is what makes it trustworthy.

  39. Why might a task appear to run every build despite the build cache, and how do you diagnose it?

    The build cache only reuses a task's output when the task is cacheable and its declared inputs and outputs hash identically, so a task reruns if it is not marked cacheable, if it has an undeclared input like an environment variable or absolute path, or if a non-reproducible input such as a timestamp changes each build. You diagnose it with a build scan or the input-comparison tooling, which shows which input changed between builds. The underlying principle is that caching correctness depends on complete and stable input declaration, and hidden inputs are the usual culprit behind a task that never gets a cache hit.

  40. What is the risk of forcing a dependency version globally with resolutionStrategy force in a multi-module build?

    Forcing a version overrides Gradle's conflict resolution everywhere, pinning the module to your chosen version regardless of what transitive consumers requested, which resolves a conflict but can downgrade a library below the minimum a dependency needs or upgrade it past a breaking API, producing NoSuchMethodError or subtle runtime failures that compile cleanly. Because the force is global and silent, the incompatibility surfaces far from the declaration. A safer modern alternative is a version catalog with constraints or a platform BOM that expresses compatible versions declaratively, so alignment is coordinated rather than bluntly overridden.

  41. How does Play App Signing enable key rotation, and what limitation remains for older devices?

    Because Google holds the app signing key under Play App Signing, it can perform key rotation using the APK Signature Scheme v3 lineage, where a new key is certified by the old one so updates remain trusted without a fresh install. The limitation is that key rotation and the newer signature schemes are only honored on sufficiently new Android versions, so on older devices the original key still governs identity and certain rotation benefits do not apply. This means rotation improves your security posture going forward but is not a clean break, and anything that pins to the signing certificate must account for both the old and rotated keys.

  42. Why can enabling R8 full mode break code that worked with ProGuard-compatible mode, and how do you respond?

    R8 full mode, the default in recent AGP, drops several ProGuard-compatibility assumptions and makes more aggressive assumptions about reachability, for instance being stricter about keeping default constructors, annotations, and generic signatures unless explicitly told to, so reflection or serialization that relied on those implicit retentions can start failing. The response is not to disable full mode but to add the specific keep rules, such as keeping generic signatures and runtime-visible annotations for your serialization library, ideally supplied as the library's consumer rules. It rewards understanding exactly what reflective access your code performs rather than blanket-keeping everything to make the crash go away.

  43. How does a ValueSource or provider-based approach let you read external state without breaking the configuration cache?

    The configuration cache forbids uncontrolled reads of external state, but Gradle provides tracked entry points like providers.environmentVariable, providers.systemProperty, and custom ValueSource implementations that declare and capture the value so the cache knows the input and can decide when to invalidate. Wrapping a git-describe call in a ValueSource, for example, lets the version be computed lazily and re-evaluated only when appropriate, instead of an unmanaged process call during configuration that would either be forbidden or make caching unsound. The key mental shift is treating every external read as a declared, lazy provider input rather than an imperative side effect in the script body.

  44. Why can two modules that both apply the same convention plugin still end up with different behavior, and how do you prevent it?

    A convention plugin centralizes configuration, but drift creeps in when a module overrides settings after applying the plugin, applies an extra plugin that changes defaults, or the convention reads a module-specific value like a property that is set inconsistently, so the same plugin yields different Kotlin or Compose configuration per module. Prevention means keeping convention plugins authoritative and side-effect-free, avoiding per-module overrides, and using version catalogs so even the dependency versions the convention pulls are identical. The trap is assuming applying the plugin guarantees uniformity when local overrides can silently diverge from the intended baseline.

  45. When measuring startup with Macrobenchmark, why does CompilationMode matter and which mode reflects real user devices?

    Macrobenchmark lets you set the CompilationMode used before measuring, and results differ dramatically between fully JIT-compiled with no ahead-of-time compilation and a mode that applies your baseline profile, so a number is meaningless without stating the mode. To reflect what users experience shortly after install you measure with the baseline-profile mode, since that mirrors the ahead-of-time compilation ART performs from your shipped profile, whereas full ahead-of-time compilation overstates real-world performance and none understates it. Reporting startup without pinning the compilation mode is a classic way to produce numbers that look great in one configuration and cannot be reproduced by anyone else.

  46. Why is enabling BuildConfig, view binding, or Compose treated as a build feature flag, and what changed in recent AGP defaults?

    The Android Gradle Plugin gates optional generated code and processing behind buildFeatures flags so projects only pay the sync and generation cost of features they use, and in recent AGP versions several of these, notably BuildConfig, are disabled by default to keep builds lean. The practical consequence is that code referencing BuildConfig or a binding class suddenly fails to compile after an AGP upgrade until you explicitly enable the corresponding build feature. Understanding this prevents the confusing situation where an upgrade appears to delete generated classes, when in fact the default simply flipped and the feature must be opted into.

  47. How can obfuscation interact badly with enums, Parcelable, or Serializable, and what keep considerations apply?

    Obfuscation renames members, which is fine for code that only references them by symbol, but reflection-driven mechanisms break subtly: enum valueOf and name-based lookups fail if enum constants are renamed, Serializable relies on class and field names and a stable serialVersionUID, and Parcelable requires the CREATOR field to survive. R8 ships default rules for some of these, like keeping enum values methods and Parcelable CREATOR, but Serializable field-name stability across obfuscated builds is fragile and is generally a reason to avoid Java serialization for persisted data. The takeaway is to know which framework contracts are name-based and keep exactly those, rather than assuming obfuscation is always transparent.

  48. Why can a project build faster after being split into more modules, and when does adding modules backfire?

    More modules enable Gradle to compile independent modules in parallel and let incremental builds recompile only the changed module and its dependents, and with clean api and implementation boundaries a local change stays contained, so parallelism and reduced recompilation scope speed up iterative builds. It backfires when modules are too fine-grained or heavily interdependent, because the per-module configuration and task overhead, plus a dense dependency graph where everything depends on everything, serialize the build and inflate sync time. The goal is cohesive modules with narrow public surfaces, not maximal fragmentation, so the graph is wide and shallow rather than deep and entangled.

  49. During a slow-startup investigation, how do you use Perfetto to tell whether the bottleneck is your code, I/O, lock contention, or scheduling?

    In a Perfetto trace you inspect your main thread's slices to see where wall time accumulates, then correlate with the thread state track: a thread that is runnable but not running indicates the scheduler had no core for it, time in uninterruptible sleep points to blocking I/O, and a slice waiting on a monitor reveals lock contention with another thread you can follow via the blocking-call links. You also watch binder transaction tracks to catch time lost calling into system services. This whole-system view distinguishes your own slow code from external causes, which a single-process profiler that only shows your CPU usage cannot disambiguate.

  50. Why is deobfuscating a production crash sometimes impossible even when you understand obfuscation, and what discipline prevents it?

    Retracing a stack trace requires the exact mapping.txt produced by the same build that shipped, and because R8's renaming and optimizations are non-deterministic across builds, a mapping from a rebuilt binary, even from identical source, will not correctly deobfuscate the original crash, and native crashes additionally need the matching unstripped symbols. If the mapping and symbols were not archived or uploaded to the crash reporter at release time, that build's traces are permanently unreadable. The preventive discipline is to make mapping and symbol upload an automatic, non-skippable step of every release build in CI, keyed to the version code, so every shipped artifact has its retracing data preserved.

Practice all Android Tools questions interactively

Search, filter, and mark questions complete in the free Preparation Path. You can start immediately without an account.

Open free Preparation Path

More Android interview topics