Skip to content

feature: implement blur engine#78

Merged
l2hyunwoo merged 30 commits intomainfrom
feature/implement-blur-engine
Dec 5, 2025
Merged

feature: implement blur engine#78
l2hyunwoo merged 30 commits intomainfrom
feature/implement-blur-engine

Conversation

@l2hyunwoo
Copy link
Copy Markdown
Collaborator

@l2hyunwoo l2hyunwoo commented Dec 4, 2025

🎯 Goal

Improve blur performance with platform-optimized GPU implementations and stabilize legacy CPU rendering

This PR introduces a major architectural upgrade to the blur engine:

  • iOS: Implements Skia ImageFilter.makeBlur() for GPU-accelerated blur via Metal backend (replacing placeholder bitmaps).
  • Android API 31+: Adopts RenderEffect for zero-copy, GPU-accelerated blur on the RenderThread.
  • Android API 30-: Significantly refactors the CPU-based legacy path to fix critical rendering bugs (invisible content, race conditions in lists) and improve animation smoothness.

Breaking Change: CloudyState.Success is now a sealed interface with Success.Applied and Success.Captured subtypes.

🛠 Implementation details

1. CloudyState Redesign

Separated the success state to distinguish between GPU (direct rendering) and CPU (bitmap capture) paths.

sealed interface CloudyState {
    data object Nothing : CloudyState
    data object Loading : CloudyState

    sealed interface Success : CloudyState {
        /** GPU blur applied in rendering pipeline (no bitmap extraction) */
        data object Applied : Success

        /** CPU blur completed with bitmap capture (legacy path) */
        data class Captured(val bitmap: PlatformBitmap) : Success
    }

    data class Error(val throwable: Throwable) : CloudyState
}

2. Platform Strategies

Platform Implementation Success Type Performance
Android 31+ RenderEffect (GPU) Success.Applied Fast: RenderThread, no bitmap copy
Android 30- Native C++ (CPU) Success.Captured Robust: Background thread, Fixed concurrency issues
iOS Skia ImageFilter (GPU) Success.Applied Fast: GPU accelerated via Metal

3. Critical Fixes for Android API 30- (CPU Path)

Recent investigations revealed that the legacy CPU path suffered from race conditions and timing issues.

  • Synchronous Bitmap Capture: Fixed an issue where async capture resulted in empty/transparent bitmaps (invisible content). Capture is now performed synchronously on the main thread before offloading blur to the background.
  • Concurrency Handling: Introduced pendingInvalidateRequest flag to handle race conditions in scrolling lists (e.g., LazyColumn). This ensures blur requests triggered during processing (e.g., by Coil loading completion) are not lost.
  • Animation Smoothing: The drawing logic now prioritizes the cached blurred bitmap (replacing original content) over overlaying, resolving darkening/flickering artifacts during animations.
  • Passive Waiting: Optimized for async image loaders by passively waiting for content to load instead of busy-looping with invalidateDraw().

✍️ Migration Guide & Examples

Migration for CloudyState users:

// Before
when (state) {
    is CloudyState.Success -> {
        val bitmap = state.bitmap // nullable
    }
}

// After
when (state) {
    is CloudyState.Success.Applied -> {
        // GPU blur applied directly. No bitmap available.
    }
    is CloudyState.Success.Captured -> {
        // Legacy CPU path. Bitmap is available.
        val bitmap = state.bitmap 
    }
}

Android API Branching Logic:

if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
    // RenderEffect GPU path
    Modifier.graphicsLayer {
        renderEffect = RenderEffect.createBlurEffect(sigma, sigma, Shader.TileMode.DECAL)
    }
} else {
    // Refactored Native C++ CPU path
    CloudyModifierNodeElement(...)
}

Key Decisions

  • No GPU→CPU fallback: GPU resource exhaustion is extremely rare; complexity vs benefit unfavorable.
  • No bitmap extraction for GPU paths: Avoids expensive GPU→CPU readback overhead.
  • Sigma conversion: sigma = radius / 2.0 standard formula applied consistently across all platforms.

Screenshots

Android (API 30-) Android (API 31+)
Screen_recording_20251205_145050.mp4
Screen_recording_20251205_145026.mp4
iOS
Simulator Screenshot - iPhone 16 Pro - 2025-12-05 at 03 41 54

Summary by CodeRabbit

  • New Features

    • Blur success now has two distinct outcomes (Applied vs Captured); bitmap is provided only for captured results
    • Disney-inspired color theme and refreshed demo app with interactive radius list/detail previews
    • Platform back-handler API added (Android active, iOS no-op)
  • Updates

    • Minimum Android API updated to 23+
    • Blur modifier selects GPU or CPU strategy per platform for smoother rendering
  • Documentation

    • Expanded docs and platform support table detailing states and bitmap availability
  • Tests

    • Added unit tests covering API-level rendering paths and state exhaustiveness

✏️ Tip: You can customize this high-level summary in your review settings.

- Introduce CloudyState.Success.Applied for GPU blur (no bitmap)
- Introduce CloudyState.Success.Captured for CPU blur (with bitmap)
- Update KDoc and expect functions
- Use RenderEffect.createBlurEffect() on Android 12+ (API 31+)
- Retain Native C++ fallback for API 30-
- Return CloudyState.Success.Applied for RenderEffect path
- Add Robolectric dependency for version branching tests
- Update unit tests for new state hierarchy
- Use Skia BlurEffect (backed by Metal) for high-performance blur
- Remove placeholder bitmap generation and CPU readback
- Return CloudyState.Success.Applied (no bitmap)
- Update iOS unit tests
- Update iOS app project configuration and bundle identifiers
- Verify RenderEffect condition on API 31+
- Verify Native path condition on API 30-
- Verify CloudyState subtypes behavior
- Update README with platform support details and migration guide
@l2hyunwoo l2hyunwoo self-assigned this Dec 4, 2025
@l2hyunwoo l2hyunwoo requested a review from skydoves as a code owner December 4, 2025 19:01
@l2hyunwoo l2hyunwoo added the enhancement New feature or request label Dec 4, 2025
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 4, 2025

Walkthrough

Split CloudyState.Success into two subtypes (Applied and Captured), introduced Android strategy-based rendering (RenderEffect for API ≥31, legacy bitmap fallback for <31), simplified iOS to GPU Skia blur, rewrote demo and theming, added tests and test utilities, and renamed frameworks to CloudyApp.

Changes

Cohort / File(s) Summary
Core API & Types
cloudy/src/commonMain/kotlin/com/skydoves/cloudy/CloudyState.kt, cloudy/api/cloudy.api, cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt
Replaced CloudyState.Success(bitmap: PlatformBitmap?) with a sealed interface Success having Applied and Captured(bitmap: PlatformBitmap) subtypes; expanded KDoc and updated generated API signatures.
Android Strategy & Modifier
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt, cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBlurStrategy.kt, cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt, cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt
Removed node-based modifier; added CloudyBlurStrategy interface and two strategies: CloudyRenderEffectStrategy (API 31+ GPU path, emits Success.Applied) and CloudyLegacyBlurStrategy (CPU bitmap capture and async blur, emits Success.Captured). Removed debugTag from modifier signature.
iOS Implementation
cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt
Replaced Core Image/node-based iOS implementation with Skia BlurEffect via graphicsLayer; signals Success.Applied via LaunchedEffect and removed bitmap processing.
Tests & Test Utilities
cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/CloudyModifierTest.kt, cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/CloudyStateTest.kt, cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/TestUtils.kt, cloudy/src/iosTest/kotlin/com/skydoves/cloudy/CloudyStateTest.kt
Added/updated tests for SDK branching, subtype coverage, exhaustiveness checks; added createMockPlatformBitmap test helper; updated iOS tests for new Success subtypes.
Demo App & Back Handling
app/src/commonMain/kotlin/demo/Main.kt, app/src/commonMain/kotlin/demo/BackHandler.kt, app/src/androidMain/kotlin/demo/BackHandler.android.kt, app/src/iosMain/kotlin/demo/BackHandler.ios.kt
Reworked demo to CloudyDemoApp with radius list/detail flow; added expect/actual PlatformBackHandler (Android uses BackHandler, iOS no-op).
Theme & Styling
app/src/commonMain/kotlin/demo/theme/Color.kt, app/src/commonMain/kotlin/demo/theme/PosterTheme.kt
Replaced previous palette with new Disney-inspired color tokens and added PosterTheme composable supporting light/dark palettes.
Build & Gradle
app/build.gradle.kts, cloudy/build.gradle.kts, gradle/libs.versions.toml
Renamed emitted framework to CloudyApp in KMP framework config; added androidx.core:core-ktx to androidMain and Robolectric (4.14.1) as a test dependency.
iOS App Project
iosApp/iosApp.xcodeproj/project.pbxproj, iosApp/iosApp/ContentView.swift, iosApp/iosApp/Info.plist
Updated embed script target and framework search paths to new xcode-frameworks output, changed linked framework name to CloudyApp, adjusted Swift import, and added CADisableMinimumFrameDurationOnPhone = true.
README / Documentation
README.md
Bumped API level to 23+, expanded docs with CloudyState Types, Platform Support table, state-driven usage examples, and clarified bitmap availability per state.

Sequence Diagram(s)

sequenceDiagram
    actor App
    participant Modifier as Modifier.cloudy
    participant PlatformCheck as SDK/Platform Check
    participant RenderStrategy as RenderEffectStrategy
    participant LegacyStrategy as LegacyBlurStrategy
    participant onState as onStateChanged

    App->>Modifier: apply blur(radius)
    Modifier->>PlatformCheck: determine platform / SDK

    alt SDK >= 31 or iOS GPU
        PlatformCheck->>RenderStrategy: select GPU strategy
        RenderStrategy->>RenderStrategy: compute sigma & apply RenderEffect/BlurEffect
        RenderStrategy->>onState: emit Success.Applied
    else Android <31 (legacy)
        PlatformCheck->>LegacyStrategy: select legacy strategy
        LegacyStrategy->>LegacyStrategy: capture bitmap & start async blur
        LegacyStrategy->>onState: emit Loading
        LegacyStrategy->>LegacyStrategy: finish -> emit Success.Captured(bitmap) or Error
    end

    onState-->>App: deliver state update
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~85 minutes

  • Review focus:
    • CloudyState API change and consumer impact (Applied vs Captured).
    • Android strategy selection and correct SDK checks.
    • Legacy strategy: threading, cancellation, bitmap lifecycle, memory cleanup.
    • iOS GPU path: correct BlurEffect usage and consistent state signaling.
    • Tests: Robolectric mocks and cross-platform test expectations.

Possibly related PRs

Suggested reviewers

  • skydoves

Poem

🐰 Hop-hop, I blurred the day away,

GPU gleam where pixels play,
Old phones hum with bitmap care,
iOS skias whisper in the air,
Applied or Captured—both delight!

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 62.50% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title 'feature: implement blur engine' is concise, clear, and accurately describes the main change—a major architectural upgrade to the blur engine with platform-optimized implementations.
Description check ✅ Passed The PR description comprehensively covers the goal, implementation details with code examples, platform strategy table, critical bug fixes, and migration guidance. All required template sections are addressed.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feature/implement-blur-engine

📜 Recent review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between b784b84 and a6f5251.

📒 Files selected for processing (1)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build
  • GitHub Check: Spotless check
  • GitHub Check: API check

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (5)
cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (1)

52-77: Consider guarding onStateChanged against repeated calls from recomposition

onStateChanged(CloudyState.Success.Applied) is invoked directly in the composable body, so it will run on every recomposition with the same radius/enabled inputs; if callers update Compose state in this callback, this can create unnecessary cycles. Consider moving this into a SideEffect or tracking the last emitted state so the callback only fires when the state actually changes.

cloudy/src/iosTest/kotlin/com/skydoves/cloudy/CloudyStateTest.kt (1)

66-90: Consider simplifying the pattern matching test.

The test verifies pattern matching works but uses mutable flags with empty branches. A more idiomatic approach would use exhaustive when expressions directly.

  @Test
  fun successTypeHierarchyAllowsPatternMatchingBothSubtypes() {
    val appliedState: CloudyState = CloudyState.Success.Applied
    val capturedState: CloudyState = CloudyState.Success.Captured(
      createTestPlatformBitmap(100, 100),
    )

-    var appliedMatched = false
-    var capturedMatched = false
-
-    when (appliedState) {
-      is CloudyState.Success.Applied -> appliedMatched = true
-      is CloudyState.Success.Captured -> {}
-      else -> {}
-    }
-
-    when (capturedState) {
-      is CloudyState.Success.Captured -> capturedMatched = true
-      is CloudyState.Success.Applied -> {}
-      else -> {}
-    }
-
-    assertTrue(appliedMatched)
-    assertTrue(capturedMatched)
+    val appliedMatched = when (appliedState) {
+      is CloudyState.Success.Applied -> true
+      else -> false
+    }
+
+    val capturedMatched = when (capturedState) {
+      is CloudyState.Success.Captured -> true
+      else -> false
+    }
+
+    assertTrue(appliedMatched)
+    assertTrue(capturedMatched)
  }
cloudy/src/commonMain/kotlin/com/skydoves/cloudy/CloudyState.kt (1)

59-60: Consider using @Immutable instead of @Stable for the sealed interface.

Both subtypes (Applied and Captured) are marked @Immutable. Since the sealed interface itself has no mutable state and all implementations are immutable, @Immutable would be more precise and consistent.

-  @Stable
+  @Immutable
   public sealed interface Success : CloudyState {
cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/CloudyModifierTest.kt (1)

204-211: Consider extracting duplicate createMockPlatformBitmap to shared test utilities.

This helper is duplicated in CloudyStateTest.kt (lines 147-155 per the relevant snippets). Consider extracting it to a shared test utilities file to reduce duplication and simplify maintenance.

cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt (1)

121-127: Redundant API level check inside graphicsLayer.

The Build.VERSION.SDK_INT >= Build.VERSION_CODES.S check on line 122 is redundant since cloudyWithRenderEffect is only called when this condition is already true (line 88). However, this may be intentional for lint suppression.

If kept for lint compliance, consider adding a comment explaining why:

return this.graphicsLayer {
  // Note: API check retained for lint compliance; this function is only called on API 31+
  if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 40d5655 and cf4e711.

📒 Files selected for processing (17)
  • README.md (2 hunks)
  • app/build.gradle.kts (2 hunks)
  • app/src/commonMain/kotlin/demo/theme/Color.kt (1 hunks)
  • app/src/commonMain/kotlin/demo/theme/PosterTheme.kt (1 hunks)
  • cloudy/api/cloudy.api (1 hunks)
  • cloudy/build.gradle.kts (1 hunks)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt (7 hunks)
  • cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/CloudyModifierTest.kt (1 hunks)
  • cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/CloudyStateTest.kt (3 hunks)
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt (1 hunks)
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/CloudyState.kt (1 hunks)
  • cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (2 hunks)
  • cloudy/src/iosTest/kotlin/com/skydoves/cloudy/CloudyStateTest.kt (2 hunks)
  • gradle/libs.versions.toml (2 hunks)
  • iosApp/iosApp.xcodeproj/project.pbxproj (5 hunks)
  • iosApp/iosApp/ContentView.swift (1 hunks)
  • iosApp/iosApp/Info.plist (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: l2hyunwoo
Repo: skydoves/Cloudy PR: 63
File: cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt:0-0
Timestamp: 2025-07-06T06:13:55.820Z
Learning: In the Cloudy project, the UIImage.asImageBitmap() function in cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt currently uses a placeholder implementation that generates synthetic gradients instead of extracting actual CGImage pixels. This is known technical debt that should be addressed in a future PR focused on pixel conversion optimization.
📚 Learning: 2025-07-06T06:13:55.820Z
Learnt from: l2hyunwoo
Repo: skydoves/Cloudy PR: 63
File: cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt:0-0
Timestamp: 2025-07-06T06:13:55.820Z
Learning: In the Cloudy project, the UIImage.asImageBitmap() function in cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt currently uses a placeholder implementation that generates synthetic gradients instead of extracting actual CGImage pixels. This is known technical debt that should be addressed in a future PR focused on pixel conversion optimization.

Applied to files:

  • README.md
  • cloudy/src/iosTest/kotlin/com/skydoves/cloudy/CloudyStateTest.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/CloudyState.kt
  • cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt
  • cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/CloudyModifierTest.kt
  • cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/CloudyStateTest.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt
  • cloudy/api/cloudy.api
🧬 Code graph analysis (3)
cloudy/src/iosTest/kotlin/com/skydoves/cloudy/CloudyStateTest.kt (1)
cloudy/src/iosTest/kotlin/com/skydoves/cloudy/TestUtils.kt (1)
  • createTestPlatformBitmap (29-30)
cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/CloudyModifierTest.kt (1)
cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/CloudyStateTest.kt (1)
  • createMockPlatformBitmap (148-156)
cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/CloudyStateTest.kt (1)
cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/CloudyModifierTest.kt (1)
  • createMockPlatformBitmap (204-211)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build
  • GitHub Check: API check
  • GitHub Check: Spotless check
🔇 Additional comments (26)
iosApp/iosApp/Info.plist (1)

49-50: Enabling higher frame rates on iPhone looks appropriate

Adding CADisableMinimumFrameDurationOnPhone = true is valid here and aligns well with the GPU blur work (smoother animations on ProMotion devices). Just be aware this opts the whole app into potentially higher frame rates, which can slightly increase power usage, but there’s no compatibility downside—older iOS versions will simply ignore the key.

app/src/commonMain/kotlin/demo/theme/PosterTheme.kt (2)

24-34: Dark theme palette wiring looks consistent

All dark palette slots are wired to the new Disney-themed tokens in a consistent way (dark background/surface, light on-colors, bright secondary). No functional or semantic issues spotted.


36-45: Light theme palette wiring is coherent with the new tokens

Light palette correctly mirrors the dark palette using the light background/surface tokens and appropriate on-colors; using backgroundDark as onSecondary should give good contrast on the gold secondary. No changes needed.

app/src/commonMain/kotlin/demo/theme/Color.kt (1)

20-39: New Disney-inspired color tokens are well-structured

The new color constants are valid, scoped appropriately (internal), and line up with how PosterTheme consumes them (background/surface/on-background + primary/secondary). This is a clean, forward-compatible tokenization of the palette.

iosApp/iosApp/ContentView.swift (1)

3-3: Import rename to CloudyApp is consistent with framework baseName changes

The updated import aligns with the new Kotlin/Native framework name and the Xcode link flags; no issues here.

cloudy/build.gradle.kts (1)

137-146: Robolectric dependency wiring for androidUnitTest looks correct

Adding implementation(libs.robolectric) to androidUnitTest.dependencies matches the existing pattern for other test libs and is appropriate for JVM-based Android unit tests.

Please confirm locally that ./gradlew :cloudy:androidUnitTest runs successfully with this Robolectric version alongside your AGP/Kotlin toolchain.

app/build.gradle.kts (1)

30-48: Framework baseName rename to CloudyApp is coherent across targets

Using baseName = "CloudyApp" for all Kotlin/Native frameworks matches the updated Xcode project and Swift import; this keeps the iOS integration consistent.

gradle/libs.versions.toml (1)

28-28: Robolectric version and library alias are wired correctly

The new robolectric version entry and corresponding library alias are consistent and correctly referenced from the Android unit test source set.

Please double-check that Robolectric 4.14.1 is compatible with your AGP and AndroidX test stack in this repo’s toolchain.

Also applies to: 77-77

iosApp/iosApp.xcodeproj/project.pbxproj (1)

139-156: Xcode embedding & linking now correctly target the CloudyApp framework

The embed script, framework search paths, and OTHER_LDFLAGS entries consistently point to the app module’s CloudyApp framework under app/build/xcode-frameworks, which matches the Gradle and Swift-side renames.

Also applies to: 291-309, 319-338

cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt (1)

21-76: Updated KDoc accurately reflects the new GPU/CPU blur behavior and state model

The added documentation clearly explains platform-specific implementations, the Success.Applied vs Success.Captured split, and radius→sigma handling, while keeping the public API stable; this should make the new state model much easier to consume.

cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (1)

21-45: iOS GPU blur path and state signaling are consistent with the new design

Using Skia BlurEffect through graphicsLayer (with sigma = radius / 2.0f) and always reporting CloudyState.Success.Applied (no bitmap) cleanly aligns iOS with the GPU-only success path described in the common KDoc and removes dependence on the previous bitmap extraction path.

Also applies to: 52-77

cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/CloudyStateTest.kt (1)

22-100: New tests give solid coverage of the CloudyState.Success hierarchy

The added assertions around Success.Applied singleton behavior, Success.Captured bitmap containment, subtype checks, pattern matching, and equality semantics collectively provide good safety nets for the new state design.

Also applies to: 111-125

cloudy/src/iosTest/kotlin/com/skydoves/cloudy/CloudyStateTest.kt (3)

25-44: LGTM! Good singleton verification tests.

The tests correctly verify that Nothing, Loading, and Success.Applied are singletons using referential equality (===).


46-64: LGTM! State subtype tests are well-structured.

Tests correctly verify that Captured holds a bitmap and both subtypes are instances of Success.


99-114: LGTM! Equality tests cover the expected scenarios.

Tests correctly verify that Captured states with the same bitmap are equal and those with different bitmaps are not.

README.md (3)

13-25: LGTM! Clear platform support documentation.

The table effectively communicates the blur implementation strategy per platform, aligning with the PR objectives.


114-139: LGTM! Code example demonstrates proper state handling.

The example clearly shows how to handle both Success.Applied (GPU) and Success.Captured (CPU with bitmap) states with appropriate comments for each platform behavior.


141-150: LGTM! State types table provides clear reference.

The table succinctly documents bitmap availability for each state, which is crucial for users migrating from the previous API.

cloudy/src/commonMain/kotlin/com/skydoves/cloudy/CloudyState.kt (1)

59-84: LGTM! Well-designed state hierarchy.

The sealed interface with Applied and Captured subtypes clearly models the GPU vs CPU blur paths. The documentation is thorough, explaining platform-specific behavior and when each subtype is used.

cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/CloudyModifierTest.kt (2)

44-112: LGTM! Comprehensive API version branching tests.

The tests effectively verify the RenderEffect condition logic across different API levels (S, S_V2, TIRAMISU, R, Q), ensuring the correct path selection.


182-202: LGTM! Exhaustive when expression test ensures type safety.

This test validates that all CloudyState variants can be matched exhaustively, which is valuable for detecting breaking changes if new states are added.

cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt (3)

87-97: LGTM! Clean API level branching.

The conditional routing to RenderEffect (API 31+) vs native CPU blur (API 30-) is clear and follows Android best practices for feature detection.


174-178: Inconsistent state for radius <= 0 in CPU path.

When radius <= 0, the CPU path emits Success.Applied but no blur is actually applied—the content is just drawn as-is. While this matches the GPU path behavior, emitting Applied (which semantically means "GPU blur applied") for a no-op case in the CPU path could be confusing.

Consider whether Nothing would be more semantically accurate for "no blur requested", or document this behavior explicitly.


182-216: LGTM! Proper coroutine and resource management.

The CPU blur path correctly:

  • Launches on Dispatchers.Main.immediate for UI updates
  • Handles bitmap caching and disposal
  • Releases graphics layer in finally block
  • Emits Success.Captured with the blurred bitmap
cloudy/api/cloudy.api (2)

32-41: Well-structured sealed hierarchy for GPU/CPU blur distinction.

The Success sealed interface with Applied (singleton for GPU path) and Captured (data class for CPU path with bitmap) is a clean design that clearly communicates which blur strategy was used and whether a bitmap is available. This properly addresses the iOS placeholder behavior mentioned in prior learnings.


43-53: Breaking API change: verify migration documentation is in place.

The rename from CloudyState.Success(bitmap) to CloudyState.Success.Captured(bitmap) is a binary-incompatible change. Consumers pattern-matching on the old Success type will need code updates.

Ensure the README or CHANGELOG includes migration guidance, particularly for the pattern matching change:

// Before
when (state) {
    is CloudyState.Success -> state.bitmap
}

// After  
when (state) {
    is CloudyState.Success.Captured -> state.bitmap
    is CloudyState.Success.Applied -> /* GPU path, no bitmap */
}

Comment thread cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt Outdated
@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 4, 2025

Note

Docstrings generation - SUCCESS
Generated docstrings for this pull request at #81

coderabbitai Bot added a commit that referenced this pull request Dec 4, 2025
Docstrings generation was requested by @l2hyunwoo.

* #78 (comment)

The following files were modified:

* `app/src/commonMain/kotlin/demo/theme/PosterTheme.kt`
* `cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt`
* `cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt`
* `cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt`
* `iosApp/iosApp/ContentView.swift`
coderabbitai Bot and others added 3 commits December 5, 2025 09:49
Docstrings generation was requested by @l2hyunwoo.

* #78 (comment)

The following files were modified:

* `app/src/commonMain/kotlin/demo/theme/PosterTheme.kt`
* `cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt`
* `cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt`
* `cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt`
* `iosApp/iosApp/ContentView.swift`

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (1)
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt (1)

123-146: Previous review feedback not addressed.

The side effect issue with onStateChanged.invoke() being called directly during composition (lines 129 and 137) was already flagged in a previous review. This should be wrapped in SideEffect or LaunchedEffect to prevent execution on every recomposition.

🧹 Nitpick comments (2)
app/src/commonMain/kotlin/demo/theme/PosterTheme.kt (1)

59-79: PosterTheme implementation looks idiomatic; consider visibility intent

The PosterTheme composable correctly switches palettes and typography based on darkTheme and delegates to MaterialTheme in an idiomatic way. If you ever plan to reuse this theme entry point from outside the demo module, you might want to relax internal; otherwise this scope is fine.

cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (1)

25-59: Consolidate duplicate KDoc blocks.

Two KDoc blocks document the same function with overlapping content. The first block (lines 25-45) and second block (lines 46-59) both describe the iOS blur implementation, sigma conversion, and state behavior. Merge them into a single, comprehensive documentation block to improve maintainability.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between cf4e711 and bb457d7.

📒 Files selected for processing (5)
  • app/src/commonMain/kotlin/demo/theme/PosterTheme.kt (2 hunks)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt (7 hunks)
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt (1 hunks)
  • cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (2 hunks)
  • iosApp/iosApp/ContentView.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt
  • iosApp/iosApp/ContentView.swift
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: l2hyunwoo
Repo: skydoves/Cloudy PR: 63
File: cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt:0-0
Timestamp: 2025-07-06T06:13:55.820Z
Learning: In the Cloudy project, the UIImage.asImageBitmap() function in cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt currently uses a placeholder implementation that generates synthetic gradients instead of extracting actual CGImage pixels. This is known technical debt that should be addressed in a future PR focused on pixel conversion optimization.
📚 Learning: 2025-07-06T06:13:55.820Z
Learnt from: l2hyunwoo
Repo: skydoves/Cloudy PR: 63
File: cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt:0-0
Timestamp: 2025-07-06T06:13:55.820Z
Learning: In the Cloudy project, the UIImage.asImageBitmap() function in cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt currently uses a placeholder implementation that generates synthetic gradients instead of extracting actual CGImage pixels. This is known technical debt that should be addressed in a future PR focused on pixel conversion optimization.

Applied to files:

  • cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Spotless check
  • GitHub Check: build
  • GitHub Check: API check
🔇 Additional comments (4)
app/src/commonMain/kotlin/demo/theme/PosterTheme.kt (2)

24-46: Updated color palettes are wired cleanly to the new token set

Dark/light palettes use the new background/surface and disney* tokens consistently, and map on* colors in a coherent way for MaterialTheme usage. No functional issues spotted here.


48-57: KDoc for PosterTheme is clear and sufficient

The documentation accurately describes behavior, parameters, and defaulting to isSystemInDarkTheme(), which should make usage straightforward.

cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt (2)

210-214: Consider side effect implications in draw phase.

While onStateChanged is called in the draw phase (not during composition), repeated draw calls will trigger the callback multiple times for radius <= 0. Consider whether this is intentional or if you need to gate the callback to fire only once per radius change.


245-246: LGTM: Correct state handling for CPU path.

The implementation correctly uses CloudyState.Success.Captured(blurredBitmap) for the CPU-based blur path, aligning with the PR's redesign of CloudyState into distinct GPU (Applied) and CPU (Captured) subtypes.

Comment thread cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt Outdated
Updates README to reflect the new GPU/CPU blur implementations and CloudyState types.
- Refactor `cloudy` modifier to delegate to `CloudyRenderEffectStrategy` or `CloudyLegacyBlurStrategy` based on API level.
- Add `debugTag` parameter to `cloudy` modifier for easier debugging.
- Implement `CloudyRenderEffectStrategy` using RenderEffect (API 31+).
- Implement `CloudyLegacyBlurStrategy` with optimized bitmap capture and iterative blur (API < 31).
- Add support for `GraphicsLayer` capture in `androidMain` source set.
- Update demo app with new API and UI improvements.
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 9

🧹 Nitpick comments (6)
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (1)

44-56: Unused debugTag parameter.

The debugTag parameter is accepted but never used in this strategy. Consider either adding logging support or removing the parameter if it's not needed for the legacy path.

   @Composable
   override fun apply(
     modifier: Modifier,
     radius: Int,
     onStateChanged: (CloudyState) -> Unit,
     debugTag: String,
   ): Modifier = modifier.then(
     CloudyModifierNodeElement(
       radius = radius,
       onStateChanged = onStateChanged,
+      debugTag = debugTag,
     ),
   )

If logging is added later, pass it through to the node element.

cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt (1)

66-72: Missing @param documentation for debugTag.

The new debugTag parameter at line 92 is not documented in the KDoc block. Add documentation to describe its purpose.

Add to the @param section:

 * @param debugTag Optional identifier for debugging and logging purposes.
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt (1)

37-37: Unused debugTag parameter.

Same as the legacy strategy, debugTag is accepted but not used. Consider adding logging support or documenting it as reserved for future use.

cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (1)

66-66: Unused debugTag parameter.

The debugTag parameter is added for API consistency with the common signature but is not used in the iOS implementation. Consider suppressing the warning or adding a comment explaining it's reserved for future use / platform parity.

 public actual fun Modifier.cloudy(
   @IntRange(from = 0) radius: Int,
   enabled: Boolean,
   onStateChanged: (CloudyState) -> Unit,
+  @Suppress("UNUSED_PARAMETER") // Reserved for debugging; unused on iOS
   debugTag: String,
 ): Modifier {
androidMain/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt (2)

123-135: Broad exception handling may hide unrelated failures.

Catching Throwable (line 125) is overly broad and could swallow OutOfMemoryError, StackOverflowError, or other critical errors that shouldn't trigger the fallback logic.

Consider catching a more specific exception type:

                     try {
                         GraphicsLayerV23(ownerView, ownerId)
-                    } catch (_: Throwable) {
+                    } catch (_: Exception) {
                         // If we ever failed to create an instance of the RenderNode stub
                         // based GraphicsLayer, always fallback to creation of View based layers

Note: If this is directly copied from AOSP, you may want to keep parity with upstream; in that case, add a comment noting the intentional broad catch.


139-140: Redundant local variable.

The local layer variable is assigned and immediately returned.

-            val layer = GraphicsLayer(layerImpl)
-            return layer
+            return GraphicsLayer(layerImpl)
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5237fc3 and f9046d5.

📒 Files selected for processing (11)
  • androidMain/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt (1 hunks)
  • androidMain/androidx/compose/ui/graphics/layer/GraphicsLayerV29.android.kt (1 hunks)
  • app/src/commonMain/kotlin/demo/Main.kt (2 hunks)
  • cloudy/api/cloudy.api (1 hunks)
  • cloudy/build.gradle.kts (2 hunks)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt (1 hunks)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBlurStrategy.kt (1 hunks)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (1 hunks)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt (1 hunks)
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt (1 hunks)
  • cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • cloudy/build.gradle.kts
  • app/src/commonMain/kotlin/demo/Main.kt
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: l2hyunwoo
Repo: skydoves/Cloudy PR: 63
File: cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt:0-0
Timestamp: 2025-07-06T06:13:55.820Z
Learning: In the Cloudy project, the UIImage.asImageBitmap() function in cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt currently uses a placeholder implementation that generates synthetic gradients instead of extracting actual CGImage pixels. This is known technical debt that should be addressed in a future PR focused on pixel conversion optimization.
📚 Learning: 2025-07-06T06:13:55.820Z
Learnt from: l2hyunwoo
Repo: skydoves/Cloudy PR: 63
File: cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt:0-0
Timestamp: 2025-07-06T06:13:55.820Z
Learning: In the Cloudy project, the UIImage.asImageBitmap() function in cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt currently uses a placeholder implementation that generates synthetic gradients instead of extracting actual CGImage pixels. This is known technical debt that should be addressed in a future PR focused on pixel conversion optimization.

Applied to files:

  • cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBlurStrategy.kt
  • cloudy/api/cloudy.api
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: Spotless check
  • GitHub Check: build
  • GitHub Check: API check
🔇 Additional comments (9)
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBlurStrategy.kt (1)

27-44: Well-designed strategy interface.

The interface cleanly abstracts the blur implementation details, enabling runtime strategy selection based on API level. The @Composable annotation on apply is appropriate since implementations will use Compose-specific APIs.

androidMain/androidx/compose/ui/graphics/layer/GraphicsLayerV29.android.kt (2)

1-50: Verify AOSP code attribution and licensing.

This file has a different copyright header ("The Android Open Source Project") compared to other Cloudy files ("skydoves"). If this is adapted from Jetpack Compose sources, ensure proper attribution and verify that incorporating it complies with the Apache 2.0 license terms (which it should, but the origin should be documented).

Consider adding a comment indicating the source, e.g.:

/**
 * GraphicsLayer implementation for Android Q+ that uses the public RenderNode API.
 * 
 * Adapted from AndroidX Compose UI graphics layer implementation.
 */

235-257: LGTM - Recording implementation.

The record() method correctly wraps the RenderNode recording with proper try-finally to ensure endRecording() is always called. The CanvasDrawScope configuration is properly set up.

cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (2)

74-77: State signaling via LaunchedEffect is correct.

Using LaunchedEffect(radius) properly scopes the onStateChanged callback to avoid side effects during composition. This correctly addresses the previous review feedback about composition-time side effects.


83-94: GPU blur implementation looks good.

The sigma conversion (radius / 2.0f) and BlurEffect application via graphicsLayer are correctly implemented. The use of TileMode.Clamp for edge treatment is appropriate.

cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt (2)

43-45: Inspection mode handling is appropriate.

Using the simple blur modifier for preview/inspection mode is a pragmatic fallback that ensures tooling support without full strategy initialization.


47-58: Clean strategy pattern implementation.

The API-level branching logic is well-structured. Using Build.VERSION_CODES.S (API 31) as the threshold correctly delegates to CloudyRenderEffectStrategy for GPU-accelerated blur on newer devices and CloudyLegacyBlurStrategy for bitmap-based blur on API 30 and below. Both strategies properly implement the CloudyBlurStrategy interface with the expected @Composable override fun apply signature.

androidMain/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt (1)

17-17: Vendoring AOSP code into androidx.compose.ui.graphics package.

This file is placed under the androidx.compose.ui.graphics package, which is typically owned by Jetpack Compose. Vendoring framework code can lead to maintenance burden and potential conflicts with the actual library.

Please clarify:

  1. Is this necessary because the required APIs aren't exposed publicly?
  2. Consider adding a comment explaining why this is vendored and from which Compose version.
  3. If this is for internal testing or compatibility, consider an internal package namespace to avoid confusion.
cloudy/api/cloudy.api (1)

32-53: API surface correctly reflects the CloudyState.Success redesign.

The breaking change from CloudyState.Success as a data class to a sealed interface with Applied and Captured subtypes is properly reflected in the API file. This aligns with the PR objectives for distinguishing GPU blur (no bitmap) from CPU blur (with bitmap).

Consumers will need to update pattern matching from:

// Before
is CloudyState.Success -> state.bitmap

// After  
is CloudyState.Success.Captured -> state.bitmap
is CloudyState.Success.Applied -> // no bitmap available

Comment on lines +170 to +172
internal companion object {
var isRenderNodeCompatible: Boolean = true
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Mutable companion object state without synchronization.

isRenderNodeCompatible is a mutable var in a companion object that can be modified from createGraphicsLayer(). While createGraphicsLayer uses synchronized(lock), the companion object is shared across all AndroidGraphicsContext instances, and lock is an instance field—not a class-level lock.

If multiple AndroidGraphicsContext instances exist, concurrent access to isRenderNodeCompatible is unsynchronized:

     internal companion object {
-        var isRenderNodeCompatible: Boolean = true
+        @Volatile
+        var isRenderNodeCompatible: Boolean = true
     }

Alternatively, use AtomicBoolean or a class-level lock object for the companion.

🤖 Prompt for AI Agents
In androidMain/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
around lines 170 to 172, the companion object exposes a mutable var
isRenderNodeCompatible that can be written from instance-level synchronized code
causing race conditions; replace the mutable companion var with a thread-safe
alternative (preferred: java.util.concurrent.atomic.AtomicBoolean) or use a
class-level lock in the companion. Concretely, change isRenderNodeCompatible to
an AtomicBoolean and update all reads/writes to use get()/set()/compareAndSet(),
or declare a private @JvmStatic final lock object inside the companion and
synchronize on that lock for all accesses to isRenderNodeCompatible so all
instances marshal access consistently.

Comment thread cloudy/api/cloudy.api Outdated

public final class com/skydoves/cloudy/Cloudy_androidKt {
public static final fun cloudy (Landroidx/compose/ui/Modifier;IZLkotlin/jvm/functions/Function1;Landroidx/compose/runtime/Composer;II)Landroidx/compose/ui/Modifier;
public static final fun cloudy (Landroidx/compose/ui/Modifier;IZLkotlin/jvm/functions/Function1;Ljava/lang/String;Landroidx/compose/runtime/Composer;II)Landroidx/compose/ui/Modifier;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

rg -n "fun Modifier\.cloudy" --type=kotlin -A 10

Repository: skydoves/Cloudy

Length of output: 2681


debugTag parameter has a default value in the expect declaration but is missing in platform-specific implementations.

The common expect declaration (Cloudy.kt:92) correctly includes debugTag: String = "", confirming source-level compatibility. However, the actual implementations in Cloudy.android.kt:35 and Cloudy.ios.kt:66 are missing default values for this parameter. This creates a platform-specific incompatibility where callers using the default value on the common API will fail at runtime on Android and iOS platforms.

🤖 Prompt for AI Agents
In cloudy/api/cloudy.api around line 56 (and corresponding implementations
Cloudy.android.kt:35 and Cloudy.ios.kt:66), the platform-specific function
signatures are missing the default value for the debugTag parameter; update the
Android and iOS implementation signatures to declare debugTag: String = "" to
match the common expect declaration so callers using the default compile and run
correctly; adjust any generated metadata/ABI if needed and rebuild to ensure the
default propagates to platform binaries.

Comment on lines +74 to +78
override fun update(node: CloudyModifierNode) {
node.onUpdate()
node.updateRadius(radius)
}
}
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

onStateChanged callback not updated on recomposition.

When the element is updated, only radius is propagated to the node. If the onStateChanged callback changes between compositions, the node will continue using the stale callback.

   override fun update(node: CloudyModifierNode) {
     node.onUpdate()
     node.updateRadius(radius)
+    node.updateOnStateChanged(onStateChanged)
   }

Add an updateOnStateChanged method to CloudyModifierNode:

fun updateOnStateChanged(callback: (CloudyState) -> Unit) {
  onStateChanged = callback
}

This requires changing onStateChanged from private val to private var.

Comment thread cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt Outdated
Comment thread cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt
- Introduce pendingInvalidateRequest flag in CloudyModifierNode to handle concurrent draw requests efficiently, preventing blur omissions in LazyColumn scenarios.
- Modify drawing logic to prioritize cached blurred bitmaps (replacing original content) to improve animated blur smoothness and resolve flickering/darkening.
- Adjust updateRadius and onUpdate to correctly manage the new pendingInvalidateRequest state.
- Ensure invalidateDraw() is triggered for missed requests after blur processing completes.
@l2hyunwoo
Copy link
Copy Markdown
Collaborator Author

@coderabbitai generate docstrings

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 5, 2025

✅ Actions performed

Initiated docstring generation; will generate only if new commits exist.

coderabbitai Bot added a commit that referenced this pull request Dec 5, 2025
Docstrings generation was requested by @l2hyunwoo.

* #78 (comment)

The following files were modified:

* `androidMain/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt`
* `androidMain/androidx/compose/ui/graphics/layer/GraphicsLayerV29.android.kt`
* `app/src/androidMain/kotlin/demo/BackHandler.android.kt`
* `app/src/commonMain/kotlin/demo/BackHandler.kt`
* `app/src/commonMain/kotlin/demo/Main.kt`
* `app/src/iosMain/kotlin/demo/BackHandler.ios.kt`
* `cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt`
* `cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBlurStrategy.kt`
* `cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt`
* `cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt`
* `cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/TestUtils.kt`
* `cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt`
* `cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt`
* `iosApp/iosApp/ContentView.swift`
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (3)
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (3)

69-78: onStateChanged callback not updated on recomposition (stale callback).

CloudyModifierNodeElement.update only forwards radius, so if the caller passes a new onStateChanged lambda on recomposition, the node will keep invoking the old one.

You can fix this by adding an update function on the node and calling it from update:

override fun update(node: CloudyModifierNode) {
  node.onUpdate()
  node.updateRadius(radius)
+ node.updateOnStateChanged(onStateChanged)
}

And in CloudyModifierNode, switch to a mutable callback and provide the updater (example):

private class CloudyModifierNode(
  radius: Int = 10,
  initialOnStateChanged: (CloudyState) -> Unit = {},
) : Modifier.Node(), DrawModifierNode {

  private var onStateChanged: (CloudyState) -> Unit = initialOnStateChanged

  fun updateOnStateChanged(callback: (CloudyState) -> Unit) {
    onStateChanged = callback
  }

  // ...
}

153-227: Handle bitmap capture failures and cancellation more robustly (avoid leaks & spurious errors).

Two issues in the blur coroutine:

  1. Graphics layer leak & silent failure on capture error (duplicate of prior review).
    In:

    val capturedBitmap: Bitmap = try {
      graphicsLayer.toImageBitmap().asAndroidBitmap()
    } catch (_: Exception) {
      return@launch
    }

    if toImageBitmap() throws, you return@launch without releasing graphicsLayer and without emitting an error state. The outer catch/finally won’t release it because no exception is propagated.

    Suggested fix:

    val capturedBitmap: Bitmap = try {
      graphicsLayer.toImageBitmap().asAndroidBitmap()
  • } catch (_: Exception) {
  • return@launch
  • }
  • } catch (e: Exception) {
  • if (!graphicsLayer.isReleased) {
  •  graphicsContext.releaseGraphicsLayer(graphicsLayer)
    
  • }
  • onStateChanged.invoke(
  •  CloudyState.Error(RuntimeException("Failed to capture bitmap", e))
    
  • )
  • return@launch
  • }
    
    
  1. Cancellation reported as Error.
    In the outer catch (e: Exception), a blurJob?.cancel() will surface as CloudyState.Error(e), even though cancellation is part of normal radius/content change flow. Consider treating CancellationException as non-error:

    } catch (e: Exception) {
      if (!graphicsLayer.isReleased) {
        graphicsContext.releaseGraphicsLayer(graphicsLayer)
      }
      if (e is kotlinx.coroutines.CancellationException) {
        return@launch // normal cancellation; no error state
      }
      onStateChanged.invoke(CloudyState.Error(e))
    }

    (Add the appropriate CancellationException import.)


234-248: Guard against division by zero in isTransparentBitmap.

When grid == 1, (grid - 1) becomes 0, so the row.toFloat() / (grid - 1) and col.toFloat() / (grid - 1) terms will divide by zero. While default grid = 4 is safe, this helper is more robust with an explicit guard.

One simple fix is to clamp the denominator:

  private fun isTransparentBitmap(bitmap: Bitmap, grid: Int = 4): Boolean {
     if (bitmap.width == 0 || bitmap.height == 0) return true
     val maxX = bitmap.width - 1
     val maxY = bitmap.height - 1
+    val steps = (grid - 1).coerceAtLeast(1)
     var nonZeroAlpha = 0

     for (row in 0 until grid) {
-      val y = (row.toFloat() / (grid - 1) * maxY).toInt().coerceIn(0, maxY)
+      val y = (row.toFloat() / steps * maxY).toInt().coerceIn(0, maxY)
       for (col in 0 until grid) {
-        val x = (col.toFloat() / (grid - 1) * maxX).toInt().coerceIn(0, maxX)
+        val x = (col.toFloat() / steps * maxX).toInt().coerceIn(0, maxX)
         val pixel = bitmap.getPixel(x, y)
         val alpha = (pixel shr 24) and 0xFF
         if (alpha > 0) nonZeroAlpha++
       }
     }
     return nonZeroAlpha == 0
   }
🧹 Nitpick comments (1)
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (1)

44-56: Consider wiring debugTag through for diagnostics.

apply receives debugTag but drops it when constructing CloudyModifierNodeElement. If debugTag is meant to help trace per-instance behavior (as in the new strategy-based architecture), consider threading it into the node (and optionally InspectorInfo) so you can correlate logs/states with specific modifiers.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f9046d5 and 0a34a66.

📒 Files selected for processing (1)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: l2hyunwoo
Repo: skydoves/Cloudy PR: 63
File: cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt:0-0
Timestamp: 2025-07-06T06:13:55.820Z
Learning: In the Cloudy project, the UIImage.asImageBitmap() function in cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt currently uses a placeholder implementation that generates synthetic gradients instead of extracting actual CGImage pixels. This is known technical debt that should be addressed in a future PR focused on pixel conversion optimization.
📚 Learning: 2025-07-06T06:13:55.820Z
Learnt from: l2hyunwoo
Repo: skydoves/Cloudy PR: 63
File: cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt:0-0
Timestamp: 2025-07-06T06:13:55.820Z
Learning: In the Cloudy project, the UIImage.asImageBitmap() function in cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt currently uses a placeholder implementation that generates synthetic gradients instead of extracting actual CGImage pixels. This is known technical debt that should be addressed in a future PR focused on pixel conversion optimization.

Applied to files:

  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt
🧬 Code graph analysis (1)
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (1)
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internals/render/RenderScriptToolkit.kt (1)
  • iterativeBlur (377-404)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build
  • GitHub Check: API check
  • GitHub Check: Spotless check

@l2hyunwoo
Copy link
Copy Markdown
Collaborator Author

@coderabbitai generate docstrings

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai Bot commented Dec 5, 2025

✅ Actions performed

Initiated docstring generation; will generate only if new commits exist.

coderabbitai Bot added a commit that referenced this pull request Dec 5, 2025
Docstrings generation was requested by @l2hyunwoo.

* #78 (comment)

The following files were modified:

* `androidMain/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt`
* `androidMain/androidx/compose/ui/graphics/layer/GraphicsLayerV29.android.kt`
* `app/src/androidMain/kotlin/demo/BackHandler.android.kt`
* `app/src/commonMain/kotlin/demo/BackHandler.kt`
* `app/src/commonMain/kotlin/demo/Main.kt`
* `app/src/iosMain/kotlin/demo/BackHandler.ios.kt`
* `cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt`
* `cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBlurStrategy.kt`
* `cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt`
* `cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt`
* `cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/TestUtils.kt`
* `cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt`
* `cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt`
* `iosApp/iosApp/ContentView.swift`
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (1)
cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (1)

52-52: debugTag parameter is declared but unused.

The debugTag parameter is part of the function signature but never referenced in the implementation. Consider either:

  1. Using it for logging/debugging (e.g., in a debug build)
  2. Adding a suppress annotation if intentionally unused
 @Composable
 public actual fun Modifier.cloudy(
   @IntRange(from = 0) radius: Int,
   enabled: Boolean,
   onStateChanged: (CloudyState) -> Unit,
+  @Suppress("UNUSED_PARAMETER")
   debugTag: String,
 ): Modifier {
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0a34a66 and 4e34803.

📒 Files selected for processing (2)
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt (1 hunks)
  • cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (1 hunks)
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: l2hyunwoo
Repo: skydoves/Cloudy PR: 63
File: cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt:0-0
Timestamp: 2025-07-06T06:13:55.820Z
Learning: In the Cloudy project, the UIImage.asImageBitmap() function in cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt currently uses a placeholder implementation that generates synthetic gradients instead of extracting actual CGImage pixels. This is known technical debt that should be addressed in a future PR focused on pixel conversion optimization.
📚 Learning: 2025-07-06T06:13:55.820Z
Learnt from: l2hyunwoo
Repo: skydoves/Cloudy PR: 63
File: cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt:0-0
Timestamp: 2025-07-06T06:13:55.820Z
Learning: In the Cloudy project, the UIImage.asImageBitmap() function in cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt currently uses a placeholder implementation that generates synthetic gradients instead of extracting actual CGImage pixels. This is known technical debt that should be addressed in a future PR focused on pixel conversion optimization.

Applied to files:

  • cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build
  • GitHub Check: Spotless check
  • GitHub Check: API check
🔇 Additional comments (1)
cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (1)

60-80: LGTM! State signaling and blur implementation are correct.

Good improvements:

  • LaunchedEffect(radius) properly addresses the side-effect concern from the previous review by moving the callback out of the composition phase.
  • The sigma conversion (radius / 2.0f) and BlurEffect application are consistent with the documented behavior.
  • TileMode.Clamp is an appropriate choice for edge treatment.

Comment thread cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt Outdated
Comment thread cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt Outdated
Docstrings generation was requested by @l2hyunwoo.

* #78 (comment)

The following files were modified:

* `androidMain/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt`
* `androidMain/androidx/compose/ui/graphics/layer/GraphicsLayerV29.android.kt`
* `app/src/androidMain/kotlin/demo/BackHandler.android.kt`
* `app/src/commonMain/kotlin/demo/BackHandler.kt`
* `app/src/commonMain/kotlin/demo/Main.kt`
* `app/src/iosMain/kotlin/demo/BackHandler.ios.kt`
* `cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt`
* `cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBlurStrategy.kt`
* `cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt`
* `cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt`
* `cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/TestUtils.kt`
* `cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt`
* `cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt`
* `iosApp/iosApp/ContentView.swift`

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (5)
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (3)

99-102: onStateChanged callback not updated on recomposition.

When the element is updated, only radius is propagated to the node via updateRadius(). If the onStateChanged callback changes between compositions, the node will continue using the stale callback.

Add an updateOnStateChanged method to CloudyModifierNode and call it from update():

  override fun update(node: CloudyModifierNode) {
    node.onUpdate()
    node.updateRadius(radius)
+   node.updateOnStateChanged(onStateChanged)
  }

And modify CloudyModifierNode:

private var onStateChanged: (CloudyState) -> Unit = {}

fun updateOnStateChanged(callback: (CloudyState) -> Unit) {
  onStateChanged = callback
}

209-213: Resource leak and silent failure on bitmap capture exception.

When graphicsLayer.toImageBitmap() throws, the method returns early without releasing graphicsLayer or notifying the error state. This causes a resource leak and leaves the caller unaware of the failure.

          val capturedBitmap: Bitmap = try {
            graphicsLayer.toImageBitmap().asAndroidBitmap()
          } catch (_: Exception) {
+           graphicsContext.releaseGraphicsLayer(graphicsLayer)
+           onStateChanged.invoke(CloudyState.Error(RuntimeException("Failed to capture bitmap")))
            return@launch
          }

292-308: Potential division by zero when grid equals 1.

If grid is 1, the expression row.toFloat() / (grid - 1) at line 299 divides by zero. While the default value of 4 is safe and the function is private, adding a guard improves robustness.

  private fun isTransparentBitmap(bitmap: Bitmap, grid: Int = 4): Boolean {
    if (bitmap.width == 0 || bitmap.height == 0) return true
+   if (grid < 2) return false // Need at least 2x2 grid for sampling
    val maxX = bitmap.width - 1
cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt (1)

77-88: Syntax error: Missing KDoc closing delimiter.

Line 87 ends the @return documentation but the KDoc block is never closed with */ before the @Composable annotation on line 88. This will cause a compilation error.

Additionally, there are still two consecutive KDoc blocks for the same function. The first block (lines 21-76) is comprehensive but lacks debugTag documentation, while this second block documents debugTag but is redundant. Consider merging the debugTag documentation into the first block and removing this duplicate.

  * @param debugTag Optional tag appended to internal debug traces for identifying this modifier instance.
  * @return A Modifier with the requested blur effect applied (or the original modifier when disabled or unsupported).
+ */
 @Composable
 public expect fun Modifier.cloudy(
cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (1)

26-56: Remove duplicate KDoc blocks.

Two separate KDoc blocks are present (lines 26-46 and 47-56), despite a previous review marking this as addressed. The first block is more comprehensive but missing @param debugTag and @throws documentation, while the second includes these but duplicates content.

Consolidate into a single KDoc block that includes all information:

 /**
  * iOS implementation of the cloudy modifier that applies blur effects to composables.
  *
  * This implementation uses Skia's GPU-accelerated blur via [BlurEffect] which is backed
  * by Metal on iOS for optimal performance. The blur is applied directly in the rendering
  * pipeline without bitmap extraction.
  *
  * ## Performance
  * - Uses GPU acceleration via Skia's Metal backend
  * - No bitmap extraction (GPU→CPU readback) for maximum performance
  * - Returns [CloudyState.Success.Applied] to indicate GPU blur was applied
  *
  * ## Sigma Conversion
  * The blur radius is converted to sigma using: `sigma = radius / 2.0`
  *
  * @param radius The blur radius in pixels. Higher values create more blur.
  * @param enabled Whether the blur effect is enabled. When false, returns the original modifier unchanged.
  * @param onStateChanged Callback that receives updates about the blur processing state.
  *        On iOS, this will receive [CloudyState.Success.Applied] (no bitmap available).
+ * @param debugTag Optional tag for debugging purposes (currently unused on iOS for GPU path).
  * @return Modified Modifier with blur effect applied.
+ * @throws IllegalArgumentException if [radius] is negative.
  */
-/**
- * Applies an iOS GPU-accelerated blur to this Modifier when enabled and radius is greater than zero.
- *
- * @param radius The blur radius (must be >= 0).
- * @param enabled If `false`, the original Modifier is returned unchanged.
- * @param onStateChanged Callback invoked with `CloudyState.Success.Applied` when the blur radius changes and the effect is applied.
- * @param debugTag Unused debug tag available for callers.
- * @return A Modifier with a GPU-accelerated blur renderEffect applied when `enabled` is `true` and `radius` > 0; otherwise the original Modifier.
- * @throws IllegalArgumentException if `radius` is negative.
- */
 @Composable
 public actual fun Modifier.cloudy(
🧹 Nitpick comments (8)
iosApp/iosApp/ContentView.swift (1)

6-8: Consider consolidating redundant documentation.

Lines 6 and 7 convey essentially the same information. Consider keeping only one of them for brevity.

Apply this diff to streamline the documentation:

-    /// Creates the view controller used by this UIViewControllerRepresentable.
     /// Creates the UIViewController that will be presented by this UIViewControllerRepresentable.
     /// - Returns: An instance of `MainViewControllerKt.MainViewController`.
cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/TestUtils.kt (1)

22-35: Remove duplicate KDoc blocks.

There are two consecutive KDoc blocks documenting the same function. Keep only one (preferably the more detailed second block at lines 29-35) and remove the first.

-/**
- * Creates a mock `PlatformBitmap` of the specified width and height for testing purposes.
- *
- * @param width The width of the bitmap in pixels. Defaults to 100.
- * @param height The height of the bitmap in pixels. Defaults to 100.
- * @return A `PlatformBitmap` instance wrapping a mocked Android Bitmap.
- */
 /**
  * Creates a test PlatformBitmap that wraps a Mockito-mocked Android Bitmap with the given dimensions and a non-recycled, mutable state.
  *
androidMain/androidx/compose/ui/graphics/layer/GraphicsLayerV29.android.kt (1)

347-371: Inconsistent indentation in private helper methods.

The methods obtainLayerPaint(), requiresCompositingLayer(), and requiresLayerPaint() have extra indentation compared to the rest of the class. This appears to be a formatting issue.

-    /**
-         * Gets the Paint instance used for the layer, creating and caching it on first access.
-         *
-         * @return The cached `android.graphics.Paint` used to apply layer compositing properties. */
-        private fun obtainLayerPaint(): android.graphics.Paint =
-        layerPaint ?: android.graphics.Paint().also { layerPaint = it }
+    /**
+     * Gets the Paint instance used for the layer, creating and caching it on first access.
+     *
+     * @return The cached `android.graphics.Paint` used to apply layer compositing properties.
+     */
+    private fun obtainLayerPaint(): android.graphics.Paint =
+        layerPaint ?: android.graphics.Paint().also { layerPaint = it }

-    /**
-             * Determines whether this graphics layer requires a separate compositing layer.
-             *
-             * @return `true` if the compositing strategy is `Offscreen`, a layer paint is required, or a render effect is present; `false` otherwise.
-             */
-            private fun requiresCompositingLayer(): Boolean =
-        compositingStrategy == CompositingStrategy.Offscreen ||
-            requiresLayerPaint() ||
-            renderEffect != null
+    /**
+     * Determines whether this graphics layer requires a separate compositing layer.
+     *
+     * @return `true` if the compositing strategy is `Offscreen`, a layer paint is required, or a render effect is present; `false` otherwise.
+     */
+    private fun requiresCompositingLayer(): Boolean =
+        compositingStrategy == CompositingStrategy.Offscreen ||
+            requiresLayerPaint() ||
+            renderEffect != null

-    /**
-         * Indicates whether the layer requires a dedicated Paint for compositing.
-         *
-         * @return `true` if the layer needs a Paint because the blend mode is not `SrcOver` or a color filter is set, `false` otherwise.
-         */
-        private fun requiresLayerPaint(): Boolean =
-        blendMode != BlendMode.SrcOver || colorFilter != null
+    /**
+     * Indicates whether the layer requires a dedicated Paint for compositing.
+     *
+     * @return `true` if the layer needs a Paint because the blend mode is not `SrcOver` or a color filter is set, `false` otherwise.
+     */
+    private fun requiresLayerPaint(): Boolean =
+        blendMode != BlendMode.SrcOver || colorFilter != null
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (1)

233-241: Consider handling potential OutOfMemoryError during blur processing.

The iterativeBlur operation creates intermediate bitmaps and could fail with OutOfMemoryError on memory-constrained devices, especially with large images. The current catch (e: Exception) at line 259 won't catch this.

-      } catch (e: Exception) {
+      } catch (e: Throwable) {
        if (!graphicsLayer.isReleased) {
          graphicsContext.releaseGraphicsLayer(graphicsLayer)
        }
-       onStateChanged.invoke(CloudyState.Error(e))
+       onStateChanged.invoke(CloudyState.Error(if (e is Exception) e else RuntimeException(e)))
      } finally {

Alternatively, wrap bitmap allocation in a try-catch to handle OOM gracefully.

cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (1)

62-62: Clarify the unused debugTag parameter.

The debugTag parameter is accepted but never used in the iOS implementation. While this is likely intentional for cross-platform API consistency (Android strategies use it), consider documenting this explicitly in the KDoc.

Update the @param debugTag documentation to clarify:

 * @param debugTag Reserved for debugging purposes; currently unused in iOS GPU-accelerated path.
app/src/commonMain/kotlin/demo/Main.kt (3)

111-146: Radius list screen structure is clean; consider adding item keys if this grows

testRadiusList plus RadiusListScreen make the demo intent very readable, and padding/insets handling is solid. If this screen later evolves into a dynamic list, consider adding a stable key to items(testRadiusList) (e.g., key = { it }) to guard against item reordering/state mismatches.


208-271: BlurDetailScreen layout and back handling are well-structured

Using PlatformBackHandler alongside a top app bar back affordance gives a predictable detail experience, and the scrollable column that composes static, animated, and text blur tests is easy to follow. If you want to polish accessibility further, you could replace the plain "←" text button with an icon (or text) that includes a descriptive label for screen readers (e.g., “Back”).


273-342: Replayable blur animation in BlurTestCard with radius/flag changes

The separation between static and animated cards and the use of animateIntAsState are solid. One small enhancement: if BlurTestCard is ever reused with a changing radius or animated flag (beyond the current fixed two-card usage), the animation will only run on the first composition because both remember and LaunchedEffect are keyed to Unit.

To make the component robust to prop changes (e.g., different radii over time), you can key both the state and the side‑effect on radius and animated:

-  var animationPlayed by remember { mutableStateOf(!animated) }
+  var animationPlayed by remember(radius, animated) { mutableStateOf(!animated) }
@@
-  LaunchedEffect(Unit) {
+  LaunchedEffect(radius, animated) {
     if (animated) {
       animationPlayed = true
     }
   }

This keeps current behavior for the demo but ensures the blur animates correctly if the inputs change dynamically.

📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4e34803 and 92c9335.

📒 Files selected for processing (14)
  • androidMain/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt (1 hunks)
  • androidMain/androidx/compose/ui/graphics/layer/GraphicsLayerV29.android.kt (1 hunks)
  • app/src/androidMain/kotlin/demo/BackHandler.android.kt (1 hunks)
  • app/src/commonMain/kotlin/demo/BackHandler.kt (1 hunks)
  • app/src/commonMain/kotlin/demo/Main.kt (2 hunks)
  • app/src/iosMain/kotlin/demo/BackHandler.ios.kt (1 hunks)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt (1 hunks)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBlurStrategy.kt (1 hunks)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (1 hunks)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt (1 hunks)
  • cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/TestUtils.kt (1 hunks)
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt (1 hunks)
  • cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (1 hunks)
  • iosApp/iosApp/ContentView.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt
  • androidMain/androidx/compose/ui/graphics/AndroidGraphicsContext.android.kt
  • app/src/commonMain/kotlin/demo/BackHandler.kt
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: l2hyunwoo
Repo: skydoves/Cloudy PR: 63
File: cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt:0-0
Timestamp: 2025-07-06T06:13:55.820Z
Learning: In the Cloudy project, the UIImage.asImageBitmap() function in cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt currently uses a placeholder implementation that generates synthetic gradients instead of extracting actual CGImage pixels. This is known technical debt that should be addressed in a future PR focused on pixel conversion optimization.
📚 Learning: 2025-07-06T06:13:55.820Z
Learnt from: l2hyunwoo
Repo: skydoves/Cloudy PR: 63
File: cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt:0-0
Timestamp: 2025-07-06T06:13:55.820Z
Learning: In the Cloudy project, the UIImage.asImageBitmap() function in cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt currently uses a placeholder implementation that generates synthetic gradients instead of extracting actual CGImage pixels. This is known technical debt that should be addressed in a future PR focused on pixel conversion optimization.

Applied to files:

  • cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt
  • cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/TestUtils.kt
🧬 Code graph analysis (1)
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (1)
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internals/render/RenderScriptToolkit.kt (1)
  • iterativeBlur (377-404)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build
  • GitHub Check: API check
  • GitHub Check: Spotless check
🔇 Additional comments (12)
iosApp/iosApp/ContentView.swift (1)

3-3: LGTM! Import aligns with framework rename.

The change from ComposeApp to CloudyApp is consistent with the framework renaming across the PR.

app/src/iosMain/kotlin/demo/BackHandler.ios.kt (1)

20-29: LGTM for the iOS no-op back handler

KDoc and implementation are aligned: providing a no-op PlatformBackHandler on iOS is a clear and safe way to satisfy the common expect API where there is no system back button.

app/src/androidMain/kotlin/demo/BackHandler.android.kt (1)

21-29: Android BackHandler wiring is correct and properly imported

The implementation correctly delegates PlatformBackHandler to androidx.activity.compose.BackHandler with no shadowing concerns. The import on line 18 is explicit and unambiguous, and no local BackHandler definitions exist elsewhere in the codebase that would create conflicts. The KDoc accurately documents the parameters, and the pattern matches the expected Compose API for platform-specific back handling.

cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/TestUtils.kt (1)

36-43: LGTM!

The mock setup correctly stubs the essential Bitmap properties needed for unit testing the blur pipeline.

cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBlurStrategy.kt (1)

27-49: LGTM!

Clean strategy interface design with clear contract documentation. The separation of GPU and CPU blur strategies via this interface improves maintainability and testability.

androidMain/androidx/compose/ui/graphics/layer/GraphicsLayerV29.android.kt (1)

44-61: LGTM!

The GraphicsLayerV29 implementation correctly wraps the public RenderNode API for Android Q+ with appropriate initialization and compositing strategy defaults.

cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (1)

170-183: LGTM!

The early exit for zero/negative radius correctly bypasses blur processing and reports Success.Applied state.

cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt (1)

26-74: LGTM! Clean refactoring to strategy pattern.

The strategy-based approach cleanly separates concerns and handles all edge cases appropriately. The API-level branching is correct, and the KDoc is comprehensive.

cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (1)

70-90: LGTM! Proper side effect management with LaunchedEffect.

The use of LaunchedEffect(radius) correctly addresses the previous review concern about unconditional side effects during composition. The state notification is properly scoped to radius changes, and the GPU blur application via BlurEffect is clean and efficient.

app/src/commonMain/kotlin/demo/Main.kt (3)

74-108: CloudyDemoApp navigation and AnimatedContent usage look cohesive

The list/detail navigation via selectedRadius and AnimatedContent is clear, and the KDoc accurately describes the flow and transitions. Nice use of directional slide + fade to communicate navigation context.


148-206: RadiusListItem composition and blur preview look good

The card layout, preview image with .cloudy(radius = radius, debugTag = "RadiusListItem"), and the sigma text (radius / 2.0f) align well with the new blur engine semantics. The "No blur" special‑case at radius 0 is also a nice UX touch.


344-404: TextBlurTest nicely showcases content blur

The blurred column around the poster title/description is a clear demonstration of text/content blur, and the KDoc matches the behavior. The use of a dedicated debugTag ("TextBlurColumn") will also help when inspecting engine behavior.

Comment thread cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt Outdated
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
README.md (1)

151-163: Clarify platform-specific behavior in the "Maintaining Blurring Effect" section.

Line 153 states the modifier "captures the bitmap of the composable node under the hood," but this is only true for the CPU path (Android 30-). On GPU paths (iOS and Android 31+), no bitmap is captured—the blur is rendered directly. The example context (LazyVerticalGrid) should either specify the target platform or clarify that bitmap capture is platform-dependent.

Update the comment to reflect platform-specific behavior:

-The `Modifier.cloudy` captures the bitmap of the composable node under the hood.
+The `Modifier.cloudy` behavior depends on the platform: on Android 30-, it captures a bitmap on a background thread; on iOS and Android 31+, blur is rendered directly without bitmap extraction. See the Platform Support table for details.
♻️ Duplicate comments (4)
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt (2)

57-63: Redundant API check inside graphicsLayer.

The strategy selection in Cloudy.android.kt already gates this to API 31+. The check is redundant but provides defense-in-depth. Consider adding @RequiresApi(Build.VERSION_CODES.S) to the class instead, which would allow removing the runtime check.

+import androidx.annotation.RequiresApi
+
+@RequiresApi(Build.VERSION_CODES.S)
 internal object CloudyRenderEffectStrategy : CloudyBlurStrategy {
   // ...
   return modifier.graphicsLayer {
-    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
-      renderEffect = RenderEffect
-        .createBlurEffect(sigma, sigma, Shader.TileMode.CLAMP)
-        .asComposeRenderEffect()
-    }
+    renderEffect = RenderEffect
+      .createBlurEffect(sigma, sigma, Shader.TileMode.CLAMP)
+      .asComposeRenderEffect()
   }

47-53: Success.Applied emitted even when radius=0.

The LaunchedEffect(radius) emits Success.Applied before the radius == 0 check. When blur is effectively disabled, the state still reports Success.Applied, which may be misleading.

   @Composable
   override fun apply(
     modifier: Modifier,
     radius: Int,
     onStateChanged: (CloudyState) -> Unit,
   ): Modifier {
+    if (radius == 0) {
+      LaunchedEffect(Unit) {
+        onStateChanged(CloudyState.Nothing)
+      }
+      return modifier
+    }
+
     LaunchedEffect(radius) {
       onStateChanged(CloudyState.Success.Applied)
     }
-
-    if (radius == 0) {
-      return modifier
-    }

     val sigma = radius / 2.0f
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (2)

98-101: onStateChanged callback not updated on recomposition.

When the element is updated, only radius is propagated to the node. If the onStateChanged callback changes between compositions, the node continues using the stale callback.

   override fun update(node: CloudyModifierNode) {
     node.onUpdate()
     node.updateRadius(radius)
+    node.updateOnStateChanged(onStateChanged)
   }

This requires adding an updateOnStateChanged method to CloudyModifierNode and changing onStateChanged from private val to private var.


280-296: Edge case: potential division by zero when grid is 1.

If grid is 1, the expression row.toFloat() / (grid - 1) divides by zero. While the default is 4 and the function is private, consider adding a guard.

   private fun isTransparentBitmap(bitmap: Bitmap, grid: Int = 4): Boolean {
     if (bitmap.width == 0 || bitmap.height == 0) return true
+    if (grid < 2) return false // Need at least 2x2 grid for sampling
     val maxX = bitmap.width - 1
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 92c9335 and 0d050cd.

📒 Files selected for processing (13)
  • README.md (2 hunks)
  • app/src/androidMain/kotlin/demo/BackHandler.android.kt (1 hunks)
  • app/src/commonMain/kotlin/demo/BackHandler.kt (1 hunks)
  • app/src/commonMain/kotlin/demo/Main.kt (2 hunks)
  • app/src/iosMain/kotlin/demo/BackHandler.ios.kt (1 hunks)
  • cloudy/api/cloudy.api (1 hunks)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt (2 hunks)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBlurStrategy.kt (1 hunks)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (1 hunks)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt (1 hunks)
  • cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/TestUtils.kt (1 hunks)
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt (1 hunks)
  • cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • app/src/androidMain/kotlin/demo/BackHandler.android.kt
  • cloudy/src/commonMain/kotlin/com/skydoves/cloudy/Cloudy.kt
  • app/src/commonMain/kotlin/demo/Main.kt
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: l2hyunwoo
Repo: skydoves/Cloudy PR: 63
File: cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt:0-0
Timestamp: 2025-07-06T06:13:55.820Z
Learning: In the Cloudy project, the UIImage.asImageBitmap() function in cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt currently uses a placeholder implementation that generates synthetic gradients instead of extracting actual CGImage pixels. This is known technical debt that should be addressed in a future PR focused on pixel conversion optimization.
📚 Learning: 2025-07-06T06:13:55.820Z
Learnt from: l2hyunwoo
Repo: skydoves/Cloudy PR: 63
File: cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt:0-0
Timestamp: 2025-07-06T06:13:55.820Z
Learning: In the Cloudy project, the UIImage.asImageBitmap() function in cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt currently uses a placeholder implementation that generates synthetic gradients instead of extracting actual CGImage pixels. This is known technical debt that should be addressed in a future PR focused on pixel conversion optimization.

Applied to files:

  • README.md
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt
  • cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt
  • cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/TestUtils.kt
  • cloudy/api/cloudy.api
🧬 Code graph analysis (1)
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (1)
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/internals/render/RenderScriptToolkit.kt (1)
  • iterativeBlur (377-404)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (3)
  • GitHub Check: build
  • GitHub Check: Spotless check
  • GitHub Check: API check
🔇 Additional comments (9)
README.md (1)

5-5: Documentation accurately reflects the new GPU-first architecture and state redesign.

The updated intro, CloudyState tables, platform support matrix, and code examples correctly document the blur engine overhaul. The when-expression pattern over state subtypes (Applied, Captured, Loading, Error, Nothing) clearly shows the new state-driven API. Platform distinctions (GPU vs. CPU) are explicit and helpful.

Also applies to: 13-13, 17-17, 106-130, 133-150

app/src/commonMain/kotlin/demo/BackHandler.kt (1)

1-27: LGTM! Well-structured multiplatform API.

The expect declaration follows Kotlin multiplatform conventions correctly. The KDoc is clear, and the default parameter value placement is appropriate for the expect side.

app/src/iosMain/kotlin/demo/BackHandler.ios.kt (1)

1-29: LGTM! Appropriate no-op implementation for iOS.

The actual implementation correctly omits the default parameter value (inherited from the expect declaration) and appropriately provides a no-op implementation since iOS uses swipe gestures instead of a system back button. The KDoc clearly explains the platform-specific behavior.

cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyBlurStrategy.kt (1)

27-42: Clean strategy interface design.

The interface provides a clear contract for platform-specific blur implementations. The @Composable annotation on apply allows strategies to use Compose effects internally, which is appropriate for the RenderEffect strategy's LaunchedEffect usage.

cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/TestUtils.kt (1)

36-43: LGTM!

The mock setup correctly stubs the essential Bitmap properties needed for testing the blur strategies.

cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (2)

43-64: LGTM!

The strategy object correctly delegates to CloudyModifierNodeElement for the legacy blur path.


107-145: LGTM!

The node's state management properly handles radius updates by canceling ongoing blur jobs and the pendingInvalidateRequest flag correctly addresses race conditions mentioned in the PR objectives.

cloudy/src/androidMain/kotlin/com/skydoves/cloudy/Cloudy.android.kt (1)

45-55: Clean strategy-based architecture.

The API-level branching is clear and the delegation to strategy implementations is straightforward. This design cleanly separates the GPU and CPU blur paths.

cloudy/api/cloudy.api (1)

32-53: LGTM! CloudyState.Success refactoring is well-structured.

The changes correctly implement the sealed-interface pattern for CloudyState.Success:

  • Success as interface (lines 32-33): Allows polymorphic handling of Applied and Captured variants
  • Applied singleton (lines 35-41): GPU-accelerated path with no bitmap extraction, appropriate for render-effect-based blur
  • Captured with bitmap (lines 43-53): Legacy CPU path providing bitmap access

The copy methods on Captured correctly return the concrete type CloudyState$Success$Captured (lines 47-48) rather than the interface, which maintains type safety and follows Kotlin data class conventions.

This refactoring aligns perfectly with the PR's GPU-first architecture and provides a clean API for distinguishing between GPU-applied blur (no bitmap) and CPU-captured blur (with bitmap).

Comment on lines +22 to +35
/**
* Creates a mock `PlatformBitmap` of the specified width and height for testing purposes.
*
* @param width The width of the bitmap in pixels. Defaults to 100.
* @param height The height of the bitmap in pixels. Defaults to 100.
* @return A `PlatformBitmap` instance wrapping a mocked Android Bitmap.
*/
/**
* Creates a test PlatformBitmap that wraps a Mockito-mocked Android Bitmap with the given dimensions and a non-recycled, mutable state.
*
* @param width The mocked bitmap width in pixels.
* @param height The mocked bitmap height in pixels.
* @return A PlatformBitmap containing the configured mock Bitmap.
*/
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Remove duplicate KDoc block.

There are two consecutive KDoc blocks for the same function. Keep only one.

-/**
- * Creates a mock `PlatformBitmap` of the specified width and height for testing purposes.
- *
- * @param width The width of the bitmap in pixels. Defaults to 100.
- * @param height The height of the bitmap in pixels. Defaults to 100.
- * @return A `PlatformBitmap` instance wrapping a mocked Android Bitmap.
- */
 /**
  * Creates a test PlatformBitmap that wraps a Mockito-mocked Android Bitmap with the given dimensions and a non-recycled, mutable state.
  *
  * @param width The mocked bitmap width in pixels.
  * @param height The mocked bitmap height in pixels.
  * @return A PlatformBitmap containing the configured mock Bitmap.
  */
 internal fun createMockPlatformBitmap(width: Int = 100, height: Int = 100): PlatformBitmap {
🤖 Prompt for AI Agents
In cloudy/src/androidUnitTest/kotlin/com/skydoves/cloudy/TestUtils.kt around
lines 22 to 35, there are two consecutive KDoc blocks documenting the same
function; remove the duplicate block so only a single KDoc comment remains
directly above the function declaration, preserving the more complete/desired
documentation text and leaving surrounding code and formatting unchanged.

Comment thread cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

♻️ Duplicate comments (1)
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (1)

98-102: onStateChanged callback not updated on recomposition.

When the element is updated, only radius is propagated to the node. If the onStateChanged callback changes between compositions, the node will continue using the stale callback.

   override fun update(node: CloudyModifierNode) {
     node.onUpdate()
     node.updateRadius(radius)
+    node.updateOnStateChanged(onStateChanged)
   }

Add an updateOnStateChanged method to CloudyModifierNode and change onStateChanged from private val to private var:

private var onStateChanged: (CloudyState) -> Unit = {}

fun updateOnStateChanged(callback: (CloudyState) -> Unit) {
  onStateChanged = callback
}
🧹 Nitpick comments (1)
cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (1)

194-227: Consider recycling intermediate bitmaps for memory optimization.

On legacy devices (API 30 and below), memory is more constrained. The intermediate bitmaps (capturedBitmap and potentially the copied softwareBitmap) are not explicitly recycled after the blur completes. While they'll eventually be garbage collected, explicit recycling could help reduce memory pressure during rapid recomposition.

           val blurResult = withContext(Dispatchers.Default) {
             val outputBitmap: Bitmap =
               softwareBitmap.toPlatformBitmap().createCompatible().toAndroidBitmap()
             iterativeBlur(
               androidBitmap = softwareBitmap,
               outputBitmap = outputBitmap,
               radius = currentRadius,
             ).await()
           }
+          
+          // Recycle intermediate bitmaps if they differ from the result
+          if (softwareBitmap !== capturedBitmap && !softwareBitmap.isRecycled) {
+            softwareBitmap.recycle()
+          }
+          if (!capturedBitmap.isRecycled) {
+            capturedBitmap.recycle()
+          }

           val result = blurResult?.toPlatformBitmap()
📜 Review details

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0d050cd and 4c34629.

📒 Files selected for processing (3)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (1 hunks)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt (1 hunks)
  • cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyRenderEffectStrategy.kt
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
Learnt from: l2hyunwoo
Repo: skydoves/Cloudy PR: 63
File: cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt:0-0
Timestamp: 2025-07-06T06:13:55.820Z
Learning: In the Cloudy project, the UIImage.asImageBitmap() function in cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt currently uses a placeholder implementation that generates synthetic gradients instead of extracting actual CGImage pixels. This is known technical debt that should be addressed in a future PR focused on pixel conversion optimization.
📚 Learning: 2025-07-06T06:13:55.820Z
Learnt from: l2hyunwoo
Repo: skydoves/Cloudy PR: 63
File: cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt:0-0
Timestamp: 2025-07-06T06:13:55.820Z
Learning: In the Cloudy project, the UIImage.asImageBitmap() function in cloudy/src/iosMain/kotlin/com/skydoves/cloudy/PlatformBitmap.ios.kt currently uses a placeholder implementation that generates synthetic gradients instead of extracting actual CGImage pixels. This is known technical debt that should be addressed in a future PR focused on pixel conversion optimization.

Applied to files:

  • cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt
  • cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: API check
🔇 Additional comments (7)
cloudy/src/iosMain/kotlin/com/skydoves/cloudy/Cloudy.ios.kt (1)

26-63: GPU blur path and state signaling look correct and aligned with the new model.

LaunchedEffect(radius) confines onStateChanged side effects properly, the radius > 0 guard prevents emitting Success.Applied when no blur is rendered, and the enabled/radius == 0 early returns keep the modifier side-effect free in those cases. The sigma conversion and BlurEffect configuration (symmetric radii, TileMode.Clamp) match the documented design of the GPU Applied path. I don’t see any correctness or lifecycle issues here.

cloudy/src/androidMain/kotlin/com/skydoves/cloudy/CloudyLegacyBlurStrategy.kt (6)

39-64: Well-structured strategy pattern implementation.

The CloudyLegacyBlurStrategy object cleanly implements the blur strategy interface with proper documentation. The @SuppressLint annotation is appropriate since the modifier receiver is used via .then().


107-121: Well-designed state management for async blur operations.

The flags (isProcessing, pendingInvalidateRequest, contentMayHaveChanged) properly coordinate between UI updates and background blur processing, addressing the race conditions mentioned in the PR objectives.


123-136: Proper state cleanup on radius change.

The method correctly cancels any in-flight blur work, resets processing flags, and schedules a redraw. The isAttached guard before invalidateDraw() is good defensive programming.


192-260: Solid error handling and resource cleanup.

The structured error handling properly reports CloudyState.Error on failures, and the finally block ensures graphicsLayer is always released. CancellationException is correctly rethrown to support coroutine cancellation.


233-244: Verify transparent bitmap behavior aligns with expected UX.

When the blurred result is fully transparent (lines 235-237), the node sets contentMayHaveChanged = true and clears isProcessing, but no state callback is invoked—the caller remains in Loading. This appears intentional for passive waiting on async image loaders per PR objectives, but if content is genuinely transparent (e.g., a fully transparent view), the caller will stay in Loading indefinitely.

Consider whether an explicit state (e.g., Loading with a hint or a new state variant) should be emitted to inform callers that content capture yielded an empty result.


278-295: Division by zero guard correctly implemented.

The guard at line 280 (if (grid <= 1) return false) prevents the division-by-zero issue identified in a previous review. The grid-based sampling approach is efficient for detecting empty captures.

@l2hyunwoo l2hyunwoo merged commit fe36be4 into main Dec 5, 2025
4 checks passed
@l2hyunwoo l2hyunwoo deleted the feature/implement-blur-engine branch December 5, 2025 09:32
This was referenced Dec 9, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant