cswork / libraryOpen practice workspace

Articles

Android Build Interview Questions: Variants, R8, and Dependency Conflicts

Prepare for Android build interviews with variant tracing, R8 failure diagnosis, dependency conflict examples, and release-artifact verification steps.

An Android application can compile successfully, pass debug tests, and still fail as a release artifact. Android build interview questions ask whether you can explain that gap using the selected variant, dependency graph, merged configuration, and optimizer behavior.

The practical answer is to identify the exact failing artifact before changing anything. Compare its source sets and runtime dependencies, reproduce with the same build configuration, and isolate optimization as a variable. A release-only crash is evidence to investigate R8, not proof that R8 is defective.

The original scenarios below focus on that diagnostic reasoning. Android Gradle Plugin, Gradle, Kotlin, and library versions matter; examples here are conceptual unless a specific configuration is stated, and the linked official documentation should be read against the project's pinned versions.

1. What exactly is a build variant?

A build variant combines a build type with a selection of product flavors across configured flavor dimensions. A build type commonly controls development or release behavior, while flavors describe product differences such as a free and paid edition.

With one dimension containing free and paid, and build types debug and release, there are four combinations before filtering: freeDebug, freeRelease, paidDebug, and paidRelease. Additional dimensions multiply the combinations, although a project can disable combinations it does not ship.

The interview value is in the next question: which combination failed? "Release is broken" is incomplete if free release works and paid release crashes. Ask for the task used, application ID, version, device conditions, and artifact identity.

Keep a reproduction note attached to the artifact. A successful local run of freeDebug says little about a failure in the paid release delivered to testers.

2. How do source sets change the code you ship?

Android source sets allow a variant to draw from shared and more specific locations. Resources and manifest content have documented merge priorities; source code needs extra care because two included classes with the same fully qualified name can cause a duplicate-class failure rather than acting like a simple resource override.

Consider a payment URL that should differ between debug and release. Inspect the generated configuration or packaged resource used by the failing variant, not just the file a developer expects to win. The final merged result is the relevant evidence.

A useful interview exercise supplies a shared app name and a paid-edition app name, then asks which appears in paidRelease. Explain the applicable merge rules, then verify the result in the merged resources or artifact. Do not assume every file type follows the same override behavior.

For manifests, inspect the merger report when a permission, provider, or exported setting appears unexpectedly. A dependency can contribute configuration that is not visible in the app's primary manifest.

3. What does R8 do beyond renaming classes?

Android's documentation describes shrinking, optimization, and obfuscation as distinct activities. Shrinking removes unreachable code; optimization can rewrite code; obfuscation changes names. Resource optimization also depends on the plugin version and settings.

This distinction matters because an exception mentioning a missing class might involve removal, renaming, a missing dependency, or a variant-specific path. Simply saying "disable obfuscation" does not identify which transformation matters.

Current Android documentation distinguishes a newer optimization DSL from legacy configuration. Before pasting a build-script snippet, inspect the project's AGP version and the corresponding supported syntax. A code example that is correct for one project can fail to configure another.

In an interview, keep the explanation tied to evidence: what was reachable in the analyzed program, what was only discovered dynamically, and what changed between the working and failing artifact? You are demonstrating a method for narrowing the problem, not guessing the optimizer's internals from one stack trace.

4. Why can reflection fail only after optimization?

Suppose an original example loads com.example.ReportAdapter from a string in configuration and invokes its constructor. Ordinary static references may not fully communicate that runtime requirement to optimization tools. If the class or required name is not preserved appropriately, the optimized artifact can behave differently.

First confirm the class belongs to the selected variant and is present through the intended runtime dependency. Next inspect the library's integration instructions and consumer rules. Only then design a rule around the actual dynamically accessed class or member.

Treat a broad keep rule as a diagnostic experiment, not a finished fix. If preserving an entire package removes the symptom, reduce the rule while retaining the specific reflection path and regression coverage.

A generated serializer or explicit adapter registration may make the dependency easier to analyze than unrestricted reflection. That is an architectural option to evaluate, not a requirement to rewrite a library during an incident.

5. How narrow should a keep rule be?

A keep rule should preserve the behavior the runtime actually requires. Keeping a class, preserving a member name, and preventing removal are different constraints. Android explicitly documents that names-only options do not necessarily prevent shrinking.

For the report-adapter example, write down three questions: must the class exist, must a particular constructor exist, and must the original name remain stable? The answer depends on how the lookup works. A registry holding a class reference has different needs from a configuration file holding its full name.

Document why a rule exists next to the rule or in its ownership notes. "Fix release" is not enough for someone deciding whether it remains necessary after a library upgrade. Record the runtime access path and the test that exercises it.

Also distinguish rules packaged by a library for its consumers from rules used while building that library. The application ultimately needs the constraints relevant to its optimization step. Inspect published consumer guidance before copying rules from an unrelated repository.

6. What is a dependency conflict actually telling you?

Gradle's dependency reports show requested and selected dependencies and the reasons for selection. Use the configuration belonging to the failing variant rather than assuming every classpath is identical.

Imagine the application requests codec version 1.4 and an analytics library requests version 2.0. Under ordinary version-conflict resolution, absent overriding constraints or other rules, 2.0 would be selected. That is not a compatibility test. It only explains which version won.

EvidenceWhat it answersWhat it cannot establish
Dependency treePaths introducing a moduleRuntime behavior is correct
Dependency insightWhy a version was selectedEvery caller uses compatible APIs
Runtime classpathLibraries packaged for executionAll reflective entry points survive
Crash reproductionThe observed failing pathEvery variant fails identically

Do not describe all dependency failures as version conflicts. Duplicate classes can come from different artifacts packaging overlapping classes, which is a different problem from selecting one version of one module.

7. How would you investigate a missing method?

A NoSuchMethodError suggests that a caller expects a method unavailable in the class actually loaded. Begin with the full error, caller, and resolved artifact versions. Check compile and runtime classpaths, including any variant-specific dependency declarations.

For the codec scenario, suppose the analytics adapter was compiled against a method in one API shape, but the packaged codec does not supply that shape. The next step is to compare the actual signatures and compatibility requirements, not immediately force the numerically highest version.

The following commands illustrate the kind of evidence to request; replace names with the project's real module and configuration:

./gradlew :app:dependencies --configuration paidReleaseRuntimeClasspath
./gradlew :app:dependencyInsight --dependency codec --configuration paidReleaseRuntimeClasspath

An exclusion or constraint can be appropriate after compatibility is understood. Otherwise it may merely replace a visible conflict with a runtime failure in a less frequently tested path.

8. How do you isolate a release-only crash?

Build a small comparison matrix that changes one major variable at a time. Keep the flavor, test data, backend environment, and device conditions aligned where possible. Preserve each artifact and its configuration so results remain attributable.

ExperimentResult in this hypothetical caseInterpretation
Paid release, optimization enabledFails opening reportBaseline reproduction
Paid release, optimization disabledPassesOptimization-related hypothesis strengthened
Free release, optimization enabledPassesPaid-specific path deserves attention
Paid release, targeted rulePassesValidate rule scope and regressions

Those observations still do not prove every detail of the cause. A change in configuration can alter packaging or execution in ways worth checking. Use the smallest reproduction to confirm the suspected dynamic access path.

Do not begin by deleting every cache. A clean build can help rule out stale outputs, but it does not explain why a reproducible artifact fails. Retain useful evidence before replacing the build environment.

9. What evidence must survive a release?

An optimized crash report may need the matching mapping information to recover meaningful names. Archive that information with the exact release identity, alongside source revision, dependency resolution information, build-tool versions, and artifact checksums where your release process supports them.

Imagine receiving two crash traces after a hotfix. If both are labeled only "latest," it becomes difficult to know which code or mapping belongs to either. A precise version and build identifier makes the investigation tractable.

In an interview, explain the chain from distributed artifact to reproducible inputs. A developer's workstation is not an adequate archive. Neither is a mapping file from a nearby build that happens to share a human-readable version label.

Be honest about limits: reproducing the same build configuration does not automatically reproduce every backend response, device behavior, or account condition. Record the environmental conditions needed to trigger the fault as well as the build facts.

10. How should you test the variant matrix?

Testing every conceivable combination equally can be expensive. Start from the variants actually shipped and the differences most likely to affect behavior: authentication configuration, paid features, manifests, native libraries, serialization, and optimization-sensitive entry points.

For the four-variant example, debug tests remain useful for fast feedback, but the shipped release combinations need meaningful smoke coverage. A report-opening test should exercise the same dynamic adapter path that failed, not merely launch the home screen.

Describe a risk-based proposal: validate shared logic broadly, verify flavor-specific resources and configuration, and run critical user journeys against optimized release artifacts. The exact allocation depends on the app's distribution model and supported devices.

If a library upgrade fixes the report crash, retain the regression test. Replacing a workaround is an opportunity to simplify rules only after the actual release path still works. A smaller keep file is not valuable if it silently removes behavior customers rely on.

11. What makes an interview answer convincing?

Practice answering this prompt: "Paid release crashes after login, but free debug works. What do you do first?" A useful response names the artifact mismatch, asks for the crash evidence, and proposes a controlled comparison before editing dependency versions.

Then explain a decision you would defer. For example, you would not force codec 2.0 until checking the adapter's compatibility requirements. That demonstrates restraint based on evidence rather than reluctance to act.

Finish with the prevention step that follows from the diagnosis: a targeted release test, a documented consumer rule, a dependency constraint with an explanation, or archived mapping data. Avoid reciting every build tool you have used.

The strongest preparation exercise is a short incident narrative with four parts: observed failure, discriminating experiment, supported cause, and regression protection. Make each part specific enough that another engineer could reproduce or challenge your conclusion.

Sources and Further Reading

Research checked September 24, 2026. Version numbers in the codec scenario are fictional; configuration syntax must match the project's pinned toolchain.