Skip to content

fix: correct @JvmName in ViewGroupBindings, fix nullable in findRootView, add tests#166

Open
kirich1409 wants to merge 2 commits intodevelopfrom
fix/code-quality-fixes
Open

fix: correct @JvmName in ViewGroupBindings, fix nullable in findRootView, add tests#166
kirich1409 wants to merge 2 commits intodevelopfrom
fix/code-quality-fixes

Conversation

@kirich1409
Copy link
Collaborator

@kirich1409 kirich1409 commented Mar 9, 2026

Summary

  • Fix @JvmName("viewBindingFragment")"viewBindingViewGroup"/"viewBindingViewGroupInflate" in reflection ViewGroupBindings.kt (copy-paste error)
  • Fix DialogFragment.findRootView() returning nullable from findViewById instead of using requireViewByIdCompat when showsDialog=false
  • Add EagerViewBindingProperty.clear() test verifying no-op behavior
  • Add ActivityViewBindingProperty test verifying binding recreation after clear()

Test plan

  • ./gradlew check passes
  • Verify @JvmName change doesn't break Java callers (binary-incompatible for reflection module's ViewGroup extensions from Java)

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Copilot AI review requested due to automatic review settings March 9, 2026 16:24
@qodo-code-review
Copy link

Review Summary by Qodo

Add thread-safety and fix code quality issues across view binding modules

🐞 Bug fix ✨ Enhancement 🧪 Tests

Grey Divider

Walkthroughs

Description
• Added thread-safety with @Volatile and synchronized double-checked locking to
  LazyViewBindingProperty, ActivityViewBindingProperty, and FragmentViewBindingProperty to
  prevent race conditions
• Switched ViewBindingCache to ConcurrentHashMap and marked impl field as @Volatile for
  thread-safe caching
• Fixed nullable bug in DialogFragment.findRootView() using requireViewByIdCompat instead of
  findViewById
• Corrected @JvmName annotations in ViewGroupBindings.kt from "viewBindingFragment" to
  "viewBindingViewGroup"/"viewBindingViewGroupInflate"
• Added comprehensive tests for concurrent access, clear() behavior, and binding recreation
Diagram
flowchart LR
  A["LazyViewBindingProperty"] -->|"add @Volatile + synchronized"| B["Thread-safe getValue/clear"]
  C["ActivityViewBindingProperty"] -->|"add @Volatile + synchronized"| B
  D["FragmentViewBindingProperty"] -->|"add @Volatile + synchronized"| B
  E["ViewBindingCache"] -->|"use ConcurrentHashMap + @Volatile"| F["Thread-safe reflection cache"]
  G["DialogFragment.findRootView"] -->|"fix nullable return"| H["Use requireViewByIdCompat"]
  I["ViewGroupBindings"] -->|"correct @JvmName annotations"| J["Fix Java interop"]
  B -->|"verify with"| K["Concurrent access tests"]
  F -->|"verify with"| K
Loading

Grey Divider

File Changes

1. vbpd-core/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingProperty.kt Thread-safety +8/-2

Add thread-safety to LazyViewBindingProperty

vbpd-core/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingProperty.kt


2. vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/EagerViewBindingPropertyTest.kt 🧪 Tests +11/-0

Add clear() behavior test for EagerViewBindingProperty

vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/EagerViewBindingPropertyTest.kt


3. vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/LazyViewBindingPropertyTest.kt 🧪 Tests +27/-0

Add concurrent access thread-safety test

vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/LazyViewBindingPropertyTest.kt


View more (6)
4. vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingCache.kt Thread-safety +4/-2

Switch to ConcurrentHashMap and add @volatile

vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingCache.kt


5. vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindings.kt 🐞 Bug fix +4/-4

Correct @JvmName annotations for ViewGroup extensions

vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindings.kt


6. vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindings.kt Thread-safety +17/-11

Add thread-safety with synchronized blocks

vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindings.kt


7. vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindings.kt Thread-safety +21/-15

Add thread-safety with synchronized blocks

vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindings.kt


8. vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/internal/VbpdUtils.kt 🐞 Bug fix +1/-1

Fix nullable return in DialogFragment.findRootView

vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/internal/VbpdUtils.kt


9. vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindingPropertyTest.kt 🧪 Tests +34/-0

Add clear() and binding recreation tests

vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindingPropertyTest.kt


Grey Divider

Qodo Logo

@qodo-code-review
Copy link

qodo-code-review bot commented Mar 9, 2026

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📎 Requirement gaps (0)

Grey Divider


Action required

1. clear test lacks visibility 📘 Rule violation ✓ Correctness
Description
The added test function has implicit public visibility because it declares fun without a
visibility modifier. This violates the requirement that all public Kotlin declarations must declare
visibility explicitly.
Code

vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/EagerViewBindingPropertyTest.kt[45]

+    fun `clear does not affect getValue`() {
Evidence
Compliance rules 33295 and 33305 require explicit visibility modifiers for public Kotlin
declarations; the added function declaration omits any modifier, making it implicitly public.

Rule 33295: Require explicit visibility modifiers for all public Kotlin declarations
Rule 33305: Require explicit visibility modifiers on all public Kotlin declarations
vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/EagerViewBindingPropertyTest.kt[45-45]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A newly added Kotlin declaration is implicitly `public` because it omits a visibility modifier.
## Issue Context
The project compliance rules require explicit visibility modifiers for any declaration that would otherwise be public by default.
## Fix Focus Areas
- vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/EagerViewBindingPropertyTest.kt[44-53]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Dialog rootId 0 throws 🐞 Bug ✓ Correctness
Description
DialogFragment.findRootView() treats viewBindingRootId==0 as valid when showsDialog=true (returns
decorView), but when showsDialog=false it unconditionally calls
requireViewByIdCompat(viewBindingRootId) and will throw for 0, making the API behavior inconsistent
depending on showsDialog.
Code

vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/internal/VbpdUtils.kt[55]

+        return requireView().requireViewByIdCompat(viewBindingRootId)
Evidence
In the showsDialog branch, viewBindingRootId==0 is explicitly accepted and returns the decorView. In
the non-dialog branch, the same sentinel is not handled and is passed into requireViewByIdCompat,
which delegates to ViewCompat.requireViewById with no local guard, so 0 is not supported on that
path.

vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/internal/VbpdUtils.kt[45-56]
vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/internal/VbpdUtils.kt[17-21]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`DialogFragment.findRootView()` is inconsistent: it supports `viewBindingRootId == 0` when `showsDialog == true` (returns `decorView`), but will throw when `showsDialog == false` because it unconditionally calls `requireViewByIdCompat(0)`.
### Issue Context
This function already encodes `0` as a sentinel value in the dialog path, so the non-dialog path should handle it consistently.
### Fix Focus Areas
- vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/internal/VbpdUtils.kt[45-56]
### Suggested change (concept)
In the `else` branch:
- `val root = requireView()`
- `return if (viewBindingRootId != 0) root.requireViewByIdCompat(viewBindingRootId) else root`

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Cache getOrPut not atomic 🐞 Bug ➹ Performance
Description
ViewBindingCacheImpl.Default uses ConcurrentHashMap but populates it via Kotlin MutableMap.getOrPut,
allowing concurrent threads to compute/reflect multiple times for the same key and overwrite
entries, undermining the intent of a thread-safe cache.
Code

vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingCache.kt[R22-26]

+        private val inflateCache = ConcurrentHashMap<Class<out ViewBinding>, InflateViewBinding<*>>()
+        private val bindCache = ConcurrentHashMap<Class<out ViewBinding>, BindViewBinding<*>>()

       @Suppress("UNCHECKED_CAST")
       override fun <T : ViewBinding> getInflateWithLayoutInflater(viewBindingClass: Class<T>): InflateViewBinding<T> =
Evidence
The PR changes the caches to ConcurrentHashMap, but the code still uses getOrPut to fill the cache.
On a ConcurrentHashMap, getOrPut is a non-atomic read-then-put sequence, so multiple threads can
still execute the expensive reflection factory creation concurrently for the same ViewBinding class.

vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingCache.kt[21-35]
Best Practice: JDK ConcurrentHashMap

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`ViewBindingCacheImpl.Default` uses `ConcurrentHashMap` but populates it with Kotlin `getOrPut`, which is not an atomic compute-if-absent operation; under contention it can compute the reflection wrapper multiple times.
### Issue Context
The PR’s goal is a thread-safe cache; `computeIfAbsent` provides the required per-key atomicity.
### Fix Focus Areas
- vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingCache.kt[21-35]
### Suggested change (concept)
- For `inflateCache` and `bindCache`, replace `getOrPut` with `computeIfAbsent`.
- Keep the existing casts/suppressions, e.g.:
- `inflateCache.computeIfAbsent(viewBindingClass) { InflateViewBinding(viewBindingClass) } as InflateViewBinding&amp;lt;T&amp;gt;`
- `bindCache.computeIfAbsent(viewBindingClass) { BindViewBinding(viewBindingClass) } as BindViewBinding&amp;lt;T&amp;gt;`

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ The new review experience is currently in Beta. Learn more

Grey Divider

Qodo Logo

}

@Test
fun `clear does not affect getValue`() {

Choose a reason for hiding this comment

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

Action required

1. clear test lacks visibility 📘 Rule violation ✓ Correctness

The added test function has implicit public visibility because it declares fun without a
visibility modifier. This violates the requirement that all public Kotlin declarations must declare
visibility explicitly.
Agent Prompt
## Issue description
A newly added Kotlin declaration is implicitly `public` because it omits a visibility modifier.

## Issue Context
The project compliance rules require explicit visibility modifiers for any declaration that would otherwise be public by default.

## Fix Focus Areas
- vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/EagerViewBindingPropertyTest.kt[44-53]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

}
} else {
return requireView().findViewById(viewBindingRootId)
return requireView().requireViewByIdCompat(viewBindingRootId)

Choose a reason for hiding this comment

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

Action required

2. Dialog rootid 0 throws 🐞 Bug ✓ Correctness

DialogFragment.findRootView() treats viewBindingRootId==0 as valid when showsDialog=true (returns
decorView), but when showsDialog=false it unconditionally calls
requireViewByIdCompat(viewBindingRootId) and will throw for 0, making the API behavior inconsistent
depending on showsDialog.
Agent Prompt
### Issue description
`DialogFragment.findRootView()` is inconsistent: it supports `viewBindingRootId == 0` when `showsDialog == true` (returns `decorView`), but will throw when `showsDialog == false` because it unconditionally calls `requireViewByIdCompat(0)`.

### Issue Context
This function already encodes `0` as a sentinel value in the dialog path, so the non-dialog path should handle it consistently.

### Fix Focus Areas
- vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/internal/VbpdUtils.kt[45-56]

### Suggested change (concept)
In the `else` branch:
- `val root = requireView()`
- `return if (viewBindingRootId != 0) root.requireViewByIdCompat(viewBindingRootId) else root`

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses several thread-safety, correctness, and code quality issues across the vbpd, vbpd-core, and vbpd-reflection modules of the View Binding Property Delegate library.

Changes:

  • Thread-safety hardening: LazyViewBindingProperty, ActivityViewBindingProperty, and FragmentViewBindingProperty now use @Volatile + double-checked locking (synchronized) to prevent race conditions on concurrent getValue()/clear() calls; ViewBindingCache switched to ConcurrentHashMap with @Volatile impl
  • Bug fixes: Corrected DialogFragment.findRootView() to use non-null requireViewByIdCompat instead of nullable findViewById (preventing a return-type contract violation), and fixed all four @JvmName annotations on ViewGroupBindings.kt extension functions (were incorrectly annotated as "viewBindingFragment")
  • New tests: Added concurrent-access test for LazyViewBindingProperty, and clear() behavior tests for EagerViewBindingProperty and ActivityViewBindingProperty

Reviewed changes

Copilot reviewed 9 out of 10 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
vbpd-core/src/main/kotlin/.../ViewBindingProperty.kt Adds @Volatile + synchronized double-checked locking to LazyViewBindingProperty.getValue and clear
vbpd/src/main/kotlin/.../ActivityViewBindings.kt Adds @Volatile to lifecycleCallbacks and wraps registration/clearing in synchronized(this)
vbpd/src/main/kotlin/.../FragmentViewBindings.kt Same thread-safety treatment as ActivityViewBindings.kt for fragment lifecycle callbacks
vbpd/src/main/kotlin/.../internal/VbpdUtils.kt Replaces nullable findViewById with non-null requireViewByIdCompat in DialogFragment.findRootView non-dialog branch
vbpd-reflection/src/main/kotlin/.../ViewGroupBindings.kt Corrects @JvmName on all four ViewGroup.viewBinding overloads from "viewBindingFragment" to correct names
vbpd-reflection/src/main/kotlin/.../ViewBindingCache.kt Adds @Volatile to impl and switches cache maps to ConcurrentHashMap
vbpd-core/src/test/kotlin/.../LazyViewBindingPropertyTest.kt Adds concurrent-access test verifying viewBinder is called exactly once
vbpd-core/src/test/kotlin/.../EagerViewBindingPropertyTest.kt Adds test verifying clear() has no effect on EagerViewBindingProperty
vbpd/src/test/kotlin/.../ActivityViewBindingPropertyTest.kt Adds test verifying clear() resets binding and allows recreation
.gitignore Adds .worktrees/ to ignore list
Comments suppressed due to low confidence (1)

vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingCache.kt:35

  • Using ConcurrentHashMap.getOrPut does NOT provide thread-safe atomic put-if-absent semantics. The Kotlin getOrPut extension function is defined on MutableMap and is not atomic — under concurrent access, it can invoke the value-producing lambda multiple times and may overwrite entries. For true thread-safety on ConcurrentHashMap, use computeIfAbsent instead, which guarantees the lambda is called at most once per key.
        private val inflateCache = ConcurrentHashMap<Class<out ViewBinding>, InflateViewBinding<*>>()
        private val bindCache = ConcurrentHashMap<Class<out ViewBinding>, BindViewBinding<*>>()

        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewBinding> getInflateWithLayoutInflater(viewBindingClass: Class<T>): InflateViewBinding<T> =
            inflateCache.getOrPut(viewBindingClass) {
                InflateViewBinding(viewBindingClass)
            } as InflateViewBinding<T>

        @Suppress("UNCHECKED_CAST")
        override fun <T : ViewBinding> getBind(viewBindingClass: Class<T>): BindViewBinding<T> =
            bindCache.getOrPut(viewBindingClass) {
                BindViewBinding(viewBindingClass)
            } as BindViewBinding<T>

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

You can also share your feedback on Copilot code review. Take the survey.

Comment on lines 52 to 57
@CallSuper
public override fun clear() {
viewBinding = null
synchronized(this) {
viewBinding = null
}
}
Copy link

Copilot AI Mar 9, 2026

Choose a reason for hiding this comment

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

Both ActivityViewBindingProperty.clear() and FragmentViewBindingProperty.clear() call super.clear() (which does synchronized(this) { viewBinding = null }) from within their own synchronized(this) block. While JVM synchronized is reentrant for the same thread so this does not deadlock, it introduces an unnecessary nested lock acquisition. Since viewBinding is already protected by the outer synchronized(this), the inner lock in the parent clear() is redundant. Consider refactoring LazyViewBindingProperty to provide a non-synchronized internal method (e.g. a protected fun clearViewBinding()) that subclasses can call within their own synchronized block.

Copilot uses AI. Check for mistakes.
…iew, add tests

- Fix @JvmName("viewBindingFragment") → "viewBindingViewGroup"/"viewBindingViewGroupInflate"
  in reflection ViewGroupBindings (copy-paste error from FragmentViewBindings)
- Fix DialogFragment.findRootView() using findViewById (nullable) instead of
  requireViewByIdCompat (non-null) when showsDialog=false
- Add EagerViewBindingProperty.clear() test verifying no-op behavior
- Add ActivityViewBindingProperty test verifying binding recreation after clear()

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
@kirich1409 kirich1409 force-pushed the fix/code-quality-fixes branch from f0feb15 to 0619668 Compare March 9, 2026 17:01
@kirich1409 kirich1409 changed the title fix: thread-safety and code quality improvements across all modules fix: correct @JvmName in ViewGroupBindings, fix nullable in findRootView, add tests Mar 9, 2026
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.

2 participants