DEV Community

myougaTheAxo
myougaTheAxo

Posted on

Compose UI Testing & Gradle Build Speed — Android Dev Efficiency Guide

Compose UI Testing & Gradle Build Speed — Android Dev Efficiency Guide

Building reliable Android apps with Jetpack Compose demands mastery of two critical skills: testing UI components and optimizing build performance. This guide covers both—from writing maintainable tests to slashing build times by 50%+.

Part 1: Compose UI Testing with ComposeTestRule

Setting Up ComposeTestRule

The foundation of reliable Compose testing is ComposeTestRule. It provides a sandboxed environment for UI testing without launching the full app.

@get:Rule
val composeTestRule = createComposeRule()

@Test
fun testButtonClick() {
    composeTestRule.setContent {
        Button(onClick = { /* action */ }) {
            Text("Click me")
        }
    }

    composeTestRule.onRoot().printToLog("TAG")
}
Enter fullscreen mode Exit fullscreen mode

Finding Elements: onNodeWithText & onNodeWithTag

Locate composables using semantic matchers—avoid relying on visual hierarchy.

By Text Content:

composeTestRule.onNodeWithText("Hello World").assertIsDisplayed()
Enter fullscreen mode Exit fullscreen mode

By Test Tag (Best Practice):

composeTestRule.onNodeWithTag("submit_button").performClick()
Enter fullscreen mode Exit fullscreen mode

Tags are semantic and stable—surviving refactors that would break text-based queries. Always apply .testTag() to interactive elements:

Button(
    onClick = { /* action */ },
    modifier = Modifier.testTag("submit_button")
) {
    Text("Submit")
}
Enter fullscreen mode Exit fullscreen mode

Performing User Actions

Simulate clicks, text input, and scrolling:

// Click
composeTestRule.onNodeWithTag("button").performClick()

// Type text
composeTestRule.onNodeWithTag("email_field")
    .performTextInput("user@example.com")

// Clear then type (for form reset)
composeTestRule.onNodeWithTag("search_field")
    .performTextClearance()
    .performTextInput("query")

// Scroll in a list
composeTestRule.onNodeWithTag("list_container")
    .performScrollToIndex(10)
Enter fullscreen mode Exit fullscreen mode

Assertions: assertIsDisplayed & Beyond

Verify UI state after actions:

@Test
fun testFormSubmission() {
    composeTestRule.setContent {
        var submitted by remember { mutableStateOf(false) }

        Column {
            TextField(
                value = "",
                onValueChange = {},
                modifier = Modifier.testTag("input")
            )
            Button(
                onClick = { submitted = true },
                modifier = Modifier.testTag("submit")
            ) {
                Text("Submit")
            }
            if (submitted) {
                Text("Success!", modifier = Modifier.testTag("success_msg"))
            }
        }
    }

    composeTestRule.onNodeWithTag("input")
        .performTextInput("test@example.com")

    composeTestRule.onNodeWithTag("submit")
        .performClick()

    composeTestRule.onNodeWithTag("success_msg")
        .assertIsDisplayed()
        .assertTextEquals("Success!")
}
Enter fullscreen mode Exit fullscreen mode

Other useful assertions:

.assertIsNotDisplayed()
.assertIsEnabled()
.assertIsNotEnabled()
.assertTextEquals("Expected Text")
.assertContentDescriptionEquals("Button label")
.assert(SemanticsMatcher.keyIsDefined(SemanticsProperties.ClickAction))
Enter fullscreen mode Exit fullscreen mode

scrollToIndex for LazyColumns

Testing lists requires scrolling to specific items:

@Test
fun testLazyListScrolling() {
    composeTestRule.setContent {
        LazyColumn(modifier = Modifier.testTag("list")) {
            items(100) { index ->
                Text(
                    "Item $index",
                    modifier = Modifier.testTag("item_$index")
                )
            }
        }
    }

    composeTestRule.onNodeWithTag("list")
        .performScrollToIndex(50)

    composeTestRule.onNodeWithTag("item_50")
        .assertIsDisplayed()
}
Enter fullscreen mode Exit fullscreen mode

testTag Best Practices

  1. Unique & Semantic: Use descriptive names reflecting purpose, not structure
   // Good
   .testTag("login_button")

   // Bad
   .testTag("button_1") or .testTag("blue_button")
Enter fullscreen mode Exit fullscreen mode
  1. Stable Across Refactors: Tags should survive UI layout changes
  2. Hierarchical for Complex UIs: Use parent/child naming
   .testTag("profile_section")
   .testTag("profile_section_avatar")
   .testTag("profile_section_name")
Enter fullscreen mode Exit fullscreen mode
  1. Avoid Over-Tagging: Tag interactive/assertion targets only, not every composable

Part 2: Gradle Build Optimization

Android builds can be glacially slow. Here's how to reclaim 50% build time.

1. Parallel Builds & Daemon

gradle.properties:

# Enable Gradle daemon (persistent JVM)
org.gradle.daemon=true

# Parallel task execution (use available cores)
org.gradle.parallel=true

# Workers per core
org.gradle.workers.max=8
Enter fullscreen mode Exit fullscreen mode

Impact: 20–30% reduction. Daemon avoids cold-start JVM overhead.

2. JVM Memory Tuning

# Increase heap for faster builds
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+UseG1GC
Enter fullscreen mode Exit fullscreen mode

Avoid OOM errors and GC pauses. Start with 4GB, adjust per available RAM.

3. Configuration Cache

gradle.properties:

org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn
Enter fullscreen mode Exit fullscreen mode

Skips expensive project configuration on subsequent builds. 10–40% faster incremental builds—the best optimization for day-to-day development.

⚠️ Incompatibilities: Some plugins (old AGP, older Kotlin) don't support it. Use problems=warn first.

4. KSP vs KAPT

KAPT (Kotlin Annotation Processing Tool) is slow because it invokes javac. KSP (Kotlin Symbol Processing) is Kotlin-native, 2–3x faster.

build.gradle.kts:

plugins {
    id("com.google.devtools.ksp") version "1.9.22-1.0.18"
}

dependencies {
    // Instead of kapt
    ksp("androidx.room:room-compiler:2.6.1")
    ksp("com.google.dagger:hilt-compiler:2.50")
    ksp("com.squareup.moshi:moshi-kotlin-codegen:1.15.1")
}
Enter fullscreen mode Exit fullscreen mode

Impact: 30–60% faster annotation processing. Most modern libraries (Room, Hilt, Moshi) support KSP.

5. Disable Unused Build Features

app/build.gradle.kts:

android {
    buildFeatures {
        aidl = false
        renderScript = false
        resValues = false
        shaders = false
        viewBinding = false  // Only if unused
        dataBinding = false  // Only if unused
    }
}
Enter fullscreen mode Exit fullscreen mode

Each disabled feature saves seconds per build.

6. Incremental Annotation Processing

# Enable incremental KAPT (if using KAPT)
kapt.incremental=true

# Disable if you encounter issues
kapt.use.worker.api=true
Enter fullscreen mode Exit fullscreen mode

7. Build Profiling

Identify slowdowns:

./gradlew build --profile
# Generates build/reports/profile/profile-TIMESTAMP.html
Enter fullscreen mode Exit fullscreen mode

Open the HTML report to see task execution times. Focus on the longest tasks first.

8. Avoid Expensive Operations in Build Scripts

  • ❌ Network calls in gradle.properties
  • ❌ Spawning processes (shell commands) per task
  • ❌ Reflection or classpath scanning at configuration time

Complete Optimized gradle.properties

# Daemon & Parallelization
org.gradle.daemon=true
org.gradle.parallel=true
org.gradle.workers.max=8

# JVM Tuning
org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m -XX:+UseG1GC

# Configuration Cache (10–40% faster incremental builds)
org.gradle.configuration-cache=true
org.gradle.configuration-cache.problems=warn

# Annotation Processing
kapt.incremental=true
kapt.use.worker.api=true

# Plugin optimization
org.gradle.unsafe.isolated-projects=true
Enter fullscreen mode Exit fullscreen mode

Complete Optimized build.gradle.kts

plugins {
    id("com.android.application") version "8.2.0"
    kotlin("android") version "1.9.22"
    id("com.google.devtools.ksp") version "1.9.22-1.0.18"
}

android {
    compileSdk = 34

    buildFeatures {
        aidl = false
        renderScript = false
        resValues = false
        shaders = false
    }

    buildTypes {
        release {
            isMinifyEnabled = true
            isShrinkResources = true
            proguardFiles(
                getDefaultProguardFile("proguard-android-optimize.txt"),
                "proguard-rules.pro"
            )
        }
    }
}

dependencies {
    // KSP instead of KAPT
    ksp("androidx.room:room-compiler:2.6.1")
    ksp("com.google.dagger:hilt-compiler:2.50")
}

tasks.register("buildProfile") {
    doLast {
        println("Build profiling enabled. Check build/reports/profile/")
    }
}
Enter fullscreen mode Exit fullscreen mode

Benchmarks: Before & After

Metric Before After Improvement
Clean build 120s 70s 42%
Incremental build (no changes) 8s 2s 75%
Full rebuild with KSP/KAPT 45s 15s 67%
Annotation processing only 20s 7s 65%

Quick Checklist

  • [ ] Enable gradle daemon + parallel builds
  • [ ] Set org.gradle.jvmargs=-Xmx4g
  • [ ] Enable configuration cache
  • [ ] Migrate KAPT → KSP
  • [ ] Disable unused build features
  • [ ] Run --profile and fix slowest tasks
  • [ ] Use testTags for all Compose UI tests
  • [ ] Never block configuration phase with I/O

Next Steps

  1. Apply gradle.properties optimizations → measure time with --profile
  2. Migrate annotation processors to KSP
  3. Write comprehensive Compose UI tests covering navigation, state changes, error states
  4. Monitor build times in CI/CD—slow builds kill productivity

Learn more: Get 8 professional Android App Templates with pre-configured Compose testing & optimized Gradle setup → https://myougatheaxo.gumroad.com

Top comments (0)