Android Static Analysis in CI: Lint and Detekt Without Noise
Build a practical Android static-analysis pipeline with Android Lint and Detekt, clear ownership, baselines, variant-aware checks, and CI gates developers can trust.
Table of Contents12 sections

Static analysis works best when every tool has a clear job.
For an Android project, a practical starting point is Android Lint for Android-specific correctness and platform checks, plus Detekt for Kotlin code-quality rules. Run both through Gradle, make the same commands available locally and in CI, and treat new findings differently from historical debt.
The goal is not to collect the largest possible rule set. The goal is to catch useful problems early without training developers to ignore the output.
Why Use Both Android Lint and Detekt?
The tools overlap at the edges, but their strongest responsibilities are different.
Android Lint understands Android-specific APIs, resources, manifests, compatibility concerns, and framework conventions. Google recommends running lint explicitly in CI because lint is not automatically part of every normal build.
Detekt analyzes Kotlin source and provides configurable rules for complexity, maintainability, potential defects, and project-specific conventions. Its Gradle plugin also produces reports such as HTML, Markdown, Checkstyle XML, and SARIF.
That gives a useful division:
| Tool | Primary responsibility | Typical examples |
|---|---|---|
| Android Lint | Android platform correctness | API usage, resources, manifest issues, Compose checks |
| Detekt | Kotlin code quality | complexity, suspicious constructs, maintainability rules |
| Compiler and tests | Executable correctness | type errors, behavior, regressions |
Do not force one tool to replace the other simply to reduce the number of CI steps. Separate tools are reasonable when each catches a distinct class of problem.
Start With Android Lint as the Platform Gate
For a Gradle Android project, the basic command is intentionally boring:
./gradlew lint
Android’s documentation also supports variant-specific tasks such as:
./gradlew lintRelease
That distinction matters in applications with product flavors or build types. A check that passes for a default debug variant does not automatically prove that a release-specific manifest, resource, or dependency configuration is healthy.
If your project already uses multiple variants, connect the lint task to the variant that most closely represents what you ship. This is the same reason build variants deserve explicit architecture rather than accidental configuration. The RayLabs guide on Android product flavors goes deeper into keeping those variant boundaries manageable.
Compose projects get additional value from platform lint. The official Compose lint guidance documents checks that understand Compose-specific correctness rules. Keeping Android Lint current therefore matters even if a separate Kotlin analyzer is already present.
Add Detekt for Kotlin-Level Signals
Detekt becomes useful when the team wants a consistent Kotlin quality contract beyond Android framework checks.
A typical Gradle integration exposes a detekt task that can be run locally and in CI:
./gradlew detekt
The important decision is not the command. It is the rule policy behind it.
Start with rules that developers can explain during code review. Complexity, empty blocks, suspicious exception handling, unreachable patterns, and project-specific architectural conventions can provide useful signals. A rule that nobody understands or trusts becomes noise, even when it is technically correct.
Detekt also supports type-aware analysis. Its type resolution documentation explains that some checks need compiler type and symbol information to make more precise decisions. For Android projects, prefer the variant-aware Detekt tasks when a rule requires that deeper analysis instead of assuming the generic task sees the same information.
Do Not Turn on Every Rule at Once
A common static-analysis rollout fails in the first pull request.
The team enables a large ruleset, CI reports hundreds of findings, and the fastest route back to productivity becomes suppressing warnings. Within weeks, the quality gate exists but nobody treats it as meaningful.
A safer rollout has three stages.
First, run the tools in report-only mode and inspect the findings. Remove rules that do not fit the codebase or tune thresholds that clearly misrepresent normal project structure.
Second, separate existing debt from new regressions. Detekt supports baselines for existing findings, and Android Lint also has mechanisms for managing known issues. A baseline is useful when it creates a line in the sand: old debt is visible, but new violations cannot silently join it.
Third, fail CI only on the rules the team has agreed are actionable.
A baseline should shrink over time. If every new finding is immediately added to it, the baseline is functioning as a mute button rather than a migration tool.
Make CI Fail for the Right Reason
A good quality job is easy to understand from its log.
For example:
- name: Android static analysis
run: ./gradlew lintRelease detekt
For a larger project, separate jobs can be better because they make ownership and failures clearer:
android-lint -> Android and Compose correctness
detekt -> Kotlin quality policy
tests -> behavior
build -> packaging and integration
This is not merely cosmetic. When one broad quality command fails, developers have to discover which layer broke. Clear jobs shorten that diagnosis loop and make it easier to assign responsibility.
The same principle applies to coverage. In Android Code Coverage in CI Without Chasing a Meaningless Percentage, the useful gate is the one that explains what regression it is protecting. Static analysis should follow the same rule.
Keep Local and CI Commands Identical
A check that exists only in CI creates delayed feedback.
If CI runs lintRelease, developers should be able to run ./gradlew lintRelease before pushing. If CI runs Detekt with a repository configuration file, local execution should use that same file.
Avoid reimplementing rule thresholds in workflow YAML when Gradle or the tool configuration can own them. The repository should define the quality contract. CI should execute it.
That keeps three environments aligned:
IDE feedback
|
local Gradle task
|
CI Gradle task
The IDE can provide faster hints, but the Gradle task remains the reproducible authority.
Decide Which Findings Block a Pull Request
Not every warning deserves the same consequence.
A useful policy separates findings by risk:
| Finding | Suggested treatment |
|---|---|
| Clear correctness defect | Block |
| Security or dangerous API misuse | Block |
| Newly introduced high-confidence smell | Usually block |
| Style preference with automatic fix | Fix automatically or warn |
| Existing legacy debt | Baseline and reduce deliberately |
| Debatable complexity threshold | Review before enforcing |
This prevents the static-analysis system from confusing consistency with correctness.
The stricter a gate becomes, the more confidence developers need that its findings are actionable. False positives are expensive because they consume attention and encourage broad suppressions.
Be Careful With Generated Code and Build Variants
Android builds contain generated sources, resources, build variants, and framework glue. Static-analysis configuration should reflect those boundaries.
Do not exclude an entire package because one generated file is noisy. Prefer the narrowest exclusion that removes the artifact you do not own while keeping hand-written code visible.
Likewise, verify which variant a task actually analyzes. Android Lint documents variant-specific execution, and Detekt’s Gradle integration exposes Android-aware tasks. A CI command should intentionally cover the source set you expect, not merely whichever task name was easiest to copy.
This becomes especially important as a project grows into multiple modules. If static analysis takes too long, measure which tasks and modules dominate execution before splitting or parallelizing the pipeline. The architecture guidance in When to Modularize an Android App Without Overengineering applies here too: create boundaries to solve measured problems, not to satisfy an architecture diagram.
Preserve Reports for Debugging
Console output is useful for immediate failures, but reports are better for investigation.
Android Lint can produce HTML and XML reports. Detekt can produce several formats, including SARIF. Keep the format that fits the team’s workflow and preserve it as a CI artifact when a failed run would otherwise discard the evidence.
A useful failure experience answers three questions quickly:
- Which tool reported the issue?
- Which rule failed?
- Where can the developer inspect the full report?
If the answer requires scrolling through thousands of mixed log lines, the quality pipeline needs better boundaries.
A Practical Adoption Order
For a new or existing Android project, this sequence keeps the rollout controlled:
1. Run Android Lint and inspect current findings.
2. Fix high-confidence Android correctness issues.
3. Add the chosen lint task to CI.
4. Add Detekt with a small, understood Kotlin ruleset.
5. Baseline historical debt instead of mass-editing unrelated code.
6. Block only new, actionable findings.
7. Preserve reports when they help diagnosis.
8. Review suppressions and baselines during normal maintenance.
9. Expand rules only when the previous set has earned trust.
This order matters more than the exact tool count.
The Quality Stack Should Stay Boring
Static analysis is infrastructure. Its best outcome is not an impressive dashboard. It is a predictable feedback loop that quietly prevents avoidable defects and code-quality regressions.
Use Android Lint where Android context matters. Use Detekt where Kotlin structure and team conventions matter. Keep their responsibilities visible, make the commands reproducible, and introduce enforcement gradually.
If developers can predict why a check will fail before CI runs, the system is working.
If they routinely respond to failures by adding suppressions they do not understand, reduce the noise before adding another tool.
Continue Exploring
You Might Also Like

When to Modularize an Android App Without Overengineering
A practical guide to deciding when Android modules help, what boundaries to extract first, and how to avoid turning modularization into architecture overhead.

Android Notification Opens vs App Opens: Measure the Entry Point
A practical Android analytics pattern for separating notification-driven sessions from ordinary app launches without double-counting engagement.

Parallelize Independent Android Refreshes with Coroutines
Learn when Android repository calls can run concurrently, how structured concurrency changes failure behavior, and how to test parallel refreshes safely.