Android Code Review Checklist: Architecture to Release

A bug caught in code review costs minutes. The same bug caught after a Play Store rollout costs your crash-free user rate, your store ranking, and sometimes your weekend. Google reduces the visibility of any app that crosses a 1.09% user-perceived crash rate, which makes the review the last quiet checkpoint before that exposure goes public.

This Android code review checklist is the one our reviewers use when a client opens a pull request on an Android codebase. It codifies the code review best practices our team has refined across production app audits. A solid code review checklist starts with universal principles like layered architecture, naming, and test coverage. On Android, those principles meet a specific set of failure modes around lifecycle, threading, and the Play Store release path.

Before the Review Starts

A productive Android code review starts before anyone opens the code. The reviewer needs context, a clean build, and a manageable scope. We confirm three things up front:

  • Scope and goal. A bugfix, a feature branch, a refactor, and a release-candidate audit each deserve a different lens. We ask the author what changed, what risk it carries, and what to prioritize.
  • CI is green. Lint, Detekt, Ktlint, and unit tests must pass before a human touches the code.
  • The change set is reviewable. Cisco’s classic study on peer code review showed defect detection drops sharply above 400 lines per session. A 3,000-line review becomes a rubber stamp.

We also read the ticket and commit messages first. Half the noise on most reviews comes from reviewers asking “why?” when the answer was in Jira.

Android Code Review Checklist: Architecture to Release

Architecture Review

Architecture is the layer where small mistakes turn into multi-quarter rewrites. Most issues we find on client audits trace back to one of three patterns: business logic leaking into the UI, dependency injection bypassed, or a “ViewModel” that has quietly become a god object.

Layer Separation

Domain logic belongs in use cases or interactors. Activity, Fragment, and composable functions should stay free of business rules. Repositories handle data sources, and ViewModels coordinate state. An Activity that calls Retrofit directly always becomes a refactor candidate.

Dependency Injection

Hilt or Koin should wire the graph. Manual instantiation of dependencies inside production classes is a red flag, especially for anything that needs to be tested in isolation. Centralized DI also makes legacy code modernization cheaper later, because swapping implementations becomes a module-level change instead of a hunt across the codebase.

Modularization Red Flags

Feature modules should depend on :core and :domain. Cross-feature dependencies travel through a shared layer. Circular dependencies should fail the build. We also flag the architectural anti-patterns we see most often in AI-powered code reviews: composables that compile but break Compose’s stability contract, repositories that bypass the layered architecture, and ViewModels that absorb logic the use case layer should own.

Kotlin Idioms in Android Code

Kotlin gives reviewers tools Java did not. Most modern Android codebases mix the two languages, especially after a migration. For the Java side of a mixed codebase, our Java code review checklist covers the language-specific checks. What follows targets the Kotlin patterns that intersect with Android lifecycle and threading.

We look for these idioms:

  • val by default, var only where state genuinely mutates
  • Null safety without !!. Safe calls, the Elvis operator, and requireNotNull with a meaningful message are the defaults
  • Sealed classes or sealed interfaces for UI state. A data class with seven nullable fields is a refactor candidate
  • Scope functions used to clarify intent rather than to nest four levels deep
  • Collections chosen for the access pattern. A linear scan of a List inside a hot loop should be a Set

A tight example of the pattern we want to see:

sealed interface OrderListUiState {
    data object Loading : OrderListUiState
    data class Ready(val orders: List) : OrderListUiState
    data class Error(val message: String) : OrderListUiState
}

class OrderListViewModel(
    private val repository: OrderRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow(OrderListUiState.Loading)
    val uiState: StateFlow = _uiState.asStateFlow()

    fun refresh() {
        viewModelScope.launch {
            _uiState.value = runCatching { repository.getOrders() }
                .fold(
                    onSuccess = { OrderListUiState.Ready(it) },
                    onFailure = { OrderListUiState.Error(it.message.orEmpty()) }
                )
        }
    }
}

The legacy equivalent typically uses LiveData<Order>?, a nullable error string, and a !! on value. The reviewer’s job is to catch that pattern before it merges.

Jetpack Compose Review

Compose changed what a reviewer looks for. The XML and Fragment patterns are still relevant for older screens, but new code brings recomposition cost, state hoisting, and side-effect handling into focus.

For new Compose code, we check:

  • State is hoisted to the right level. A TextField should not own its own state if the parent screen needs it
  • No business logic inside @Composable functions. Composables describe UI given state. They do not call repositories or perform IO
  • LaunchedEffect, DisposableEffect, and SideEffect are used correctly. A coroutine launched in a composable body without LaunchedEffect leaks on the next recomposition
  • remember and derivedStateOf protect expensive calculations from recomputing
  • LazyColumn and LazyRow use stable keys. A LazyColumn without key = { it.id } reshuffles composables on every data change
  • Previews exist for loading, success, and error states

For XML-based screens, the rule is unchanged: ViewBinding instead of findViewById, layouts kept flat, no business logic inside onCreateView.

Async Work Review

Async bugs are the most expensive class of Android defect we encounter on audits. They are intermittent in QA, deterministic in production, and frequently invisible in logs. The reviewer’s job is to surface them at review time.

Things we look for:

  • No GlobalScope in production code. viewModelScope, lifecycleScope, or an injected scope tied to a Hilt component are the right choices
  • Dispatchers match the work and are injected so tests can substitute them
  • Cancellation honored. Long-running work uses cancellable suspending APIs or checks isActive in loops
  • No runBlocking on the main thread. Ever
  • StateFlow for UI state, SharedFlow for one-shot events, cold Flow for repository reads
  • Exceptions cross coroutine boundaries safely. CoroutineExceptionHandler and supervisorScope are deliberate choices the reviewer should see used on purpose

A reviewer who can read viewModelScope.launch(Dispatchers.IO) { repository.fetch() } and spot the four things wrong with it is worth keeping on every release.

Memory and Performance Review

Performance issues hide in small mistakes. Memory leaks hide in places that look correct in isolation. A senior reviewer reads code with a profiler in their head.

Lifecycle Leaks

Long-lived objects, like singletons, application-scoped repositories, or static fields, should never hold a reference to an Activity, Fragment, or View. Inner classes inside lifecycle owners are a common offender. Anything you register in a lifecycle callback also needs to be unregistered when that lifecycle ends.

ViewBinding Nulling

In Fragments, setting _binding = null in onDestroyView is mandatory. This single pattern prevents the most common memory leak in modern Android code. The reviewer should see it on every Fragment that uses ViewBinding, no exceptions.

We also check the rest of the performance layer in prose. Image loading should use a real library (Coil, Glide, Picasso) rather than a hand-rolled decoder that blocks the main thread. RecyclerView adapters should use DiffUtil or ListAdapter instead of notifyDataSetChanged(). LeakCanary belongs in every debug build, and any open leak it surfaces gets fixed before the change ships.

Security Review

Mobile security is one of those areas where a single oversight costs more than a year of careful work. The Verizon 2025 Mobile Security and breach reports found that 85% of organizations reported increasing attacks on mobile devices, and 88% of application breaches are powered by stolen credentials. Secret handling and permission scoping are non-negotiable parts of any code review checklist for Android.

Manifest and Permissions

android:exported should be set explicitly on every Activity, Service, Receiver, and Provider with an intent filter. Android 12 and later require it, and the safe default is false. Every <uses-permission> entry should map to a feature the app actually uses. networkSecurityConfig should not allow cleartext traffic in production builds.

Secrets and Data Storage

API keys, signing keys, OAuth client secrets, and Firebase server keys belong in CI secret stores. gradle.properties checked into Git is a leak path we surface often during code review services engagements. Sensitive runtime data uses EncryptedSharedPreferences for credentials, Android Keystore for cryptographic keys, and internal storage for app-private files. TLS 1.2 is the minimum.

R8 should be enabled for release builds with shrinking and obfuscation on. ProGuard rules need review for over-broad -keep directives, and debug-only code must be gated by build variant so it never reaches release.

Release Readiness

Some review concerns only apply to changes touching the release path. We give them a separate pass because the cost of a mistake is high and teams usually catch them late.

We verify the following:

  • minSdkVersion, targetSdkVersion, and compileSdkVersion align with current Play Store requirements. Google raises targetSdkVersion minimums annually, and missing the cutoff blocks new uploads
  • The release artifact is an Android App Bundle (.aab), the required upload format for new apps
  • R8 shrinks the bundle visibly in CI. A 20 MB jump on a small change deserves five minutes of investigation
  • Baseline profiles are regenerated when startup-critical code changes
  • Crashlytics, Firebase Performance Monitoring, and Play Console pre-launch reports are wired into the release variant
  • Strings are translated for every locale the app ships in. The lint rule for missing translations should fail release builds instead of emitting a warning the team learns to ignore

Static Analysis Tools

Automated tools catch the issues humans should not spend time on. Four belong in every Android CI pipeline. They overlap, they complement each other, none replaces the others. Code review guidelines work best when these tools handle the mechanical checks and reviewers focus on architecture and intent.

Tool
What it does
Where it shines
Tool

Android Lint

What it does

Native Android Gradle Plugin checker. Scans Kotlin, Java, XML, and resources for platform-level issues.

Where it shines

Permissions, deprecated APIs, layout problems, missing translations, manifest issues.

Tool

Detekt

What it does

Static analysis for Kotlin. Parses the Kotlin AST, so it understands coroutines, sealed classes, and modern language features.

Where it shines

Code smells, cyclomatic complexity, naming conventions, custom rule sets.

Tool

Ktlint

What it does

Opinionated formatter and style checker for Kotlin. Minimal configuration, sensible defaults.

Where it shines

Formatting consistency, import order, line length. Pairs with Detekt rather than replacing it.

Tool

R8

What it does

Code shrinker, optimizer, and obfuscator from Google. Replaces ProGuard.

Where it shines

Release-build size reduction, dead-code elimination, identifier obfuscation.

Pair these with Android Studio’s built-in inspections, and warnings surface inline on the review where reviewers actually see them.

Why a Fresh Set of Eyes Matters

Internal code review is a culture. External code review is a service. The checklist above works hardest when applied by someone outside the team that wrote the code, because internal reviewers normalize patterns the codebase has lived with for months.

Three moments where bringing in an external reviewer pays back the investment:

  • Before a major release, when the team is too close to the code to spot what they’ve grown used to.
  • After inheriting a codebase from a previous vendor, a freelancer, or an acquisition.
  • When AI-assisted code has been merged at speed and the team needs a hardening pass before production traffic hits.

If you have an Android codebase you want a senior set of eyes on, contact us and we’ll scope a review against this checklist.

See how we audited a cross-platform network mapping app pre-release and helped achieve 90% code maintainability

Please enter your business email isn′t a business email