Back to .md Directory

šŸ” Pull Request Review Guide

Defines a 7-step framework for reviewing Android/Kotlin pull requests, covering correctness, completeness, compatibility, consistency, clarity, and edge cases.

May 2, 2026
0 downloads
1 views
ai workflow
View source

What this file does

Defines a 7-step framework for reviewing Android/Kotlin pull requests, covering correctness, completeness, compatibility, consistency, clarity, and edge cases.

When to use it

  • Establishing a team-wide code review process for an Android project
  • Onboarding new developers to review standards
  • Creating a checklist for PR authors to self-review before submission
  • Auditing existing review practices for gaps

Assumes this stack

AndroidKotlinJetpack ComposeHiltGradleRoom

šŸ” Pull Request Review Guide

Overview

Code review is one of the most important quality gates in software development. A well-conducted PR review catches bugs, improves code quality, shares knowledge, and maintains consistency across the codebase.

This guide provides a comprehensive framework for conducting effective pull request reviews in our Android/Kotlin codebase.

ā”Œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”
│                        PR REVIEW WORKFLOW                                │
ā”œā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”¤
│                                                                          │
│   PR Created → Context Check → Code Review → Testing → Feedback → Merge │
│       │              │              │           │          │         │   │
│       ā–¼              ā–¼              ā–¼           ā–¼          ā–¼         ā–¼   │
│    Author       Understand      Review     Verify    Approve/    CI/CD  │
│    Submits      the "Why"       Changes    Works     Request     Runs   │
│                                                      Changes            │
ā””ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”€ā”˜

The 7-Step PR Review Framework

Quick Reference Table

StepFocus AreaKey Questions
1. ContextPurpose & BackgroundWhy is this change needed?
2. CorrectnessTechnical AccuracyDoes it work correctly?
3. CompletenessFull ImplementationAre all affected areas updated?
4. CompatibilityBreaking ChangesDoes it break existing code?
5. ConsistencyCode StyleDoes it follow conventions?
6. ClarityReadabilityIs it understandable?
7. ConsiderationsEdge CasesWhat about performance, security?

Step 1: Context Analysis šŸ“‹

Goal: Understand the purpose before reading code.

Checklist

  • Read the PR title and description thoroughly
  • Understand the problem being solved
  • Review linked issues, tickets, or documentation
  • Check if this is a bug fix, feature, refactor, or dependency update
  • Verify the scope is appropriate (not too large, not too small)

Questions to Ask

āœ“ What problem does this PR solve?
āœ“ Why was this approach chosen over alternatives?
āœ“ Is there sufficient context for reviewers?
āœ“ Does the PR description explain the "why"?

Red Flags 🚩

  • PR with no description
  • Very large PRs (500+ lines without justification)
  • Scope creep (mixing unrelated changes)
  • Missing ticket/issue reference

Example: Good PR Description

## Summary
Add NDK version configuration for Android 15 compatibility

## Problem
Android 15 requires 16KB page size alignment for native libraries.
Without this change, the app will crash on Android 15+ devices.

## Solution
- Set NDK version to 29.0.14206865 in app/build.gradle.kts
- Add linker option for 16KB page alignment in CMakeLists.txt

## Testing
- Tested on Android 15 emulator
- Verified native library loads successfully
- All existing tests pass

## Related
- Fixes #123
- Docs: https://developer.android.com/guide/practices/page-sizes

Step 2: Correctness Analysis āœ…

Goal: Verify the implementation is technically correct.

Checklist

  • Logic is correct and handles expected use cases
  • Edge cases are handled appropriately
  • Error handling is present and correct
  • Null safety is properly managed (no unnecessary !!)
  • Threading is correct (main vs background thread)
  • Memory management is proper (no leaks)

Android/Kotlin Specific Checks

// āŒ BAD: Force unwrap can cause crashes
val user = repository.getUser()!!

// āœ… GOOD: Safe handling
val user = repository.getUser() ?: return

// āŒ BAD: Blocking main thread
fun loadData() {
    val data = networkCall() // Blocks UI!
}

// āœ… GOOD: Proper coroutine usage
suspend fun loadData() = withContext(Dispatchers.IO) {
    networkCall()
}

Common Correctness Issues

IssueExampleSolution
Race conditionsConcurrent state updatesUse Mutex or single-threaded dispatcher
Memory leaksHolding Activity referenceUse WeakReference or scoped coroutines
Incorrect scopeGlobalScope.launchUse viewModelScope or lifecycle-aware scope
State lossData lost on configuration changeUse ViewModel + SavedStateHandle

Step 3: Completeness Analysis 🧩

Goal: Ensure all necessary changes are included.

Checklist

  • All affected files are modified
  • Tests are added/updated for new functionality
  • Documentation is updated if needed
  • Migration scripts provided if needed (database changes)
  • Related modules updated consistently
  • No TODO comments left without tracking issue

Multi-Module Project Considerations

When changing a core module, check:
ā”œā”€ā”€ core/network    → Did API interfaces change?
ā”œā”€ā”€ core/database   → Did entities/DAOs change?
ā”œā”€ā”€ core/common     → Did shared utilities change?
ā”œā”€ā”€ features/*      → Are feature modules updated?
└── app             → Is app module configuration updated?

Example: Incomplete PR

// PR adds new field to API response but forgets:
// āŒ Missing: Database entity update
// āŒ Missing: Room migration
// āŒ Missing: Domain model update
// āŒ Missing: UI layer handling

data class RecipeDto(
    val id: String,
    val name: String,
    val calories: Int  // NEW FIELD - where else needs updating?
)

Step 4: Compatibility Analysis šŸ”„

Goal: Verify no breaking changes are introduced.

Checklist

  • Public APIs remain backward compatible
  • Database schema changes include migrations
  • Shared preferences changes handle old data
  • Feature flags used for gradual rollout (if needed)
  • minSdk/targetSdk implications considered

Breaking Change Categories

TypeImpactMitigation
API signature changeCompile-time errorDeprecate old, add new
Database schema changeRuntime crashRoom migration
Behavior changeLogic errorsFeature flag
Dependency updateVariousTest thoroughly

Database Migration Example

// āœ… GOOD: Proper migration
val MIGRATION_1_2 = object : Migration(1, 2) {
    override fun migrate(database: SupportSQLiteDatabase) {
        database.execSQL(
            "ALTER TABLE recipes ADD COLUMN calories INTEGER DEFAULT 0 NOT NULL"
        )
    }
}

// āŒ BAD: Destructive migration
fallbackToDestructiveMigration() // Loses user data!

Step 5: Consistency Analysis šŸ“

Goal: Ensure code follows project conventions.

Project Conventions Checklist

  • Naming conventions followed
  • Package structure maintained
  • Code formatting applied (ktlint/detekt)
  • Architectural patterns followed (MVI, Clean Architecture)
  • Dependency injection patterns consistent

Naming Conventions

// Files & Classes
HomeViewModel.kt        // ViewModel suffix
RecipeRepository.kt     // Interface, no Impl suffix
RecipeRepositoryImpl.kt // Implementation with Impl suffix
GetRecipesUseCase.kt    // UseCase suffix

// Functions
suspend fun fetchRecipes()     // Suspend for coroutines
fun validateEmail()            // Verb for actions
fun isEmailValid(): Boolean    // is/has for boolean returns

// State
data class HomeUiState(...)    // UiState suffix
sealed class HomeEvent(...)    // Event suffix for user actions

Architecture Consistency

// āœ… GOOD: Follows Clean Architecture
class HomeViewModel @Inject constructor(
    private val getRecipesUseCase: GetRecipesUseCase  // UseCase, not Repository
) : ViewModel()

// āŒ BAD: Bypasses domain layer
class HomeViewModel @Inject constructor(
    private val recipeRepository: RecipeRepository,   // Direct repo access
    private val recipeApi: RecipeApi                  // Direct API access!
) : ViewModel()

Step 6: Clarity Analysis šŸ“–

Goal: Code should be self-documenting and easy to understand.

Checklist

  • Variable/function names are descriptive
  • Complex logic has explanatory comments
  • Public APIs have KDoc documentation
  • Magic numbers are replaced with constants
  • Code intent is clear without reading implementation

Documentation Standards

/**
 * Validates user credentials and initiates login flow.
 *
 * @param email User's email address
 * @param password User's password (plain text, will be hashed)
 * @return [Result] containing [LoginResult] on success or [AuthError] on failure
 * @throws NetworkException if network is unavailable
 *
 * @see LoginResult
 * @see AuthError
 */
suspend fun login(email: String, password: String): Result<LoginResult>

Self-Documenting Code

// āŒ BAD: Magic numbers and unclear intent
if (password.length >= 8 && password.any { it.isDigit() }) { ... }

// āœ… GOOD: Clear intent through naming
private const val MIN_PASSWORD_LENGTH = 8

fun isPasswordStrong(password: String): Boolean {
    val hasMinimumLength = password.length >= MIN_PASSWORD_LENGTH
    val containsDigit = password.any { it.isDigit() }
    val containsUppercase = password.any { it.isUpperCase() }
    
    return hasMinimumLength && containsDigit && containsUppercase
}

Step 7: Considerations Analysis šŸ’­

Goal: Think about non-functional requirements and edge cases.

Performance Checklist

  • No unnecessary object allocations in loops
  • Database queries are optimized (indexed columns)
  • Images are properly sized/cached
  • Network calls are batched when possible
  • Compose recomposition is minimized

Security Checklist

  • Sensitive data not logged
  • API keys not hardcoded
  • User input is validated/sanitized
  • HTTPS enforced for network calls
  • Proper authentication token handling

Compose Performance

// āŒ BAD: Creates new lambda every recomposition
@Composable
fun RecipeList(recipes: List<Recipe>) {
    LazyColumn {
        items(recipes) { recipe ->
            RecipeItem(
                recipe = recipe,
                onClick = { viewModel.onRecipeClick(recipe.id) }  // New lambda!
            )
        }
    }
}

// āœ… GOOD: Stable reference
@Composable
fun RecipeList(
    recipes: List<Recipe>,
    onRecipeClick: (String) -> Unit  // Hoisted
) {
    LazyColumn {
        items(
            items = recipes,
            key = { it.id }  // Stable key!
        ) { recipe ->
            RecipeItem(
                recipe = recipe,
                onClick = { onRecipeClick(recipe.id) }
            )
        }
    }
}

Providing Feedback

The Right Tone

āœ… DO:
- "Consider using X because..."
- "Have you thought about...?"
- "This works, but we could improve it by..."
- "Great solution! One small suggestion..."

āŒ DON'T:
- "This is wrong."
- "Why didn't you do X?"
- "This doesn't make sense."
- "I would never do it this way."

Feedback Categories

Use prefixes to indicate severity:

PrefixMeaningRequired?
[Blocker]Must fix before mergeYes
[Major]Should fix, significant issueUsually
[Minor]Nice to fix, small improvementNo
[Nit]Nitpick, personal preferenceNo
[Question]Seeking clarificationN/A
[Praise]Calling out good work!N/A

Example Review Comments

[Blocker] This will cause a crash on null input:
`val user = users.first()` should be `users.firstOrNull()`

[Major] Consider using `remember { }` here to avoid creating 
a new object on every recomposition.

[Minor] Could extract this to a constant for clarity:
`const val MAX_RETRY_COUNT = 3`

[Nit] Personal preference, but I find `when` more readable 
than chained `if-else` here.

[Question] Why is this using `runBlocking` instead of 
`viewModelScope.launch`?

[Praise] Really clean solution! Love how you decomposed 
this into smaller functions. šŸ‘

PR Review Checklist Template

Copy this template for your reviews:

## PR Review: [PR Title]

### Context āœ“
- [ ] Purpose is clear
- [ ] Scope is appropriate
- [ ] Description is sufficient

### Correctness āœ“
- [ ] Logic is correct
- [ ] Edge cases handled
- [ ] Error handling present
- [ ] Thread safety correct

### Completeness āœ“
- [ ] All files updated
- [ ] Tests included
- [ ] Documentation updated
- [ ] Migrations provided (if needed)

### Compatibility āœ“
- [ ] No breaking changes
- [ ] Backward compatible
- [ ] Database migrations included

### Consistency āœ“
- [ ] Naming conventions
- [ ] Architecture patterns
- [ ] Code formatting

### Clarity āœ“
- [ ] Self-documenting code
- [ ] Comments for complexity
- [ ] Public API documented

### Considerations āœ“
- [ ] Performance OK
- [ ] Security OK
- [ ] Accessibility OK

### Verdict
- [ ] āœ… Approved
- [ ] šŸ”„ Request Changes
- [ ] šŸ’¬ Comment Only

Android-Specific Review Points

Gradle/Build Configuration

// Version Management: Check if using version catalog
// āŒ Hardcoded version
implementation("com.squareup.retrofit2:retrofit:2.9.0")

// āœ… Version catalog reference
implementation(libs.retrofit)

// NDK Version: Should be explicit for native modules
android {
    ndkVersion = libs.versions.ndk.get()  // Good
}

Jetpack Compose

CheckWhy
remember usagePrevents unnecessary recomposition
derivedStateOf for computed valuesEfficient state derivation
collectAsStateWithLifecycle()Lifecycle-aware collection
Stable/Immutable annotationsCompose compiler optimizations
LazyColumn keysEfficient diff/reorder

Hilt/Dependency Injection

// Check scoping is correct
@Singleton           // App-level singleton
@ActivityScoped      // Activity lifecycle
@ViewModelScoped     // ViewModel lifecycle
@ActivityRetainedScoped  // Survives configuration changes

// Check bindings are in correct modules
@Module
@InstallIn(SingletonComponent::class)  // Match scope with @InstallIn
abstract class NetworkModule { ... }

Common PR Anti-Patterns

1. The Monster PR

āŒ PR with 50+ files, 2000+ lines
→ Solution: Break into smaller, focused PRs

2. The Sneaky Refactor

āŒ Bug fix PR that also refactors unrelated code
→ Solution: Separate refactoring into its own PR

3. The Copy-Paste

āŒ Duplicated code instead of abstraction
→ Solution: Create shared utility or component

4. The Missing Tests

āŒ New feature with no test coverage
→ Solution: Add unit tests for business logic

5. The Silent Dependency Update

āŒ Updating dependencies without changelog review
→ Solution: Document breaking changes and migration

Automated Checks

Before manual review, ensure these pass:

# Linting
./gradlew ktlintCheck

# Static Analysis
./gradlew detekt

# Unit Tests
./gradlew testDebugUnitTest

# Build
./gradlew assembleDebug

CI Pipeline Requirements

CheckToolRequired
Lintktlintāœ…
Static AnalysisDetektāœ…
Unit TestsJUnitāœ…
BuildGradleāœ…
CoverageJaCoCoRecommended
UI TestsEspresso/ComposeOn critical paths

Review Turnaround Guidelines

PR SizeExpected Review Time
XS (< 50 lines)Same day
S (50-200 lines)1 business day
M (200-500 lines)2 business days
L (500+ lines)Consider splitting

Summary

Effective code review is a skill that improves with practice. Remember:

  1. Be thorough but efficient — Focus on what matters most
  2. Be kind but honest — Critique code, not people
  3. Be curious — Ask questions to understand intent
  4. Be educational — Share knowledge through reviews
  5. Be responsive — Review PRs promptly

"A good code review is a conversation, not an inspection."


Related Documentation

What's inside

7-step framework with checklists, code examples, feedback guidelines, a reusable review template, and Android-specific checks for Gradle, Compose, and Hilt.

Change this for your project

  • Replace eslamfaisal/android-compose-clean-architecture-sample with your own repository name
  • Replace libs.versions.ndk.get() with your project's NDK version reference
  • Replace app/build.gradle.kts with your module's build file path

Where it goes

Keep it in your repository where the agent or team that needs it will read it.

Worth borrowing

  • Prefix feedback comments with severity labels like [Blocker], [Major], [Minor] to clarify action required
  • Use a reusable checklist template that mirrors the review framework for consistency across PRs
  • Separate the review into distinct phases (context, correctness, completeness, etc.) to avoid skipping important checks

Related Documents