Skip to content

fix(android): stop losing view state on detach/re-attach + correctness (audit PR 1/6)#120

Merged
sbaiahmed1 merged 1 commit into
mainfrom
fix/android-pr1-lifecycle-correctness
Jul 16, 2026
Merged

fix(android): stop losing view state on detach/re-attach + correctness (audit PR 1/6)#120
sbaiahmed1 merged 1 commit into
mainfrom
fix/android-pr1-lifecycle-correctness

Conversation

@sbaiahmed1

@sbaiahmed1 sbaiahmed1 commented Jul 16, 2026

Copy link
Copy Markdown
Owner

Part 1 of 6 in the July 2026 audit remediation (see docs/audit-2026-07.md). Stacked on main.

Android correctness, including the one Critical finding.

  • AND-NEW-1 (Critical): BlurSwitch tore down its listeners in onDetachedFromWindow, so after react-navigation detach/re-attach (or FlatList cell clipping) it stopped reporting toggles. Teardown now runs only from onDropViewInstance.
  • BlurView defers its root-swap + init to a posted runnable (mirroring ProgressiveBlurView) so re-attach under navigation reliably re-establishes the Screen-scoped blur root.
  • AND-08 explicit per-corner radius 0; AND-NEW-2 order-independent centre scaling; AND-NEW-4 clamp centre gradient stops; AND-NEW-5 refresh on invalid tint; AND-NEW-3 dead-code removal.
  • AND-02: ships consumer-rules.pro so a consumer's R8 build doesn't strip the reflected QmBlurView fields and silently regress to whole-screen blur (Blur Effect doesnt work on Android it gives full screen blur effect not parent component #89).

Verified on an Android emulator: toggle works, survives navigation, and state persists across a second navigation; no exceptions in logcat.

…correctness

Addresses the PR 1 batch from the July 2026 audit.

BlurSwitch (critical): cleanup() ran from onDetachedFromWindow and nulled both
the internal check-change listener and the onValueChange callback, which are
only wired in the constructor and createViewInstance. react-native-screens
detaches inactive screens and FlatList's removeClippedSubviews detaches
off-screen cells, so after navigating away and back the switch stopped
reporting toggles. cleanup() now runs only from the manager's
onDropViewInstance (true teardown); the detach override is removed.

BlurView: the same detach/re-attach pattern intermittently dropped the
Screen-scoped blur root under react-navigation. The root swap + blur init now
defer to a posted runnable (mirroring ProgressiveBlurView) so the parent chain
and the QmBlurView library's own decor registration settle before the swap.
This also gives the previously-dead initRunnable field a real purpose.

Other correctness fixes:
- Honor an explicit per-corner radius of 0 (test >= 0 against the -1 sentinel).
- Refresh the glass effect when an invalid tint colour is caught.
- Progressive blur: derive the effective radius from the raw amount in one
  place called from both setBlurAmount and setDirection, so the center-direction
  scale-down no longer depends on prop arrival order.
- Progressive blur: clamp the center gradient startEdge to [0.01, 0.3] so the
  LinearGradient stops stay monotonic (above 0.3 they crossed over).
- Remove the unused hasExplicitBackground field.

Ship consumer-rules.pro keeping com.qmdeve.blurview classes/fields so a
consumer app's R8 does not strip the fields the root swap reflects on and
silently fall back to whole-screen blur (issue #89).

Verified on an Android emulator (Pixel 6 Pro, API arm64): baseline toggle,
toggle after tab navigation, and state persistence across a second navigation
all behave correctly; no exceptions in logcat.
@coderabbitai

coderabbitai Bot commented Jul 16, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Android blur behavior now includes published consumer R8 rules, deferred blur initialization, revised cleanup ownership, explicit zero-radius overrides, and centralized progressive blur radius calculation.

Changes

Android blur updates

Layer / File(s) Summary
Consumer R8 rule packaging
android/build.gradle, android/consumer-rules.pro
The published AAR includes consumer rules that preserve com.qmdeve.blurview.** classes and members during minification.
Blur view lifecycle and corner handling
android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt, android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurSwitch.kt
Blur initialization is deferred to the next UI frame, invalid glass tint input refreshes the glass effect, zero-valued corner radii override the base radius, and switch cleanup is limited to view-manager disposal.
Progressive blur radius and gradient state
android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeProgressiveBlurView.kt
Blur amount and direction changes recompute the effective radius, center gradient offsets use bounded monotonic stops, and cleanup no longer resets explicit background state.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: danielaraldi

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately reflects the main lifecycle and correctness fixes around detach/reattach behavior in the Android views.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/android-pr1-lifecycle-correctness

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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Caution

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

⚠️ Outside diff range comments (3)
android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt (1)

405-410: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Use topLeft instead of baseRadius for uniform corner clipping.

When all four specific corners are explicitly overridden to the same value (e.g., setting all corners to 0 to remove the default rounding), isUniform evaluates to true. Passing baseRadius here ignores the explicit override and incorrectly clips the view using the generic borderRadius.

🐛 Proposed fix
       if (isUniform) {
         outlineProvider = object : ViewOutlineProvider() {
           override fun getOutline(view: View, outline: Outline?) {
-            outline?.setRoundRect(0, 0, view.width, view.height, baseRadius)
+            outline?.setRoundRect(0, 0, view.width, view.height, topLeft)
           }
         }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt`
around lines 405 - 410, Update the uniform-corner outline provider in the
isUniform branch to pass topLeft to setRoundRect instead of baseRadius,
preserving explicit equal corner overrides such as zero.
android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeProgressiveBlurView.kt (2)

358-367: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Correct the gradient coordinates for horizontal directions.

The visual mappings for leftToRight and rightToLeft are flipped and handle the offsets incorrectly, causing the blur to render on the wrong edge. For instance, leftToRight (blur on the left, clear on the right) currently assigns point0 (TRANSPARENT/clear) to the left side and point1 (WHITE/blur) to the right side.

Adjust the coordinates to correctly match the vertical gradient logic.

🐛 Proposed fix for directional mapping
             "leftToRight" -> {
+              // Blur at left, clear at right
+              // point0 (TRANSPARENT/clear) at right, point1 (WHITE/blur) at left adjusted by offset
               val offsetPixels = width * currentStartOffset
-              floatArrayOf(offsetPixels, 0f, width.toFloat(), 0f)
+              floatArrayOf(width.toFloat(), 0f, offsetPixels, 0f)
             }
             "rightToLeft" -> {
+              // Blur at right, clear at left
+              // point0 (TRANSPARENT/clear) at left, point1 (WHITE/blur) at right adjusted by offset
               val offsetPixels = width * currentStartOffset
-              floatArrayOf(width.toFloat(), 0f, offsetPixels, 0f)
+              floatArrayOf(0f, 0f, width - offsetPixels, 0f)
             }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeProgressiveBlurView.kt`
around lines 358 - 367, Correct the horizontal coordinate arrays in the
direction mapping around the leftToRight and rightToLeft branches: assign the
gradient’s transparent and blur endpoints to the same respective edges as the
vertical logic, and apply currentStartOffset to the appropriate endpoint so each
direction renders blur on the intended edge. Leave the vertical fallback
unchanged.

78-88: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Fix the blur mapping formula's denominator.

The formula incorrectly uses MAX_BLUR_RADIUS as the denominator instead of MAX_BLUR_AMOUNT. While this coincidentally evaluates to the same result right now because both constants happen to be 100f, this logic will break entirely if MAX_BLUR_RADIUS is corrected to 25f (as documented in the comment above it).

🐛 Proposed fix
     private fun mapBlurAmountToRadius(amount: Float): Float {
       if (amount.isNaN() || amount.isInfinite()) {
         logWarning("Invalid blur amount: $amount, using default")
         return DEFAULT_BLUR_RADIUS
       }
       val clampedAmount = amount.coerceIn(MIN_BLUR_AMOUNT, MAX_BLUR_AMOUNT)
-      return (clampedAmount / MAX_BLUR_RADIUS) * MAX_BLUR_RADIUS
+      return (clampedAmount / MAX_BLUR_AMOUNT) * MAX_BLUR_RADIUS
     }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In
`@android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeProgressiveBlurView.kt`
around lines 78 - 88, Update mapBlurAmountToRadius so the normalized blur amount
is divided by MAX_BLUR_AMOUNT and then scaled by MAX_BLUR_RADIUS. Preserve the
existing invalid-value handling and clamping behavior while ensuring the
documented 0–100 input maps correctly to the 0–25 radius range.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Outside diff comments:
In `@android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt`:
- Around line 405-410: Update the uniform-corner outline provider in the
isUniform branch to pass topLeft to setRoundRect instead of baseRadius,
preserving explicit equal corner overrides such as zero.

In
`@android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeProgressiveBlurView.kt`:
- Around line 358-367: Correct the horizontal coordinate arrays in the direction
mapping around the leftToRight and rightToLeft branches: assign the gradient’s
transparent and blur endpoints to the same respective edges as the vertical
logic, and apply currentStartOffset to the appropriate endpoint so each
direction renders blur on the intended edge. Leave the vertical fallback
unchanged.
- Around line 78-88: Update mapBlurAmountToRadius so the normalized blur amount
is divided by MAX_BLUR_AMOUNT and then scaled by MAX_BLUR_RADIUS. Preserve the
existing invalid-value handling and clamping behavior while ensuring the
documented 0–100 input maps correctly to the 0–25 radius range.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 733e768a-d6d1-400e-8a3c-a5a765ea75f0

📥 Commits

Reviewing files that changed from the base of the PR and between bf8dc70 and 96382b4.

📒 Files selected for processing (5)
  • android/build.gradle
  • android/consumer-rules.pro
  • android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurSwitch.kt
  • android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeBlurView.kt
  • android/src/main/java/com/sbaiahmed1/reactnativeblur/ReactNativeProgressiveBlurView.kt

@sbaiahmed1
sbaiahmed1 merged commit 2a986b7 into main Jul 16, 2026
6 checks passed
@sbaiahmed1
sbaiahmed1 deleted the fix/android-pr1-lifecycle-correctness branch July 16, 2026 17:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant