Skip to content

PatilParas05/RecompositionGuard

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

24 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

πŸ” RecompositionGuard

API License Kotlin Compose

A lightweight Jetpack Compose debug library that automatically detects, visualizes, and logs unnecessary recompositions in real time β€” without needing to open Android Studio's Layout Inspector.

⚠️ Intended for debug builds only.


🎯 The Problem

Compose performance is silently killed by unnecessary recompositions. Detecting them manually is tedious and requires profiling tools. RecompositionGuard makes it automatic and always-on during development.


✨ Features

  • πŸ“Š Live floating overlay β€” shows recomposition count per composable, updated every 100ms
  • 🎨 Color-coded severity β€” 🟒 OK / 🟑 Moderate / πŸ”΄ Excessive
  • πŸ“ Logcat suggestions β€” tells you exactly why a composable is recomposing and how to fix it
  • βš™οΈ Configurable thresholds β€” set your own warn/error limits
  • πŸͺΆ Zero-overhead design β€” raw counts stored in plain HashMap, display state flushed via coroutine every 100ms (no recomposition cascade)
  • 🎯 Two tracking APIs β€” Modifier.trackRecomposition() or TrackRecomposition() composable

πŸ“Έ Preview

πŸ” RecompositionGuard
───────────────────────────────
RapidUnstableComposable [10x] πŸ”΄
RapidHotComposable      [10x] πŸ”΄
RapidColdComposable      [1x] 🟒

Logcat:

[πŸ”΄ EXCESSIVE] Composable: "RapidHotComposable" recomposed 10 time(s)
⚠️  [RapidHotComposable] recomposed 10 times. Possible causes:
   -> Unstable lambda - wrap with remember { }
   -> Data class missing @Stable or @Immutable annotation
   -> State read inside composition - hoist it up
   -> Inline function triggering parent recomposition
   -> Use derivedStateOf { } for computed state

πŸš€ Installation

Step 1 β€” Add JitPack to your root settings.gradle

dependencyResolutionManagement {
    repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
    repositories {
        mavenCentral()
        maven { url 'https://jitpack.io' }
    }
}

Step 2 β€” Add the dependency

dependencies {
    debugImplementation 'com.github.PatilParas05:RecompositionGuard:1.0.0'
}

Or in build.gradle.kts:

dependencies {
    debugImplementation("com.github.PatilParas05:RecompositionGuard:1.0.0")
}

πŸ’‘ Use debugImplementation so the library is never included in release builds.


πŸ› οΈ Usage

Step 1 β€” Install in MainActivity.onCreate()

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        // Install ONCE here β€” NOT inside setContent
        RecompositionGuard.install(
            ThresholdConfig(
                warnThreshold  = 3,    // 🟑 yellow after 3 recompositions
                errorThreshold = 8,    // πŸ”΄ red after 8 recompositions
                overlayEnabled = true,
                logsEnabled    = true,
                dashboardEnabled = true
            )
        )

        setContent {
            YourAppTheme {
                YourRootScreen()
            }
        }
    }
}

Step 2 β€” Add the dashboard overlay to your root composable

@Composable
fun YourRootScreen() {
    Box {
        YourContent()
        RecompositionDashboard() // floating overlay, top-right by default
    }
}

Step 3 β€” Track your composables

Option A β€” Modifier (recommended, attach to any composable):

@Composable
fun ProductCard(product: Product) {
    Text(
        text = product.name,
        modifier = Modifier.trackRecomposition("ProductCard")
    )
}

Option B β€” Composable function (use inside composable body):

@Composable
fun HomeScreen() {
    TrackRecomposition("HomeScreen")
    // ... rest of your UI
}

πŸ“‹ Full Example

class RapidViewModel : ViewModel() {
    private val _counter = MutableStateFlow(0)
    val counter: StateFlow<Int> = _counter

    fun increment() { _counter.value++ }
}

@Composable
fun RapidTestScreen(vm: RapidViewModel = viewModel()) {
    val counter by vm.counter.collectAsStateWithLifecycle()

    LaunchedEffect(Unit) {
        while (true) {
            delay(500)
            vm.increment()
        }
    }

    Box(Modifier.padding(16.dp)) {
        Column(Modifier.padding(16.dp)) {
            Text("Counter: $counter")

            // βœ… Recomposes every tick β€” will show increasing count
            HotComposable(count = counter)

            // βœ… Never changes β€” stays at [1x]
            ColdComposable()

            // βœ… Unstable String parameter β€” recomposes every tick
            UnstableComposable(value = counter.toString())
        }

        RecompositionDashboard(alignment = Alignment.Center)
    }
}

@Composable
fun HotComposable(count: Int) {
    Text(
        text = "πŸ”₯ Hot: $count",
        modifier = Modifier.trackRecomposition("HotComposable")
    )
}

@Composable
fun ColdComposable() {
    Text(
        text = "❄️ Cold: Static Content",
        modifier = Modifier.trackRecomposition("ColdComposable")
    )
}

@Composable
fun UnstableComposable(value: String) {
    Text(
        text = "⚠️ Unstable: $value",
        modifier = Modifier.trackRecomposition("UnstableComposable")
    )
}

βš™οΈ Configuration

ThresholdConfig(
    warnThreshold    = 5,     // recompositions before 🟑 warning (default: 5)
    errorThreshold   = 10,    // recompositions before πŸ”΄ error (default: 10)
    overlayEnabled   = true,  // show/hide the colored border on tracked composables
    logsEnabled      = true,  // enable/disable logcat output
    dashboardEnabled = true   // show/hide the floating dashboard panel
)

Dashboard position:

// Top-right (default)
RecompositionDashboard()

// Center
RecompositionDashboard(alignment = Alignment.Center)

// Bottom-start
RecompositionDashboard(alignment = Alignment.BottomStart)

// Custom flush interval (default 100ms)
RecompositionDashboard(flushIntervalMs = 500L)

🧠 How It Works

SideEffect fires (from trackRecomposition modifier or TrackRecomposition())
  β†’ track() increments plain HashMap (rawCounts)
  β†’ NO Compose state written β†’ NO recomposition cascade βœ…

Every 100ms (coroutine in RecompositionDashboard)
  β†’ flush() copies rawCounts β†’ SnapshotStateMap (data)
  β†’ Overlay recomposes once with updated counts βœ…

The two-map pattern is the core innovation β€” tracking itself never triggers extra recompositions.


πŸ“¦ API Reference

API Description
RecompositionGuard.install(config) Initialize β€” call once in onCreate before setContent
RecompositionGuard.reset() Reset all tracked counts
ThresholdConfig Configure warn/error thresholds and feature flags
Modifier.trackRecomposition("name") Track via modifier β€” attach to any composable
TrackRecomposition("name") Track via composable function β€” use inside body
RecompositionDashboard() Floating overlay showing live counts
RecompositionTracker.track("name") Low-level tracking β€” use inside SideEffect { }
RecompositionTracker.data SnapshotStateMap of all tracked composables
RecompositionTracker.getCount("name") Get current count for a specific composable

🚫 Common Mistake

The tracker must be in the same recomposition scope as the state being read:

// βœ… CORRECT β€” trackRecomposition is in the same scope as `count`
@Composable
fun HotComposable(count: Int) {
    Text(
        text = "Hot: $count",
        modifier = Modifier.trackRecomposition("HotComposable")
    )
}

// ❌ WRONG β€” SideEffect in a wrapper scope never fires when content recomposes
@Composable
fun GuardedComposable(name: String, content: @Composable () -> Unit) {
    SideEffect { RecompositionTracker.track(name) } // This won't work reliably
    Box { content() }
}

🀝 Contributing

Pull requests are welcome! For major changes, please open an issue first.


πŸ“„ License

MIT License β€” Copyright (c) 2026 Paras Patil

Made with ❀️ by Paras Patil

About

πŸ” A Jetpack Compose debug library that detects and visualizes unnecessary recompositions in real time. Detect, visualize & fix unnecessary Jetpack Compose recompositions with a live overlay and logcat suggestions.

Topics

Resources

License

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages