From 5979a593ab4be1d38422a5307e2ced18898286a8 Mon Sep 17 00:00:00 2001 From: kirich1409 Date: Sat, 14 Feb 2026 21:15:38 +0300 Subject: [PATCH 1/5] Add design document for tests and code quality improvements Plan covers: Detekt + ktlint, Kover, Robolectric + AndroidX Test + MockK, CI fixes. Co-Authored-By: Claude Opus 4.6 --- .../2026-02-14-tests-and-quality-design.md | 87 +++++++++++++++++++ 1 file changed, 87 insertions(+) create mode 100644 docs/plans/2026-02-14-tests-and-quality-design.md diff --git a/docs/plans/2026-02-14-tests-and-quality-design.md b/docs/plans/2026-02-14-tests-and-quality-design.md new file mode 100644 index 0000000..fcb6041 --- /dev/null +++ b/docs/plans/2026-02-14-tests-and-quality-design.md @@ -0,0 +1,87 @@ +# ViewBindingPropertyDelegate: Тесты и Качество кода + +**Дата**: 2026-02-14 +**Статус**: Одобрен +**Версия библиотеки**: 2.0.4 + +## Цель + +Добавить тестовое покрытие, статический анализ и контроль качества кода в проект ViewBindingPropertyDelegate. Исправить CI/CD пайплайн. + +## Решения + +### Подход + +«Фундамент сначала» — последовательная стратегия: +1. Инструменты (Detekt + ktlint + Kover) → 2. Исправление замечаний линтеров → 3. Тесты → 4. CI + +### Тестовый стек + +| Инструмент | Назначение | +|---|---| +| kotlin.test | Ассерты (Kotlin-нативные) | +| JUnit4 | Раннер (совместимость с Robolectric и AndroidX Test) | +| AndroidX Test | ActivityScenario, FragmentScenario — единое API для локальных и instrumented тестов | +| Robolectric | Локальный запуск Android-тестов без эмулятора | +| MockK | Kotlin-нативный мокинг | + +### Инструменты качества + +| Инструмент | Назначение | +|---|---| +| Detekt | Статический анализ Kotlin-кода | +| ktlint | Форматирование и стиль кода | +| Kover | Измерение покрытия тестами (JetBrains) | + +## Секция 1: Инфраструктура качества кода + +### Detekt +- Подключается через convention plugin `vbpdconfig.gradle.kts` +- Кастомный `detekt.yml` с правилами для библиотеки +- Задача `detekt` добавляется в `check` + +### ktlint +- Через плагин `ktlint-gradle` +- Стандартные Kotlin Coding Conventions +- Задача `ktlintCheck` добавляется в `check` + +### Kover +- Подключается через convention plugin +- HTML и XML отчёты +- Минимальный порог покрытия: 60% (с планом повышения) + +## Секция 2: Тестовое покрытие + +### vbpd-core (юнит-тесты) +- `LazyViewBindingProperty` — ленивая инициализация, кэширование, очистка +- `EagerViewBindingProperty` — немедленная инициализация + +### vbpd (Robolectric + AndroidX Test) +- `ActivityViewBindingProperty` — lifecycle Activity, очистка при onDestroy +- `FragmentViewBindingProperty` — lifecycle Fragment, очистка при onDestroyView +- `ViewGroupBindings` — обычный режим / edit mode +- `ViewHolderBindings` — привязка к ViewHolder +- DialogFragment — специальная обработка + +### vbpd-reflection (юнит + Robolectric) +- `ViewBindingCache` — включение/выключение, корректность +- Рефлексия: BIND vs INFLATE, merge-layouts +- Lifecycle-тесты с reflection API + +## Секция 3: Исправление CI + +### build.yml (develop) +- Убрать `continue-on-error: true` +- Добавить `testReleaseUnitTest` +- Добавить `detekt`, `ktlintCheck` +- Добавить `koverXmlReport` + +### android.yml (master) +- Аналогичные проверки для PR +- Upload coverage report как артефакт + +## Что НЕ входит в скоуп +- Git pre-commit hooks +- Изменение существующего публичного API +- Новые фичи библиотеки +- Автоматизация публикации на Maven Central From ab7d3fc2f935de7941ed31182f66ea9a14c8840e Mon Sep 17 00:00:00 2001 From: kirich1409 Date: Sat, 14 Feb 2026 21:20:50 +0300 Subject: [PATCH 2/5] Add implementation plan for tests and quality improvements 13 tasks: Detekt, ktlint, Kover setup in convention plugins, tests for vbpd-core/vbpd/vbpd-reflection, CI pipeline fixes. Co-Authored-By: Claude Opus 4.6 --- .../2026-02-14-tests-and-quality-plan.md | 1143 +++++++++++++++++ 1 file changed, 1143 insertions(+) create mode 100644 docs/plans/2026-02-14-tests-and-quality-plan.md diff --git a/docs/plans/2026-02-14-tests-and-quality-plan.md b/docs/plans/2026-02-14-tests-and-quality-plan.md new file mode 100644 index 0000000..8e8bb74 --- /dev/null +++ b/docs/plans/2026-02-14-tests-and-quality-plan.md @@ -0,0 +1,1143 @@ +# Tests & Quality Implementation Plan + +> **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. + +**Goal:** Add comprehensive test coverage, static analysis (Detekt + ktlint), code coverage (Kover), and fix CI for ViewBindingPropertyDelegate library. + +**Architecture:** Infrastructure-first approach — configure quality tools in Gradle convention plugins, then write tests module-by-module (vbpd-core → vbpd → vbpd-reflection), then fix CI to run everything automatically. + +**Tech Stack:** Detekt 1.23.8, ktlint-gradle 14.0.1, Kover 0.9.7, Robolectric 4.16.1, MockK 1.14.9, AndroidX Test 1.6.1, kotlin.test, JUnit4 + +--- + +## Task 1: Add tool versions to version catalog + +**Files:** +- Modify: `gradle/libs.versions.toml` + +**Step 1: Add versions and dependencies to version catalog** + +Add the following entries to `gradle/libs.versions.toml`: + +```toml +# In [versions] section, add: +detekt = "1.23.8" +ktlint = "14.0.1" +kover = "0.9.7" +robolectric = "4.16.1" +mockk = "1.14.9" +androidx-test-core = "1.6.1" +androidx-test-runner = "1.6.2" +junit = "4.13.2" + +# In [libraries] section, add: +test-robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } +test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +test-junit = { module = "junit:junit", version.ref = "junit" } +test-kotlin = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +test-androidx-core = { module = "androidx.test:core-ktx", version.ref = "androidx-test-core" } +test-androidx-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } +test-fragment = { module = "androidx.fragment:fragment-testing", version.ref = "androidx-fragment" } + +# In [plugins] section, add: +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } +kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } +``` + +**Step 2: Verify catalog is valid** + +Run: `./gradlew help` +Expected: BUILD SUCCESSFUL (validates catalog parsing) + +**Step 3: Commit** + +```bash +git add gradle/libs.versions.toml +git commit -m "Add versions for Detekt, ktlint, Kover and test dependencies" +``` + +--- + +## Task 2: Add Detekt to convention plugin + +**Files:** +- Modify: `gradle/convetions-plugins/vbpd-library-base/build.gradle.kts` +- Modify: `gradle/convetions-plugins/vbpd-library-base/src/main/kotlin/vbpdconfig.gradle.kts` +- Create: `config/detekt/detekt.yml` + +**Step 1: Add Detekt plugin dependency to convention plugin build** + +In `gradle/convetions-plugins/vbpd-library-base/build.gradle.kts`, add to `dependencies` block: + +```kotlin +// Add this line alongside existing gradleplugin dependencies: +implementation(libs.gradleplugins.detekt) +``` + +Also add to `gradle/libs.versions.toml` in `[libraries]`: +```toml +gradleplugins-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } +``` + +**Step 2: Apply and configure Detekt in convention plugin** + +In `gradle/convetions-plugins/vbpd-library-base/src/main/kotlin/vbpdconfig.gradle.kts`, add after existing plugin applies: + +```kotlin +plugins.apply(libs.plugins.detekt.get().pluginId) +``` + +And add Detekt configuration at the end of the file: + +```kotlin +extensions.configure { + config.setFrom(rootProject.files("config/detekt/detekt.yml")) + buildUponDefaultConfig = true + parallel = true +} +``` + +Import needs to be added at the top: +```kotlin +import io.gitlab.arturbosch.detekt.extensions.DetektExtension +``` + +**Step 3: Create Detekt configuration file** + +Create `config/detekt/detekt.yml`: + +```yaml +build: + maxIssues: 0 + +complexity: + LongMethod: + threshold: 60 + LongParameterList: + functionThreshold: 8 + constructorThreshold: 8 + TooManyFunctions: + thresholdInFiles: 20 + thresholdInClasses: 15 + thresholdInInterfaces: 10 + +style: + MaxLineLength: + maxLineLength: 120 + WildcardImport: + active: false + MagicNumber: + active: false + ReturnCount: + max: 3 + UnusedPrivateMember: + active: true + +naming: + FunctionNaming: + functionPattern: '[a-zA-Z][a-zA-Z0-9]*' +``` + +**Step 4: Run Detekt to verify configuration** + +Run: `./gradlew detekt` +Expected: Either BUILD SUCCESSFUL or specific lint issues to review + +**Step 5: Commit** + +```bash +git add config/detekt/detekt.yml gradle/convetions-plugins/ gradle/libs.versions.toml +git commit -m "Add Detekt static analysis to convention plugin" +``` + +--- + +## Task 3: Add ktlint to convention plugin + +**Files:** +- Modify: `gradle/convetions-plugins/vbpd-library-base/build.gradle.kts` +- Modify: `gradle/convetions-plugins/vbpd-library-base/src/main/kotlin/vbpdconfig.gradle.kts` + +**Step 1: Add ktlint plugin dependency** + +In `gradle/libs.versions.toml` in `[libraries]`: +```toml +gradleplugins-ktlint = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlint" } +``` + +In `gradle/convetions-plugins/vbpd-library-base/build.gradle.kts`, add to `dependencies`: +```kotlin +implementation(libs.gradleplugins.ktlint) +``` + +**Step 2: Apply ktlint in convention plugin** + +In `vbpdconfig.gradle.kts`, add after other plugin applies: + +```kotlin +plugins.apply(libs.plugins.ktlint.get().pluginId) +``` + +**Step 3: Run ktlint to verify** + +Run: `./gradlew ktlintCheck` +Expected: Either BUILD SUCCESSFUL or formatting issues to review + +**Step 4: Fix any formatting issues** + +Run: `./gradlew ktlintFormat` +Then manually review and fix remaining issues. + +**Step 5: Commit** + +```bash +git add -A +git commit -m "Add ktlint code formatting to convention plugin" +``` + +--- + +## Task 4: Add Kover to convention plugin + +**Files:** +- Modify: `gradle/convetions-plugins/vbpd-library-base/build.gradle.kts` +- Modify: `gradle/convetions-plugins/vbpd-library-base/src/main/kotlin/vbpdconfig.gradle.kts` + +**Step 1: Add Kover plugin dependency** + +In `gradle/libs.versions.toml` in `[libraries]`: +```toml +gradleplugins-kover = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" } +``` + +In `gradle/convetions-plugins/vbpd-library-base/build.gradle.kts`, add to `dependencies`: +```kotlin +implementation(libs.gradleplugins.kover) +``` + +**Step 2: Apply and configure Kover in convention plugin** + +In `vbpdconfig.gradle.kts`, add after other plugin applies: + +```kotlin +plugins.apply(libs.plugins.kover.get().pluginId) +``` + +**Step 3: Run to verify Kover is configured** + +Run: `./gradlew koverHtmlReport` +Expected: BUILD SUCCESSFUL (report will be empty until tests exist) + +**Step 4: Commit** + +```bash +git add gradle/convetions-plugins/ gradle/libs.versions.toml +git commit -m "Add Kover code coverage to convention plugin" +``` + +--- + +## Task 5: Fix lint issues in existing code + +**Files:** +- Potentially modify source files in all library modules + +**Step 1: Run full check** + +Run: `./gradlew detekt ktlintCheck` +Review output to understand what issues exist. + +**Step 2: Auto-fix formatting** + +Run: `./gradlew ktlintFormat` + +**Step 3: Manually fix remaining Detekt issues** + +Fix any remaining issues reported by Detekt. These might include: +- Line length violations +- Unused parameters +- Complexity issues + +Do NOT change public API signatures. Only fix internal code style. + +**Step 4: Verify all checks pass** + +Run: `./gradlew detekt ktlintCheck` +Expected: BUILD SUCCESSFUL + +**Step 5: Commit** + +```bash +git add -A +git commit -m "Fix code style issues reported by Detekt and ktlint" +``` + +--- + +## Task 6: Configure test infrastructure for vbpd-core + +**Files:** +- Modify: `vbpd-core/build.gradle.kts` +- Create: `vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/LazyViewBindingPropertyTest.kt` +- Create: `vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/EagerViewBindingPropertyTest.kt` + +**Step 1: Add test dependencies to vbpd-core** + +In `vbpd-core/build.gradle.kts`: +```kotlin +plugins { + id("vbpdconfig") +} + +dependencies { + api(libs.androidx.viewbinding) + implementation(libs.androidx.annotation) + + testImplementation(libs.test.junit) + testImplementation(libs.test.kotlin) + testImplementation(libs.test.mockk) + testImplementation(libs.test.robolectric) + testImplementation(libs.test.androidx.core) +} +``` + +Also in `vbpdconfig.gradle.kts`, enable unit tests if not already enabled: + +```kotlin +// Inside androidLibraryConfig block, add: +testOptions { + unitTests { + isIncludeAndroidResources = true + } +} +``` + +**Step 2: Write failing test for LazyViewBindingProperty** + +Create `vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/LazyViewBindingPropertyTest.kt`: + +```kotlin +package dev.androidbroadcast.vbpd + +import android.view.View +import androidx.viewbinding.ViewBinding +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertNull + +@RunWith(RobolectricTestRunner::class) +class LazyViewBindingPropertyTest { + + private fun createMockBinding(): ViewBinding { + val view = mockk() + return mockk { + every { root } returns view + } + } + + @Test + fun `getValue returns binding created by viewBinder`() { + val expectedBinding = createMockBinding() + val property = LazyViewBindingProperty { expectedBinding } + val thisRef = Any() + + val result = property.getValue(thisRef, ::result) + assertEquals(expectedBinding, result) + } + + @Test + fun `getValue returns same instance on subsequent calls`() { + var callCount = 0 + val binding = createMockBinding() + val property = LazyViewBindingProperty { + callCount++ + binding + } + val thisRef = Any() + + val first = property.getValue(thisRef, ::first) + val second = property.getValue(thisRef, ::second) + + assertEquals(first, second) + assertEquals(1, callCount, "viewBinder should be called only once") + } + + @Test + fun `clear resets cached binding`() { + var callCount = 0 + val property = LazyViewBindingProperty { + callCount++ + createMockBinding() + } + val thisRef = Any() + + property.getValue(thisRef, ::thisRef) + property.clear() + property.getValue(thisRef, ::thisRef) + + assertEquals(2, callCount, "viewBinder should be called again after clear()") + } + + @Test + fun `viewBinder receives correct thisRef`() { + var receivedRef: Any? = null + val property = LazyViewBindingProperty { ref -> + receivedRef = ref + createMockBinding() + } + + property.getValue("test", ::receivedRef) + assertEquals("test", receivedRef) + } +} +``` + +**Step 3: Run test to verify it compiles and passes** + +Run: `./gradlew :vbpd-core:testDebugUnitTest --tests "dev.androidbroadcast.vbpd.LazyViewBindingPropertyTest"` +Expected: All tests PASS + +**Step 4: Write test for EagerViewBindingProperty** + +Create `vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/EagerViewBindingPropertyTest.kt`: + +```kotlin +package dev.androidbroadcast.vbpd + +import android.view.View +import androidx.viewbinding.ViewBinding +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertEquals + +@RunWith(RobolectricTestRunner::class) +class EagerViewBindingPropertyTest { + + private fun createMockBinding(): ViewBinding { + val view = mockk() + return mockk { + every { root } returns view + } + } + + @Test + fun `getValue returns the provided binding`() { + val expectedBinding = createMockBinding() + val property = EagerViewBindingProperty(expectedBinding) + + val result = property.getValue(Any(), ::result) + assertEquals(expectedBinding, result) + } + + @Test + fun `getValue always returns same instance`() { + val binding = createMockBinding() + val property = EagerViewBindingProperty(binding) + val thisRef = Any() + + val first = property.getValue(thisRef, ::first) + val second = property.getValue(thisRef, ::second) + assertEquals(first, second) + } +} +``` + +**Step 5: Run all vbpd-core tests** + +Run: `./gradlew :vbpd-core:testDebugUnitTest` +Expected: All tests PASS + +**Step 6: Commit** + +```bash +git add vbpd-core/build.gradle.kts vbpd-core/src/test/ +git commit -m "Add unit tests for vbpd-core (LazyViewBindingProperty, EagerViewBindingProperty)" +``` + +--- + +## Task 7: Add tests for vbpd module — Activity lifecycle + +**Files:** +- Modify: `vbpd/build.gradle.kts` +- Create: `vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindingPropertyTest.kt` + +**Step 1: Add test dependencies to vbpd** + +In `vbpd/build.gradle.kts`: +```kotlin +plugins { + id("vbpdconfig") +} + +dependencies { + compileOnly(libs.androidx.fragment) + compileOnly(libs.androidx.activity) + compileOnly(libs.androidx.recyclerview) + api(projects.vbpdCore) + + testImplementation(libs.test.junit) + testImplementation(libs.test.kotlin) + testImplementation(libs.test.mockk) + testImplementation(libs.test.robolectric) + testImplementation(libs.test.androidx.core) + testImplementation(libs.test.androidx.runner) + testImplementation(libs.androidx.fragment) + testImplementation(libs.androidx.activity) + testImplementation(libs.androidx.recyclerview) + testImplementation(libs.androidx.appcompat) +} +``` + +**Step 2: Write Activity lifecycle test** + +Create `vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindingPropertyTest.kt`: + +```kotlin +package dev.androidbroadcast.vbpd + +import android.app.Activity +import android.os.Bundle +import android.view.View +import android.widget.FrameLayout +import androidx.test.core.app.ActivityScenario +import androidx.viewbinding.ViewBinding +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import org.robolectric.android.controller.ActivityController +import kotlin.test.assertNotNull + +@RunWith(RobolectricTestRunner::class) +class ActivityViewBindingPropertyTest { + + class TestActivity : Activity() { + val binding by viewBinding { activity: TestActivity -> + mockk { + every { root } returns View(activity) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val content = FrameLayout(this) + content.id = android.R.id.content + setContentView(content) + } + } + + @Test + fun `binding is accessible after onCreate`() { + val controller = Robolectric.buildActivity(TestActivity::class.java) + .create() + .start() + .resume() + + val activity = controller.get() + assertNotNull(activity.binding) + } + + @Test + fun `binding is cleared after onDestroy`() { + val controller = Robolectric.buildActivity(TestActivity::class.java) + .create() + .start() + .resume() + + val activity = controller.get() + // Access binding to initialize it + assertNotNull(activity.binding) + + // Destroy and verify no crash + controller.pause().stop().destroy() + } +} +``` + +**Step 3: Run tests** + +Run: `./gradlew :vbpd:testDebugUnitTest --tests "dev.androidbroadcast.vbpd.ActivityViewBindingPropertyTest"` +Expected: All tests PASS + +**Step 4: Commit** + +```bash +git add vbpd/build.gradle.kts vbpd/src/test/ +git commit -m "Add Activity lifecycle tests for vbpd module" +``` + +--- + +## Task 8: Add tests for vbpd module — Fragment lifecycle + +**Files:** +- Create: `vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindingPropertyTest.kt` + +**Step 1: Add Fragment testing dependency if needed** + +In `vbpd/build.gradle.kts` add: +```kotlin +testImplementation(libs.test.fragment) +``` + +**Step 2: Write Fragment lifecycle test** + +Create `vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindingPropertyTest.kt`: + +```kotlin +package dev.androidbroadcast.vbpd + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.fragment.app.Fragment +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.lifecycle.Lifecycle +import androidx.viewbinding.ViewBinding +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertNotNull + +@RunWith(RobolectricTestRunner::class) +class FragmentViewBindingPropertyTest { + + class TestFragment : Fragment() { + val binding by viewBinding { fragment: TestFragment -> + mockk { + every { root } returns fragment.requireView() + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return FrameLayout(requireContext()) + } + } + + @Test + fun `binding is accessible after onViewCreated`() { + val scenario = launchFragmentInContainer() + scenario.onFragment { fragment -> + assertNotNull(fragment.binding) + } + } + + @Test + fun `binding survives configuration change lifecycle`() { + val scenario = launchFragmentInContainer() + scenario.onFragment { fragment -> + assertNotNull(fragment.binding) + } + scenario.moveToState(Lifecycle.State.DESTROYED) + } +} +``` + +**Step 3: Run tests** + +Run: `./gradlew :vbpd:testDebugUnitTest --tests "dev.androidbroadcast.vbpd.FragmentViewBindingPropertyTest"` +Expected: All tests PASS + +**Step 4: Commit** + +```bash +git add vbpd/src/test/ +git commit -m "Add Fragment lifecycle tests for vbpd module" +``` + +--- + +## Task 9: Add tests for vbpd module — ViewGroup and ViewHolder + +**Files:** +- Create: `vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindingsTest.kt` +- Create: `vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindingsTest.kt` + +**Step 1: Write ViewGroup test** + +Create `vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindingsTest.kt`: + +```kotlin +package dev.androidbroadcast.vbpd + +import android.content.Context +import android.view.View +import android.widget.FrameLayout +import androidx.test.core.app.ApplicationProvider +import androidx.viewbinding.ViewBinding +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@RunWith(RobolectricTestRunner::class) +class ViewGroupBindingsTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + @Test + fun `viewBinding creates lazy binding for ViewGroup`() { + val viewGroup = FrameLayout(context) + val expectedBinding = mockk { + every { root } returns View(context) + } + + val property = viewGroup.viewBinding { _: FrameLayout -> expectedBinding } + val result = property.getValue(viewGroup, ::result) + + assertEquals(expectedBinding, result) + } + + @Test + fun `viewBinding returns same instance on subsequent calls`() { + val viewGroup = FrameLayout(context) + val binding = mockk { + every { root } returns View(context) + } + + val property = viewGroup.viewBinding { _: FrameLayout -> binding } + val first = property.getValue(viewGroup, ::first) + val second = property.getValue(viewGroup, ::second) + + assertEquals(first, second) + } +} +``` + +**Step 2: Write ViewHolder test** + +Create `vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindingsTest.kt`: + +```kotlin +package dev.androidbroadcast.vbpd + +import android.content.Context +import android.view.View +import android.widget.FrameLayout +import androidx.recyclerview.widget.RecyclerView +import androidx.test.core.app.ApplicationProvider +import androidx.viewbinding.ViewBinding +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertEquals +import kotlin.test.assertNotNull + +@RunWith(RobolectricTestRunner::class) +class ViewHolderBindingsTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + class TestViewHolder(view: View) : RecyclerView.ViewHolder(view) + + @Test + fun `viewBinding creates binding from ViewHolder`() { + val view = FrameLayout(context) + val viewHolder = TestViewHolder(view) + val expectedBinding = mockk { + every { root } returns view + } + + val property = viewHolder.viewBinding { _: TestViewHolder -> expectedBinding } + val result = property.getValue(viewHolder, ::result) + + assertEquals(expectedBinding, result) + } + + @Test + fun `viewBinding with factory and viewProvider`() { + val view = FrameLayout(context) + val viewHolder = TestViewHolder(view) + val expectedBinding = mockk { + every { root } returns view + } + + val property = viewHolder.viewBinding( + vbFactory = { expectedBinding }, + viewProvider = { it.itemView } + ) + val result = property.getValue(viewHolder, ::result) + + assertEquals(expectedBinding, result) + } +} +``` + +**Step 3: Run all vbpd tests** + +Run: `./gradlew :vbpd:testDebugUnitTest` +Expected: All tests PASS + +**Step 4: Commit** + +```bash +git add vbpd/src/test/ +git commit -m "Add ViewGroup and ViewHolder tests for vbpd module" +``` + +--- + +## Task 10: Add tests for vbpd-reflection module + +**Files:** +- Modify: `vbpd-reflection/build.gradle.kts` +- Create: `vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ViewBindingCacheTest.kt` +- Create: `vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ReflectionActivityViewBindingsTest.kt` + +**Step 1: Add test dependencies to vbpd-reflection** + +In `vbpd-reflection/build.gradle.kts`: +```kotlin +plugins { + id("vbpdconfig") +} + +android { + buildTypes { + release { + consumerProguardFiles("proguard-rules.pro") + } + } +} + +dependencies { + compileOnly(libs.androidx.fragment) + compileOnly(libs.androidx.recyclerview) + compileOnly(libs.androidx.activity) + api(projects.vbpd) + + testImplementation(libs.test.junit) + testImplementation(libs.test.kotlin) + testImplementation(libs.test.mockk) + testImplementation(libs.test.robolectric) + testImplementation(libs.test.androidx.core) + testImplementation(libs.test.androidx.runner) + testImplementation(libs.androidx.fragment) + testImplementation(libs.androidx.activity) + testImplementation(libs.androidx.recyclerview) + testImplementation(libs.androidx.appcompat) +} +``` + +**Step 2: Write ViewBindingCache test** + +Create `vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ViewBindingCacheTest.kt`: + +```kotlin +package dev.androidbroadcast.vbpd + +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertNotNull + +@RunWith(RobolectricTestRunner::class) +class ViewBindingCacheTest { + + @Before + fun setup() { + ViewBindingCache.clear() + } + + @After + fun tearDown() { + ViewBindingCache.clear() + ViewBindingCache.setEnabled(false) + } + + @Test + fun `cache can be enabled and disabled`() { + ViewBindingCache.setEnabled(true) + ViewBindingCache.setEnabled(false) + // No crash = success + } + + @Test + fun `clear does not crash when cache is empty`() { + ViewBindingCache.clear() + ViewBindingCache.setEnabled(true) + ViewBindingCache.clear() + // No crash = success + } + + @Test + fun `setEnabled true then false resets cache`() { + ViewBindingCache.setEnabled(true) + ViewBindingCache.setEnabled(false) + ViewBindingCache.clear() // Should not crash on Noop + } +} +``` + +**Step 3: Write reflection Activity test** + +Create `vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ReflectionActivityViewBindingsTest.kt`: + +```kotlin +package dev.androidbroadcast.vbpd + +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertNotNull + +@RunWith(RobolectricTestRunner::class) +class ReflectionActivityViewBindingsTest { + + @Test + fun `CreateMethod BIND and INFLATE enum values exist`() { + assertNotNull(CreateMethod.BIND) + assertNotNull(CreateMethod.INFLATE) + } + + @Test + fun `CreateMethod values are distinct`() { + assert(CreateMethod.BIND != CreateMethod.INFLATE) + } +} +``` + +**Step 4: Run all vbpd-reflection tests** + +Run: `./gradlew :vbpd-reflection:testDebugUnitTest` +Expected: All tests PASS + +**Step 5: Commit** + +```bash +git add vbpd-reflection/build.gradle.kts vbpd-reflection/src/test/ +git commit -m "Add tests for vbpd-reflection module (ViewBindingCache, CreateMethod)" +``` + +--- + +## Task 11: Run full test suite and verify coverage + +**Files:** None (verification only) + +**Step 1: Run all tests** + +Run: `./gradlew testDebugUnitTest` +Expected: All tests PASS across all modules + +**Step 2: Generate coverage report** + +Run: `./gradlew koverHtmlReport` +Expected: BUILD SUCCESSFUL. HTML report generated. + +**Step 3: Review coverage report** + +Open the generated HTML report and verify coverage numbers. Report locations: +- `vbpd-core/build/reports/kover/html/index.html` +- `vbpd/build/reports/kover/html/index.html` +- `vbpd-reflection/build/reports/kover/html/index.html` + +**Step 4: Run all quality checks** + +Run: `./gradlew check` +Expected: BUILD SUCCESSFUL (includes detekt + ktlint + tests) + +**Step 5: Commit any fixes if needed** + +```bash +git add -A +git commit -m "Verify full test suite and coverage reports" +``` + +--- + +## Task 12: Fix CI pipelines + +**Files:** +- Modify: `.github/workflows/build.yml` +- Modify: `.github/workflows/android.yml` + +**Step 1: Fix build.yml (develop branch)** + +Replace `.github/workflows/build.yml` with: + +```yaml +name: Build & Test + +on: + push: + branches: [ "develop" ] + workflow_dispatch: + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Build libraries + run: ./gradlew :vbpd-core:assembleRelease :vbpd:assembleRelease :vbpd-reflection:assembleRelease + + - name: Run tests + run: ./gradlew testDebugUnitTest + + - name: Run code quality checks + run: ./gradlew detekt ktlintCheck + + - name: Generate coverage report + run: ./gradlew koverXmlReport + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: | + **/build/test-results/ + **/build/reports/tests/ + + - name: Upload coverage reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-reports + path: | + **/build/reports/kover/ + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: vbpd-libs-builds + path: | + vbpd-core/build/outputs/aar/*.aar + vbpd/build/outputs/aar/*.aar + vbpd-reflection/build/outputs/aar/*.aar +``` + +**Step 2: Fix android.yml (master branch)** + +Replace `.github/workflows/android.yml` with: + +```yaml +name: Quality Checks + +on: + push: + branches: [ "master" ] + pull_request: + branches: [ "master" ] + +jobs: + check: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + + - name: Set up JDK + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run all checks + run: ./gradlew check + + - name: Generate coverage report + run: ./gradlew koverXmlReport + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v4 + with: + name: test-results + path: | + **/build/test-results/ + **/build/reports/tests/ + + - name: Upload coverage reports + if: always() + uses: actions/upload-artifact@v4 + with: + name: coverage-reports + path: | + **/build/reports/kover/ +``` + +**Step 3: Verify CI configuration is valid YAML** + +Run: `python3 -c "import yaml; yaml.safe_load(open('.github/workflows/build.yml')); yaml.safe_load(open('.github/workflows/android.yml')); print('Valid YAML')"` +Expected: "Valid YAML" (or use any YAML validator) + +**Step 4: Commit** + +```bash +git add .github/workflows/ +git commit -m "Fix CI: remove continue-on-error, add tests, linters and coverage" +``` + +--- + +## Task 13: Final verification + +**Files:** None (verification only) + +**Step 1: Clean build from scratch** + +Run: `./gradlew clean check` +Expected: BUILD SUCCESSFUL + +**Step 2: Verify test count** + +Run: `./gradlew testDebugUnitTest --info 2>&1 | grep -E "tests completed|tests found"` +Expected: Multiple tests found and completed successfully + +**Step 3: Verify coverage** + +Run: `./gradlew koverHtmlReport` +Review HTML reports. + +**Step 4: Final commit if any fixes needed** + +```bash +git add -A +git commit -m "Final verification: all checks passing" +``` From 7cde1a41232253f195a4ea2e18dfa75442d2d2a0 Mon Sep 17 00:00:00 2001 From: kirich1409 Date: Sat, 14 Feb 2026 21:38:49 +0300 Subject: [PATCH 3/5] Add test coverage, static analysis (Detekt, ktlint), code coverage (Kover), and fix CI - Add Detekt, ktlint, and Kover to convention plugin for all library modules - Add unit tests for vbpd-core (LazyViewBindingProperty, EagerViewBindingProperty) - Add lifecycle tests for vbpd (Activity, Fragment, ViewGroup, ViewHolder) - Add tests for vbpd-reflection (ViewBindingCache, CreateMethod) - Fix minor code style issues (double spaces, extra blank lines) - Update CI pipelines: remove continue-on-error, add test/lint/coverage steps - Add Detekt configuration file (config/detekt/detekt.yml) Co-Authored-By: Claude Opus 4.6 --- .github/workflows/android.yml | 39 ++++++---- .github/workflows/build.yml | 57 ++++++++------ config/detekt/detekt.yml | 29 +++++++ .../vbpd-library-base/build.gradle.kts | 3 + .../src/main/kotlin/vbpdconfig.gradle.kts | 15 ++++ gradle/libs.versions.toml | 22 ++++++ vbpd-core/build.gradle.kts | 6 ++ .../vbpd/EagerViewBindingPropertyTest.kt | 41 ++++++++++ .../vbpd/LazyViewBindingPropertyTest.kt | 76 +++++++++++++++++++ vbpd-reflection/build.gradle.kts | 11 +++ .../vbpd/ActivityViewBindings.kt | 4 +- .../vbpd/ViewBindingPropertyDelegate.kt | 1 - .../ReflectionActivityViewBindingsTest.kt | 21 +++++ .../vbpd/ViewBindingCacheTest.kt | 42 ++++++++++ vbpd/build.gradle.kts | 12 +++ .../vbpd/internal/VbpdUtils.kt | 1 - .../vbpd/ActivityViewBindingPropertyTest.kt | 57 ++++++++++++++ .../vbpd/FragmentViewBindingPropertyTest.kt | 54 +++++++++++++ .../vbpd/ViewGroupBindingsTest.kt | 46 +++++++++++ .../vbpd/ViewHolderBindingsTest.kt | 53 +++++++++++++ 20 files changed, 548 insertions(+), 42 deletions(-) create mode 100644 config/detekt/detekt.yml create mode 100644 vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/EagerViewBindingPropertyTest.kt create mode 100644 vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/LazyViewBindingPropertyTest.kt create mode 100644 vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ReflectionActivityViewBindingsTest.kt create mode 100644 vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ViewBindingCacheTest.kt create mode 100644 vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindingPropertyTest.kt create mode 100644 vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindingPropertyTest.kt create mode 100644 vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindingsTest.kt create mode 100644 vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindingsTest.kt diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index 924569d..691e7a9 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -1,4 +1,4 @@ -name: Android quality checks +name: Quality Checks on: push: @@ -8,12 +8,12 @@ on: jobs: check: - runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - name: set up JDK + + - name: Set up JDK uses: actions/setup-java@v5 with: java-version: '21' @@ -22,19 +22,26 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - - - name: Check build + + - name: Run all checks run: ./gradlew check - continue-on-error: true - - - name: Save Gradle cache - uses: actions/cache@v5 - continue-on-error: true + + - name: Generate coverage report + run: ./gradlew koverXmlReport + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v6 + with: + name: test-results + path: | + **/build/test-results/ + **/build/reports/tests/ + + - name: Upload coverage reports + if: always() + uses: actions/upload-artifact@v6 with: + name: coverage-reports path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- - + **/build/reports/kover/ diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6d7247b..bd6cfd6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -1,4 +1,4 @@ -name: Build libs +name: Build & Test on: push: @@ -7,12 +7,12 @@ on: jobs: check: - runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 - - name: set up JDK + + - name: Set up JDK uses: actions/setup-java@v5 with: java-version: '21' @@ -21,28 +21,41 @@ jobs: - name: Grant execute permission for gradlew run: chmod +x gradlew - - - name: Check build - run: ./gradlew :vbpd-core:assembleRelease :vbpd:assembleRelease :vbpd-reflection:assembleRelease --no-configuration-cache --no-build-cache - continue-on-error: true - - - name: Save Gradle cache - uses: actions/cache@v5 - continue-on-error: true + + - name: Build libraries + run: ./gradlew :vbpd-core:assembleRelease :vbpd:assembleRelease :vbpd-reflection:assembleRelease + + - name: Run tests + run: ./gradlew testDebugUnitTest + + - name: Run code quality checks + run: ./gradlew detekt ktlintCheck + + - name: Generate coverage report + run: ./gradlew koverXmlReport + + - name: Upload test results + if: always() + uses: actions/upload-artifact@v6 + with: + name: test-results + path: | + **/build/test-results/ + **/build/reports/tests/ + + - name: Upload coverage reports + if: always() + uses: actions/upload-artifact@v6 with: + name: coverage-reports path: | - ~/.gradle/caches - ~/.gradle/wrapper - key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} - restore-keys: | - ${{ runner.os }}-gradle- + **/build/reports/kover/ - - name: Save build artifacts + - name: Upload build artifacts uses: actions/upload-artifact@v6 with: name: vbpd-libs-builds - path: - vbpd/build/outputs/aar/vbpd-release.aar - vbpd-core/build/outputs/aar/vbpd-core-release.aar - vbpd-reflection/build/outputs/aar/vbpd-reflection-release.aar - + path: | + vbpd-core/build/outputs/aar/*.aar + vbpd/build/outputs/aar/*.aar + vbpd-reflection/build/outputs/aar/*.aar diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml new file mode 100644 index 0000000..bea2c36 --- /dev/null +++ b/config/detekt/detekt.yml @@ -0,0 +1,29 @@ +build: + maxIssues: 0 + +complexity: + LongMethod: + threshold: 60 + LongParameterList: + functionThreshold: 8 + constructorThreshold: 8 + TooManyFunctions: + thresholdInFiles: 20 + thresholdInClasses: 15 + thresholdInInterfaces: 10 + +style: + MaxLineLength: + maxLineLength: 120 + WildcardImport: + active: false + MagicNumber: + active: false + ReturnCount: + max: 3 + UnusedPrivateMember: + active: true + +naming: + FunctionNaming: + functionPattern: '[a-zA-Z][a-zA-Z0-9]*' diff --git a/gradle/conventions-plugins/vbpd-library-base/build.gradle.kts b/gradle/conventions-plugins/vbpd-library-base/build.gradle.kts index f110a57..9860a15 100644 --- a/gradle/conventions-plugins/vbpd-library-base/build.gradle.kts +++ b/gradle/conventions-plugins/vbpd-library-base/build.gradle.kts @@ -9,6 +9,9 @@ dependencies { implementation(libs.gradleplugins.android) implementation(libs.gradleplugins.kotlin) implementation(libs.gradleplugins.vanniktechMavenPublish) + implementation(libs.gradleplugins.detekt) + implementation(libs.gradleplugins.ktlint) + implementation(libs.gradleplugins.kover) // Workaround for version catalog working inside precompiled scripts // Issue - https://github.com/gradle/gradle/issues/15383 diff --git a/gradle/conventions-plugins/vbpd-library-base/src/main/kotlin/vbpdconfig.gradle.kts b/gradle/conventions-plugins/vbpd-library-base/src/main/kotlin/vbpdconfig.gradle.kts index 435778e..d2dcbd9 100644 --- a/gradle/conventions-plugins/vbpd-library-base/src/main/kotlin/vbpdconfig.gradle.kts +++ b/gradle/conventions-plugins/vbpd-library-base/src/main/kotlin/vbpdconfig.gradle.kts @@ -11,6 +11,9 @@ plugins.apply(libs.plugins.android.library.get().pluginId) plugins.apply("maven-publish") plugins.apply(libs.plugins.vanniktechMavenPublish.get().pluginId) plugins.apply("vbpdpublish") +plugins.apply(libs.plugins.detekt.get().pluginId) +plugins.apply(libs.plugins.ktlint.get().pluginId) +plugins.apply(libs.plugins.kover.get().pluginId) val libraryId = "${project.group}.${project.name.replace("vbpd-", "").replace("-", ".")}" @@ -43,9 +46,21 @@ androidLibraryConfig { jvmTarget = libs.versions.jvmTarget.get() freeCompilerArgs += listOf("-module-name", libraryId) } + + testOptions { + unitTests { + isIncludeAndroidResources = true + } + } } kotlinConfig { jvmToolchain(libs.versions.jvmTarget.get().toInt()) explicitApi() } + +extensions.configure { + config.setFrom(rootProject.files("config/detekt/detekt.yml")) + buildUponDefaultConfig = true + parallel = true +} diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 7237025..7fb699b 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -14,6 +14,14 @@ kotlin = "2.1.10" vanniktechMavenPublish = "0.30.0" jvmTarget = "11" vbpd = "2.0.4" +detekt = "1.23.8" +ktlint = "14.0.1" +kover = "0.9.7" +robolectric = "4.16.1" +mockk = "1.14.9" +androidx-test-core = "1.6.1" +androidx-test-runner = "1.6.2" +junit = "4.13.2" [libraries] androidx-activity = { module = "androidx.activity:activity", version.ref = "androidx-activity" } @@ -27,6 +35,17 @@ androidx-viewbinding = { module = "androidx.databinding:viewbinding", version.re gradleplugins-android = { module = "com.android.tools.build:gradle", version.ref = "agp" } gradleplugins-kotlin = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "kotlin" } gradleplugins-vanniktechMavenPublish = { module = "com.vanniktech.maven.publish:com.vanniktech.maven.publish.gradle.plugin", version.ref = "vanniktechMavenPublish" } +gradleplugins-detekt = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "detekt" } +gradleplugins-ktlint = { module = "org.jlleitschuh.gradle:ktlint-gradle", version.ref = "ktlint" } +gradleplugins-kover = { module = "org.jetbrains.kotlinx:kover-gradle-plugin", version.ref = "kover" } + +test-robolectric = { module = "org.robolectric:robolectric", version.ref = "robolectric" } +test-mockk = { module = "io.mockk:mockk", version.ref = "mockk" } +test-junit = { module = "junit:junit", version.ref = "junit" } +test-kotlin = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +test-androidx-core = { module = "androidx.test:core-ktx", version.ref = "androidx-test-core" } +test-androidx-runner = { module = "androidx.test:runner", version.ref = "androidx-test-runner" } +test-fragment = { module = "androidx.fragment:fragment-testing", version.ref = "androidx-fragment" } [plugins] android-application = { id = "com.android.application", version.ref = "agp" } @@ -34,3 +53,6 @@ android-library = { id = "com.android.library", version.ref = "agp" } jetbrains-kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } vanniktechMavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "vanniktechMavenPublish" } parcelize = { id = "org.jetbrains.kotlin.plugin.parcelize", version.ref = "kotlin" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +ktlint = { id = "org.jlleitschuh.gradle.ktlint", version.ref = "ktlint" } +kover = { id = "org.jetbrains.kotlinx.kover", version.ref = "kover" } diff --git a/vbpd-core/build.gradle.kts b/vbpd-core/build.gradle.kts index 32d928d..abd5efb 100644 --- a/vbpd-core/build.gradle.kts +++ b/vbpd-core/build.gradle.kts @@ -5,4 +5,10 @@ plugins { dependencies { api(libs.androidx.viewbinding) implementation(libs.androidx.annotation) + + testImplementation(libs.test.junit) + testImplementation(libs.test.kotlin) + testImplementation(libs.test.mockk) + testImplementation(libs.test.robolectric) + testImplementation(libs.test.androidx.core) } diff --git a/vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/EagerViewBindingPropertyTest.kt b/vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/EagerViewBindingPropertyTest.kt new file mode 100644 index 0000000..2bbaf73 --- /dev/null +++ b/vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/EagerViewBindingPropertyTest.kt @@ -0,0 +1,41 @@ +package dev.androidbroadcast.vbpd + +import android.view.View +import androidx.viewbinding.ViewBinding +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertEquals + +@RunWith(RobolectricTestRunner::class) +class EagerViewBindingPropertyTest { + + private fun createMockBinding(): ViewBinding { + val view = mockk() + return mockk { + every { root } returns view + } + } + + @Test + fun `getValue returns the provided binding`() { + val expectedBinding = createMockBinding() + val property = EagerViewBindingProperty(expectedBinding) + + val result = property.getValue(Any(), ::result) + assertEquals(expectedBinding, result) + } + + @Test + fun `getValue always returns same instance`() { + val binding = createMockBinding() + val property = EagerViewBindingProperty(binding) + val thisRef = Any() + + val first = property.getValue(thisRef, ::first) + val second = property.getValue(thisRef, ::second) + assertEquals(first, second) + } +} diff --git a/vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/LazyViewBindingPropertyTest.kt b/vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/LazyViewBindingPropertyTest.kt new file mode 100644 index 0000000..3958e22 --- /dev/null +++ b/vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/LazyViewBindingPropertyTest.kt @@ -0,0 +1,76 @@ +package dev.androidbroadcast.vbpd + +import android.view.View +import androidx.viewbinding.ViewBinding +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertEquals + +@RunWith(RobolectricTestRunner::class) +class LazyViewBindingPropertyTest { + + private fun createMockBinding(): ViewBinding { + val view = mockk() + return mockk { + every { root } returns view + } + } + + @Test + fun `getValue returns binding created by viewBinder`() { + val expectedBinding = createMockBinding() + val property = LazyViewBindingProperty { expectedBinding } + val thisRef = Any() + + val result = property.getValue(thisRef, ::result) + assertEquals(expectedBinding, result) + } + + @Test + fun `getValue returns same instance on subsequent calls`() { + var callCount = 0 + val binding = createMockBinding() + val property = LazyViewBindingProperty { + callCount++ + binding + } + val thisRef = Any() + + val first = property.getValue(thisRef, ::first) + val second = property.getValue(thisRef, ::second) + + assertEquals(first, second) + assertEquals(1, callCount, "viewBinder should be called only once") + } + + @Test + fun `clear resets cached binding`() { + var callCount = 0 + val property = LazyViewBindingProperty { + callCount++ + createMockBinding() + } + val thisRef = Any() + + property.getValue(thisRef, ::thisRef) + property.clear() + property.getValue(thisRef, ::thisRef) + + assertEquals(2, callCount, "viewBinder should be called again after clear()") + } + + @Test + fun `viewBinder receives correct thisRef`() { + var receivedRef: Any? = null + val property = LazyViewBindingProperty { ref -> + receivedRef = ref + createMockBinding() + } + + property.getValue("test", ::receivedRef) + assertEquals("test", receivedRef) + } +} diff --git a/vbpd-reflection/build.gradle.kts b/vbpd-reflection/build.gradle.kts index 43e668b..95c4281 100644 --- a/vbpd-reflection/build.gradle.kts +++ b/vbpd-reflection/build.gradle.kts @@ -18,4 +18,15 @@ dependencies { compileOnly(libs.androidx.recyclerview) compileOnly(libs.androidx.activity) api(projects.vbpd) + + testImplementation(libs.test.junit) + testImplementation(libs.test.kotlin) + testImplementation(libs.test.mockk) + testImplementation(libs.test.robolectric) + testImplementation(libs.test.androidx.core) + testImplementation(libs.test.androidx.runner) + testImplementation(libs.androidx.fragment) + testImplementation(libs.androidx.activity) + testImplementation(libs.androidx.recyclerview) + testImplementation(libs.androidx.appcompat) } diff --git a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindings.kt b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindings.kt index 9debab9..ee2a5b3 100644 --- a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindings.kt +++ b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindings.kt @@ -1,13 +1,13 @@ @file:JvmName("ReflectionActivityViewBindings") -package dev.androidbroadcast.vbpd +package dev.androidbroadcast.vbpd import android.app.Activity import android.view.View import androidx.annotation.IdRes import androidx.core.app.ActivityCompat import androidx.viewbinding.ViewBinding -import dev.androidbroadcast.vbpd.internal.findRootView +import dev.androidbroadcast.vbpd.internal.findRootView /** * Create new [ViewBinding] associated with the [Activity] diff --git a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingPropertyDelegate.kt b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingPropertyDelegate.kt index 79cf98c..27e4015 100644 --- a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingPropertyDelegate.kt +++ b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingPropertyDelegate.kt @@ -1,6 +1,5 @@ @file:JvmName("ViewBindingPropertyDelegateUtilsRef") - package dev.androidbroadcast.vbpd import android.view.LayoutInflater diff --git a/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ReflectionActivityViewBindingsTest.kt b/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ReflectionActivityViewBindingsTest.kt new file mode 100644 index 0000000..1c2690a --- /dev/null +++ b/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ReflectionActivityViewBindingsTest.kt @@ -0,0 +1,21 @@ +package dev.androidbroadcast.vbpd + +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertNotNull + +@RunWith(RobolectricTestRunner::class) +class ReflectionActivityViewBindingsTest { + + @Test + fun `CreateMethod BIND and INFLATE enum values exist`() { + assertNotNull(CreateMethod.BIND) + assertNotNull(CreateMethod.INFLATE) + } + + @Test + fun `CreateMethod values are distinct`() { + assert(CreateMethod.BIND != CreateMethod.INFLATE) + } +} diff --git a/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ViewBindingCacheTest.kt b/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ViewBindingCacheTest.kt new file mode 100644 index 0000000..6df9f6c --- /dev/null +++ b/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ViewBindingCacheTest.kt @@ -0,0 +1,42 @@ +package dev.androidbroadcast.vbpd + +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner + +@RunWith(RobolectricTestRunner::class) +class ViewBindingCacheTest { + + @Before + fun setup() { + ViewBindingCache.clear() + } + + @After + fun tearDown() { + ViewBindingCache.clear() + ViewBindingCache.setEnabled(false) + } + + @Test + fun `cache can be enabled and disabled`() { + ViewBindingCache.setEnabled(true) + ViewBindingCache.setEnabled(false) + } + + @Test + fun `clear does not crash when cache is empty`() { + ViewBindingCache.clear() + ViewBindingCache.setEnabled(true) + ViewBindingCache.clear() + } + + @Test + fun `setEnabled true then false resets cache`() { + ViewBindingCache.setEnabled(true) + ViewBindingCache.setEnabled(false) + ViewBindingCache.clear() + } +} diff --git a/vbpd/build.gradle.kts b/vbpd/build.gradle.kts index 99b940e..aac628e 100644 --- a/vbpd/build.gradle.kts +++ b/vbpd/build.gradle.kts @@ -9,4 +9,16 @@ dependencies { compileOnly(libs.androidx.activity) compileOnly(libs.androidx.recyclerview) api(projects.vbpdCore) + + testImplementation(libs.test.junit) + testImplementation(libs.test.kotlin) + testImplementation(libs.test.mockk) + testImplementation(libs.test.robolectric) + testImplementation(libs.test.androidx.core) + testImplementation(libs.test.androidx.runner) + testImplementation(libs.test.fragment) + testImplementation(libs.androidx.fragment) + testImplementation(libs.androidx.activity) + testImplementation(libs.androidx.recyclerview) + testImplementation(libs.androidx.appcompat) } diff --git a/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/internal/VbpdUtils.kt b/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/internal/VbpdUtils.kt index db98b19..04a7209 100644 --- a/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/internal/VbpdUtils.kt +++ b/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/internal/VbpdUtils.kt @@ -14,7 +14,6 @@ import java.lang.ref.WeakReference import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty - @RestrictTo(RestrictTo.Scope.LIBRARY) public fun View.requireViewByIdCompat(@IdRes id: Int): V { return ViewCompat.requireViewById(this, id) diff --git a/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindingPropertyTest.kt b/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindingPropertyTest.kt new file mode 100644 index 0000000..39fe839 --- /dev/null +++ b/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindingPropertyTest.kt @@ -0,0 +1,57 @@ +package dev.androidbroadcast.vbpd + +import android.app.Activity +import android.os.Bundle +import android.view.View +import android.widget.FrameLayout +import androidx.viewbinding.ViewBinding +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.Robolectric +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertNotNull + +@RunWith(RobolectricTestRunner::class) +class ActivityViewBindingPropertyTest { + + class TestActivity : Activity() { + val binding by viewBinding { activity: TestActivity -> + mockk { + every { root } returns View(activity) + } + } + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + val content = FrameLayout(this) + content.id = android.R.id.content + setContentView(content) + } + } + + @Test + fun `binding is accessible after onCreate`() { + val controller = Robolectric.buildActivity(TestActivity::class.java) + .create() + .start() + .resume() + + val activity = controller.get() + assertNotNull(activity.binding) + } + + @Test + fun `binding is cleared after onDestroy`() { + val controller = Robolectric.buildActivity(TestActivity::class.java) + .create() + .start() + .resume() + + val activity = controller.get() + assertNotNull(activity.binding) + + controller.pause().stop().destroy() + } +} diff --git a/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindingPropertyTest.kt b/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindingPropertyTest.kt new file mode 100644 index 0000000..f8d2a73 --- /dev/null +++ b/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindingPropertyTest.kt @@ -0,0 +1,54 @@ +package dev.androidbroadcast.vbpd + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.FrameLayout +import androidx.fragment.app.Fragment +import androidx.fragment.app.testing.launchFragmentInContainer +import androidx.lifecycle.Lifecycle +import androidx.viewbinding.ViewBinding +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertNotNull + +@RunWith(RobolectricTestRunner::class) +class FragmentViewBindingPropertyTest { + + class TestFragment : Fragment() { + val binding by viewBinding { fragment: TestFragment -> + mockk { + every { root } returns fragment.requireView() + } + } + + override fun onCreateView( + inflater: LayoutInflater, + container: ViewGroup?, + savedInstanceState: Bundle? + ): View { + return FrameLayout(requireContext()) + } + } + + @Test + fun `binding is accessible after onViewCreated`() { + val scenario = launchFragmentInContainer() + scenario.onFragment { fragment -> + assertNotNull(fragment.binding) + } + } + + @Test + fun `binding survives configuration change lifecycle`() { + val scenario = launchFragmentInContainer() + scenario.onFragment { fragment -> + assertNotNull(fragment.binding) + } + scenario.moveToState(Lifecycle.State.DESTROYED) + } +} diff --git a/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindingsTest.kt b/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindingsTest.kt new file mode 100644 index 0000000..243a0ec --- /dev/null +++ b/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindingsTest.kt @@ -0,0 +1,46 @@ +package dev.androidbroadcast.vbpd + +import android.content.Context +import android.view.View +import android.widget.FrameLayout +import androidx.test.core.app.ApplicationProvider +import androidx.viewbinding.ViewBinding +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertEquals + +@RunWith(RobolectricTestRunner::class) +class ViewGroupBindingsTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + @Test + fun `viewBinding creates lazy binding for ViewGroup`() { + val viewGroup = FrameLayout(context) + val expectedBinding = mockk { + every { root } returns View(context) + } + + val property = viewGroup.viewBinding { _: FrameLayout -> expectedBinding } + val result = property.getValue(viewGroup, ::result) + + assertEquals(expectedBinding, result) + } + + @Test + fun `viewBinding returns same instance on subsequent calls`() { + val viewGroup = FrameLayout(context) + val binding = mockk { + every { root } returns View(context) + } + + val property = viewGroup.viewBinding { _: FrameLayout -> binding } + val first = property.getValue(viewGroup, ::first) + val second = property.getValue(viewGroup, ::second) + + assertEquals(first, second) + } +} diff --git a/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindingsTest.kt b/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindingsTest.kt new file mode 100644 index 0000000..293ae1e --- /dev/null +++ b/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindingsTest.kt @@ -0,0 +1,53 @@ +package dev.androidbroadcast.vbpd + +import android.content.Context +import android.view.View +import android.widget.FrameLayout +import androidx.recyclerview.widget.RecyclerView +import androidx.test.core.app.ApplicationProvider +import androidx.viewbinding.ViewBinding +import io.mockk.every +import io.mockk.mockk +import org.junit.Test +import org.junit.runner.RunWith +import org.robolectric.RobolectricTestRunner +import kotlin.test.assertEquals + +@RunWith(RobolectricTestRunner::class) +class ViewHolderBindingsTest { + + private val context: Context = ApplicationProvider.getApplicationContext() + + class TestViewHolder(view: View) : RecyclerView.ViewHolder(view) + + @Test + fun `viewBinding creates binding from ViewHolder`() { + val view = FrameLayout(context) + val viewHolder = TestViewHolder(view) + val expectedBinding = mockk { + every { root } returns view + } + + val property = viewHolder.viewBinding { _: TestViewHolder -> expectedBinding } + val result = property.getValue(viewHolder, ::result) + + assertEquals(expectedBinding, result) + } + + @Test + fun `viewBinding with factory and viewProvider`() { + val view = FrameLayout(context) + val viewHolder = TestViewHolder(view) + val expectedBinding = mockk { + every { root } returns view + } + + val property = viewHolder.viewBinding( + vbFactory = { expectedBinding }, + viewProvider = { it.itemView } + ) + val result = property.getValue(viewHolder, ::result) + + assertEquals(expectedBinding, result) + } +} From fb899bcdf4fc1c028fcbd50c5ae75cecf68372d0 Mon Sep 17 00:00:00 2001 From: kirich1409 Date: Sat, 14 Feb 2026 21:52:37 +0300 Subject: [PATCH 4/5] Fix compilation errors, detekt/ktlint conflicts, and apply code formatting - Fix test compilation: use mockk> instead of invalid ::localVar references - Tune detekt config: ignore overridden empty blocks, disable MaxLineLength (ktlint handles it), disable MatchingDeclarationName and MemberNameEqualsClassName for existing API - Apply ktlint auto-formatting across all modules (trailing commas, body expressions, etc.) - All checks pass: detekt, ktlint, unit tests, kover, lint Co-Authored-By: Claude Opus 4.6 --- config/detekt/detekt.yml | 23 ++++- .../vbpd/ViewBindingProperty.kt | 7 +- .../vbpd/EagerViewBindingPropertyTest.kt | 8 +- .../vbpd/LazyViewBindingPropertyTest.kt | 41 ++++---- .../vbpd/ActivityViewBindings.kt | 38 ++++---- .../dev/androidbroadcast/vbpd/CreateMethod.kt | 2 +- .../vbpd/FragmentViewBindings.kt | 51 +++++----- .../androidbroadcast/vbpd/ViewBindingCache.kt | 97 +++++++------------ .../vbpd/ViewBindingPropertyDelegate.kt | 8 +- .../vbpd/ViewGroupBindings.kt | 33 +++---- .../vbpd/ViewHolderBindings.kt | 11 +-- .../ReflectionActivityViewBindingsTest.kt | 1 - .../vbpd/ViewBindingCacheTest.kt | 1 - .../vbpd/ActivityViewBindings.kt | 37 +++---- .../vbpd/FragmentViewBindings.kt | 45 ++++----- .../vbpd/ViewGroupBindings.kt | 27 +++--- .../vbpd/ViewHolderBindings.kt | 17 ++-- .../vbpd/internal/VbpdUtils.kt | 42 ++++---- .../vbpd/ActivityViewBindingPropertyTest.kt | 21 ++-- .../vbpd/FragmentViewBindingPropertyTest.kt | 7 +- .../vbpd/ViewGroupBindingsTest.kt | 23 +++-- .../vbpd/ViewHolderBindingsTest.kt | 36 ++++--- 22 files changed, 278 insertions(+), 298 deletions(-) diff --git a/config/detekt/detekt.yml b/config/detekt/detekt.yml index bea2c36..672912c 100644 --- a/config/detekt/detekt.yml +++ b/config/detekt/detekt.yml @@ -12,9 +12,26 @@ complexity: thresholdInClasses: 15 thresholdInInterfaces: 10 +empty-blocks: + EmptyFunctionBlock: + ignoreOverridden: true + +exceptions: + SwallowedException: + ignoredExceptionTypes: + - NoSuchMethodException + +naming: + FunctionNaming: + functionPattern: '[a-zA-Z][a-zA-Z0-9]*' + MatchingDeclarationName: + active: false + MemberNameEqualsClassName: + active: false + style: MaxLineLength: - maxLineLength: 120 + active: false WildcardImport: active: false MagicNumber: @@ -23,7 +40,3 @@ style: max: 3 UnusedPrivateMember: active: true - -naming: - FunctionNaming: - functionPattern: '[a-zA-Z][a-zA-Z0-9]*' diff --git a/vbpd-core/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingProperty.kt b/vbpd-core/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingProperty.kt index 5e4d9fe..f99f726 100644 --- a/vbpd-core/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingProperty.kt +++ b/vbpd-core/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingProperty.kt @@ -10,7 +10,6 @@ import kotlin.reflect.KProperty * Base ViewBindingProperty interface that provides access to operations in the property delegate. */ public interface ViewBindingProperty : ReadOnlyProperty { - /** * Clear all cached data. Will be called when own object destroys view */ @@ -26,7 +25,6 @@ public interface ViewBindingProperty : ReadOnly public open class EagerViewBindingProperty( private val viewBinding: T, ) : ViewBindingProperty { - public override fun getValue( thisRef: R, property: KProperty<*>, @@ -40,15 +38,12 @@ public open class EagerViewBindingProperty( public open class LazyViewBindingProperty( private val viewBinder: (R) -> T, ) : ViewBindingProperty { - private var viewBinding: T? = null public override fun getValue( thisRef: R, property: KProperty<*>, - ): T { - return viewBinding ?: viewBinder(thisRef).also { viewBinding = it } - } + ): T = viewBinding ?: viewBinder(thisRef).also { viewBinding = it } @CallSuper public override fun clear() { diff --git a/vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/EagerViewBindingPropertyTest.kt b/vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/EagerViewBindingPropertyTest.kt index 2bbaf73..bfda4a9 100644 --- a/vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/EagerViewBindingPropertyTest.kt +++ b/vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/EagerViewBindingPropertyTest.kt @@ -7,10 +7,12 @@ import io.mockk.mockk import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import kotlin.reflect.KProperty import kotlin.test.assertEquals @RunWith(RobolectricTestRunner::class) class EagerViewBindingPropertyTest { + private val mockProperty = mockk>(relaxed = true) private fun createMockBinding(): ViewBinding { val view = mockk() @@ -24,7 +26,7 @@ class EagerViewBindingPropertyTest { val expectedBinding = createMockBinding() val property = EagerViewBindingProperty(expectedBinding) - val result = property.getValue(Any(), ::result) + val result = property.getValue(Any(), mockProperty) assertEquals(expectedBinding, result) } @@ -34,8 +36,8 @@ class EagerViewBindingPropertyTest { val property = EagerViewBindingProperty(binding) val thisRef = Any() - val first = property.getValue(thisRef, ::first) - val second = property.getValue(thisRef, ::second) + val first = property.getValue(thisRef, mockProperty) + val second = property.getValue(thisRef, mockProperty) assertEquals(first, second) } } diff --git a/vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/LazyViewBindingPropertyTest.kt b/vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/LazyViewBindingPropertyTest.kt index 3958e22..16c59dc 100644 --- a/vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/LazyViewBindingPropertyTest.kt +++ b/vbpd-core/src/test/kotlin/dev/androidbroadcast/vbpd/LazyViewBindingPropertyTest.kt @@ -7,10 +7,12 @@ import io.mockk.mockk import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import kotlin.reflect.KProperty import kotlin.test.assertEquals @RunWith(RobolectricTestRunner::class) class LazyViewBindingPropertyTest { + private val mockProperty = mockk>(relaxed = true) private fun createMockBinding(): ViewBinding { val view = mockk() @@ -25,7 +27,7 @@ class LazyViewBindingPropertyTest { val property = LazyViewBindingProperty { expectedBinding } val thisRef = Any() - val result = property.getValue(thisRef, ::result) + val result = property.getValue(thisRef, mockProperty) assertEquals(expectedBinding, result) } @@ -33,14 +35,15 @@ class LazyViewBindingPropertyTest { fun `getValue returns same instance on subsequent calls`() { var callCount = 0 val binding = createMockBinding() - val property = LazyViewBindingProperty { - callCount++ - binding - } + val property = + LazyViewBindingProperty { + callCount++ + binding + } val thisRef = Any() - val first = property.getValue(thisRef, ::first) - val second = property.getValue(thisRef, ::second) + val first = property.getValue(thisRef, mockProperty) + val second = property.getValue(thisRef, mockProperty) assertEquals(first, second) assertEquals(1, callCount, "viewBinder should be called only once") @@ -49,15 +52,16 @@ class LazyViewBindingPropertyTest { @Test fun `clear resets cached binding`() { var callCount = 0 - val property = LazyViewBindingProperty { - callCount++ - createMockBinding() - } + val property = + LazyViewBindingProperty { + callCount++ + createMockBinding() + } val thisRef = Any() - property.getValue(thisRef, ::thisRef) + property.getValue(thisRef, mockProperty) property.clear() - property.getValue(thisRef, ::thisRef) + property.getValue(thisRef, mockProperty) assertEquals(2, callCount, "viewBinder should be called again after clear()") } @@ -65,12 +69,13 @@ class LazyViewBindingPropertyTest { @Test fun `viewBinder receives correct thisRef`() { var receivedRef: Any? = null - val property = LazyViewBindingProperty { ref -> - receivedRef = ref - createMockBinding() - } + val property = + LazyViewBindingProperty { ref -> + receivedRef = ref + createMockBinding() + } - property.getValue("test", ::receivedRef) + property.getValue("test", mockProperty) assertEquals("test", receivedRef) } } diff --git a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindings.kt b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindings.kt index ee2a5b3..f59d487 100644 --- a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindings.kt +++ b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindings.kt @@ -18,9 +18,7 @@ import dev.androidbroadcast.vbpd.internal.findRootView @JvmName("viewBindingActivity") public inline fun Activity.viewBinding( @IdRes viewBindingRootId: Int, -): ViewBindingProperty { - return viewBinding(T::class.java, viewBindingRootId) -} +): ViewBindingProperty = viewBinding(T::class.java, viewBindingRootId) /** * Create new [ViewBinding] associated with the [Activity] @@ -32,14 +30,13 @@ public inline fun Activity.viewBinding( public fun Activity.viewBinding( viewBindingClass: Class, @IdRes viewBindingRootId: Int, -): ViewBindingProperty { - return viewBinding( +): ViewBindingProperty = + viewBinding( viewBinder = { activity -> val rootView = ActivityCompat.requireViewById(activity, viewBindingRootId) ViewBindingCache.getBind(viewBindingClass).bind(rootView) - } + }, ) -} /** * Create new [ViewBinding] associated with the [Activity] @@ -51,13 +48,12 @@ public fun Activity.viewBinding( public fun Activity.viewBinding( viewBindingClass: Class, rootViewProvider: (A) -> View, -): ViewBindingProperty { - return viewBinding( +): ViewBindingProperty = + viewBinding( viewBinder = { activity -> ViewBindingCache.getBind(viewBindingClass).bind(rootViewProvider(activity)) - } + }, ) -} /** * Create new [ViewBinding] associated with the [Activity]. @@ -68,20 +64,20 @@ public fun Activity.viewBinding( @JvmName("inflateViewBindingActivity") public inline fun Activity.viewBinding( createMethod: CreateMethod = CreateMethod.BIND, -): ViewBindingProperty { - return viewBinding(T::class.java, createMethod) -} +): ViewBindingProperty = viewBinding(T::class.java, createMethod) @JvmName("inflateViewBindingActivity") public fun Activity.viewBinding( viewBindingClass: Class, createMethod: CreateMethod = CreateMethod.BIND, -): ViewBindingProperty = when (createMethod) { - CreateMethod.BIND -> viewBinding(viewBindingClass, ::findRootView) - CreateMethod.INFLATE -> { - ActivityViewBindingProperty { - ViewBindingCache.getInflateWithLayoutInflater(viewBindingClass) - .inflate(layoutInflater, null, false) +): ViewBindingProperty = + when (createMethod) { + CreateMethod.BIND -> viewBinding(viewBindingClass, ::findRootView) + CreateMethod.INFLATE -> { + ActivityViewBindingProperty { + ViewBindingCache + .getInflateWithLayoutInflater(viewBindingClass) + .inflate(layoutInflater, null, false) + } } } -} diff --git a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/CreateMethod.kt b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/CreateMethod.kt index b8397b6..851356a 100644 --- a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/CreateMethod.kt +++ b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/CreateMethod.kt @@ -14,5 +14,5 @@ public enum class CreateMethod { /** * Use `ViewBinding.inflate(LayoutInflater, ViewGroup, boolean)` */ - INFLATE + INFLATE, } diff --git a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindings.kt b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindings.kt index d587691..b7fc99d 100644 --- a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindings.kt +++ b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindings.kt @@ -19,9 +19,7 @@ import dev.androidbroadcast.vbpd.internal.requireViewByIdCompat @JvmName("viewBindingFragment") public inline fun Fragment.viewBinding( @IdRes viewBindingRootId: Int, -): ViewBindingProperty { - return viewBinding(T::class.java, viewBindingRootId) -} +): ViewBindingProperty = viewBinding(T::class.java, viewBindingRootId) /** * Create new [ViewBinding] associated with the [DialogFragment] @@ -35,12 +33,12 @@ public inline fun Fragment.viewBinding( public fun DialogFragment.viewBinding( viewBindingClass: Class, @IdRes viewBindingRootId: Int, -): ViewBindingProperty { - return viewBinding { dialogFragment: DialogFragment -> - ViewBindingCache.getBind(viewBindingClass) +): ViewBindingProperty = + viewBinding { dialogFragment: DialogFragment -> + ViewBindingCache + .getBind(viewBindingClass) .bind(dialogFragment.findRootView(viewBindingRootId)) } -} /** * Create new [ViewBinding] associated with the [Fragment] @@ -54,12 +52,12 @@ public fun DialogFragment.viewBinding( public fun Fragment.viewBinding( viewBindingClass: Class, @IdRes viewBindingRootId: Int, -): ViewBindingProperty { - return viewBinding { _: Fragment -> - ViewBindingCache.getBind(viewBindingClass) +): ViewBindingProperty = + viewBinding { _: Fragment -> + ViewBindingCache + .getBind(viewBindingClass) .bind(requireView().requireViewByIdCompat(viewBindingRootId)) } -} /** * Create new [ViewBinding] associated with the [Fragment] @@ -71,9 +69,7 @@ public fun Fragment.viewBinding( @JvmName("viewBindingFragment") public inline fun Fragment.viewBinding( createMethod: CreateMethod = CreateMethod.BIND, -): ViewBindingProperty { - return viewBinding(T::class.java, createMethod) -} +): ViewBindingProperty = viewBinding(T::class.java, createMethod) /** * Create new [ViewBinding] associated with the [Fragment] @@ -87,17 +83,20 @@ public inline fun Fragment.viewBinding( public fun Fragment.viewBinding( viewBindingClass: Class, createMethod: CreateMethod = CreateMethod.BIND, -): ViewBindingProperty = when (createMethod) { - CreateMethod.BIND -> fragmentViewBinding { - ViewBindingCache.getBind(viewBindingClass).bind(requireView()) - } - - CreateMethod.INFLATE -> { - fragmentViewBinding( - viewBinder = { - ViewBindingCache.getInflateWithLayoutInflater(viewBindingClass) - .inflate(layoutInflater, null, false) +): ViewBindingProperty = + when (createMethod) { + CreateMethod.BIND -> + fragmentViewBinding { + ViewBindingCache.getBind(viewBindingClass).bind(requireView()) } - ) + + CreateMethod.INFLATE -> { + fragmentViewBinding( + viewBinder = { + ViewBindingCache + .getInflateWithLayoutInflater(viewBindingClass) + .inflate(layoutInflater, null, false) + }, + ) + } } -} diff --git a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingCache.kt b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingCache.kt index acde828..800db0b 100644 --- a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingCache.kt +++ b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingCache.kt @@ -11,39 +11,27 @@ import dev.androidbroadcast.vbpd.InflateViewBinding.Merge import java.lang.reflect.Method private sealed interface ViewBindingCacheImpl { + fun getInflateWithLayoutInflater(viewBindingClass: Class): InflateViewBinding - fun getInflateWithLayoutInflater( - viewBindingClass: Class, - ): InflateViewBinding - - fun getBind( - viewBindingClass: Class, - ): BindViewBinding + fun getBind(viewBindingClass: Class): BindViewBinding fun clear() {} class Default : ViewBindingCacheImpl { - private val inflateCache = mutableMapOf, InflateViewBinding<*>>() private val bindCache = mutableMapOf, BindViewBinding<*>>() @Suppress("UNCHECKED_CAST") - override fun getInflateWithLayoutInflater( - viewBindingClass: Class, - ): InflateViewBinding { - return inflateCache.getOrPut(viewBindingClass) { + override fun getInflateWithLayoutInflater(viewBindingClass: Class): InflateViewBinding = + inflateCache.getOrPut(viewBindingClass) { InflateViewBinding(viewBindingClass) } as InflateViewBinding - } @Suppress("UNCHECKED_CAST") - override fun getBind( - viewBindingClass: Class, - ): BindViewBinding { - return bindCache.getOrPut(viewBindingClass) { + override fun getBind(viewBindingClass: Class): BindViewBinding = + bindCache.getOrPut(viewBindingClass) { BindViewBinding(viewBindingClass) } as BindViewBinding - } /** * Reset all cached data @@ -55,18 +43,10 @@ private sealed interface ViewBindingCacheImpl { } data object Noop : ViewBindingCacheImpl { + override fun getInflateWithLayoutInflater(viewBindingClass: Class): InflateViewBinding = + InflateViewBinding(viewBindingClass) - override fun getInflateWithLayoutInflater( - viewBindingClass: Class, - ): InflateViewBinding { - return InflateViewBinding(viewBindingClass) - } - - override fun getBind( - viewBindingClass: Class, - ): BindViewBinding { - return BindViewBinding(viewBindingClass) - } + override fun getBind(viewBindingClass: Class): BindViewBinding = BindViewBinding(viewBindingClass) } } @@ -74,19 +54,15 @@ private sealed interface ViewBindingCacheImpl { * Cache for ViewBinding.inflate(LayoutInflater, ViewGroup, Boolean) and ViewBinding.bind(View) */ public object ViewBindingCache { - private var impl: ViewBindingCacheImpl = ViewBindingCacheImpl.Noop @RestrictTo(LIBRARY) @PublishedApi - internal fun getInflateWithLayoutInflater( - viewBindingClass: Class, - ): InflateViewBinding = impl.getInflateWithLayoutInflater(viewBindingClass) + internal fun getInflateWithLayoutInflater(viewBindingClass: Class): InflateViewBinding = + impl.getInflateWithLayoutInflater(viewBindingClass) @RestrictTo(LIBRARY) - internal fun getBind( - viewBindingClass: Class, - ): BindViewBinding = impl.getBind(viewBindingClass) + internal fun getBind(viewBindingClass: Class): BindViewBinding = impl.getBind(viewBindingClass) /** * Clear all cached data @@ -107,18 +83,23 @@ public object ViewBindingCache { * Wrapper of ViewBinding.inflate(LayoutInflater, ViewGroup, Boolean) */ @RestrictTo(LIBRARY) -internal fun InflateViewBinding( - viewBindingClass: Class -): InflateViewBinding { +internal fun InflateViewBinding(viewBindingClass: Class): InflateViewBinding { // Depending on XML layout for ViewBinding inflate function with attaching to parent can exist or not try { - return viewBindingClass.getMethod( - "inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java - ).let(::Full) + return viewBindingClass + .getMethod( + "inflate", + LayoutInflater::class.java, + ViewGroup::class.java, + Boolean::class.java, + ).let(::Full) } catch (e: NoSuchMethodException) { - return viewBindingClass.getMethod( - "inflate", LayoutInflater::class.java, ViewGroup::class.java - ).let(::Merge) + return viewBindingClass + .getMethod( + "inflate", + LayoutInflater::class.java, + ViewGroup::class.java, + ).let(::Merge) } } @@ -128,7 +109,6 @@ internal fun InflateViewBinding( @RestrictTo(LIBRARY) @PublishedApi internal sealed interface InflateViewBinding { - fun inflate( layoutInflater: LayoutInflater, parent: ViewGroup?, @@ -137,33 +117,29 @@ internal sealed interface InflateViewBinding { @RestrictTo(LIBRARY) class Full( - private val inflateViewBinding: Method + private val inflateViewBinding: Method, ) : InflateViewBinding { - @Suppress("UNCHECKED_CAST") override fun inflate( layoutInflater: LayoutInflater, parent: ViewGroup?, - attachToParent: Boolean - ): VB { - return inflateViewBinding(null, layoutInflater, parent, attachToParent) as VB - } + attachToParent: Boolean, + ): VB = inflateViewBinding(null, layoutInflater, parent, attachToParent) as VB } @RestrictTo(LIBRARY) class Merge( - private val inflateViewBinding: Method + private val inflateViewBinding: Method, ) : InflateViewBinding { - @Suppress("UNCHECKED_CAST") override fun inflate( layoutInflater: LayoutInflater, parent: ViewGroup?, - attachToParent: Boolean + attachToParent: Boolean, ): VB { require(attachToParent) { "${InflateViewBinding::class.java.simpleName} " + - "supports inflate only with attachToParent=true" + "supports inflate only with attachToParent=true" } return inflateViewBinding(null, layoutInflater, parent) as VB } @@ -174,12 +150,11 @@ internal sealed interface InflateViewBinding { * Wrapper of ViewBinding.bind(View) */ @RestrictTo(LIBRARY) -internal class BindViewBinding(viewBindingClass: Class) { - +internal class BindViewBinding( + viewBindingClass: Class, +) { private val bindViewBinding by lazy { viewBindingClass.getMethod("bind", View::class.java) } @Suppress("UNCHECKED_CAST") - fun bind(view: View): VB { - return bindViewBinding(null, view) as VB - } + fun bind(view: View): VB = bindViewBinding(null, view) as VB } diff --git a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingPropertyDelegate.kt b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingPropertyDelegate.kt index 27e4015..13e143a 100644 --- a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingPropertyDelegate.kt +++ b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewBindingPropertyDelegate.kt @@ -21,9 +21,9 @@ public inline fun viewBindingLazy( layoutInflater: LayoutInflater, parent: ViewGroup? = null, attachToParent: Boolean = parent != null, -): ViewBindingProperty { - return LazyViewBindingProperty { - ViewBindingCache.getInflateWithLayoutInflater(VB::class.java) +): ViewBindingProperty = + LazyViewBindingProperty { + ViewBindingCache + .getInflateWithLayoutInflater(VB::class.java) .inflate(layoutInflater, parent, attachToParent) } -} diff --git a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindings.kt b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindings.kt index 69da103..6b2f8a9 100644 --- a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindings.kt +++ b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindings.kt @@ -17,9 +17,7 @@ import androidx.viewbinding.ViewBinding @JvmName("viewBindingFragment") public inline fun ViewGroup.viewBinding( createMethod: CreateMethod = CreateMethod.BIND, -): ViewBindingProperty { - return viewBinding(T::class.java, createMethod) -} +): ViewBindingProperty = viewBinding(T::class.java, createMethod) /** * Create new [ViewBinding] associated with the [ViewGroup] @@ -34,13 +32,15 @@ public inline fun ViewGroup.viewBinding( public fun ViewGroup.viewBinding( viewBindingClass: Class, createMethod: CreateMethod = CreateMethod.BIND, -): ViewBindingProperty = when (createMethod) { - CreateMethod.BIND -> viewBinding { viewGroup -> - ViewBindingCache.getBind(viewBindingClass).bind(viewGroup) - } +): ViewBindingProperty = + when (createMethod) { + CreateMethod.BIND -> + viewBinding { viewGroup -> + ViewBindingCache.getBind(viewBindingClass).bind(viewGroup) + } - CreateMethod.INFLATE -> viewBinding(viewBindingClass, attachToRoot = true) -} + CreateMethod.INFLATE -> viewBinding(viewBindingClass, attachToRoot = true) + } /** * Inflate new [ViewBinding] with the [ViewGroup][this] as a parent @@ -51,11 +51,8 @@ public fun ViewGroup.viewBinding( * @return [ViewBindingProperty] that holds [ViewBinding] instance */ @JvmName("viewBindingFragment") -public inline fun ViewGroup.viewBinding( - attachToRoot: Boolean = false, -): ViewBindingProperty { - return viewBinding(T::class.java, attachToRoot) -} +public inline fun ViewGroup.viewBinding(attachToRoot: Boolean = false): ViewBindingProperty = + viewBinding(T::class.java, attachToRoot) /** * Inflate new [ViewBinding] with the [ViewGroup][this] as a parent @@ -69,9 +66,9 @@ public inline fun ViewGroup.viewBinding( public fun ViewGroup.viewBinding( viewBindingClass: Class, attachToRoot: Boolean = false, -): ViewBindingProperty { - return viewBinding { viewGroup -> - ViewBindingCache.getInflateWithLayoutInflater(viewBindingClass) +): ViewBindingProperty = + viewBinding { viewGroup -> + ViewBindingCache + .getInflateWithLayoutInflater(viewBindingClass) .inflate(LayoutInflater.from(context), viewGroup, attachToRoot) } -} diff --git a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindings.kt b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindings.kt index 323cb25..8ec4ad9 100644 --- a/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindings.kt +++ b/vbpd-reflection/src/main/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindings.kt @@ -13,9 +13,7 @@ import androidx.viewbinding.ViewBinding * @return [ViewBindingProperty] that holds [ViewBinding] instance */ @JvmName("viewBindingViewHolder") -public inline fun ViewHolder.viewBinding(): ViewBindingProperty { - return viewBinding(T::class.java) -} +public inline fun ViewHolder.viewBinding(): ViewBindingProperty = viewBinding(T::class.java) /** * Create new [ViewBinding] associated with the [ViewHolder] @@ -25,8 +23,5 @@ public inline fun ViewHolder.viewBinding(): ViewBindin * @return [ViewBindingProperty] that holds [ViewBinding] instance */ @JvmName("viewBindingViewHolder") -public fun ViewHolder.viewBinding( - viewBindingClass: Class, -): ViewBindingProperty { - return viewBinding { ViewBindingCache.getBind(viewBindingClass).bind(itemView) } -} +public fun ViewHolder.viewBinding(viewBindingClass: Class): ViewBindingProperty = + viewBinding { ViewBindingCache.getBind(viewBindingClass).bind(itemView) } diff --git a/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ReflectionActivityViewBindingsTest.kt b/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ReflectionActivityViewBindingsTest.kt index 1c2690a..1dff790 100644 --- a/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ReflectionActivityViewBindingsTest.kt +++ b/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ReflectionActivityViewBindingsTest.kt @@ -7,7 +7,6 @@ import kotlin.test.assertNotNull @RunWith(RobolectricTestRunner::class) class ReflectionActivityViewBindingsTest { - @Test fun `CreateMethod BIND and INFLATE enum values exist`() { assertNotNull(CreateMethod.BIND) diff --git a/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ViewBindingCacheTest.kt b/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ViewBindingCacheTest.kt index 6df9f6c..a45955d 100644 --- a/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ViewBindingCacheTest.kt +++ b/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ViewBindingCacheTest.kt @@ -8,7 +8,6 @@ import org.robolectric.RobolectricTestRunner @RunWith(RobolectricTestRunner::class) class ViewBindingCacheTest { - @Before fun setup() { ViewBindingCache.clear() diff --git a/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindings.kt b/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindings.kt index b1957fc..b722ef8 100644 --- a/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindings.kt +++ b/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindings.kt @@ -19,14 +19,16 @@ import kotlin.reflect.KProperty public class ActivityViewBindingProperty( viewBinder: (A) -> T, ) : LazyViewBindingProperty(viewBinder) { - private var lifecycleCallbacks: ActivityLifecycleCallbacks? = null private var activity: Activity? by weakReference(null) - override fun getValue(thisRef: A, property: KProperty<*>): T { - return super.getValue(thisRef, property) + override fun getValue( + thisRef: A, + property: KProperty<*>, + ): T = + super + .getValue(thisRef, property) .also { registerLifecycleCallbacksIfNeeded(thisRef) } - } private fun registerLifecycleCallbacksIfNeeded(activity: Activity) { if (lifecycleCallbacks != null) return @@ -48,8 +50,10 @@ public class ActivityViewBindingProperty( } private inner class VBActivityLifecycleCallbacks : ActivityLifecycleCallbacks { - - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { + override fun onActivityCreated( + activity: Activity, + savedInstanceState: Bundle?, + ) { } override fun onActivityStarted(activity: Activity) { @@ -64,7 +68,10 @@ public class ActivityViewBindingProperty( override fun onActivityStopped(activity: Activity) { } - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) { + override fun onActivitySaveInstanceState( + activity: Activity, + outState: Bundle, + ) { } override fun onActivityDestroyed(activity: Activity) { @@ -83,11 +90,8 @@ public class ActivityViewBindingProperty( */ @JvmName("viewBindingActivityWithCallbacks") @Suppress("UnusedReceiverParameter") -public fun Activity.viewBinding( - viewBinder: (A) -> T, -): ViewBindingProperty { - return ActivityViewBindingProperty(viewBinder = viewBinder) -} +public fun Activity.viewBinding(viewBinder: (A) -> T): ViewBindingProperty = + ActivityViewBindingProperty(viewBinder = viewBinder) /** * Create new [ViewBinding] associated with the [Activity]. @@ -102,9 +106,7 @@ public fun Activity.viewBinding( public inline fun Activity.viewBinding( crossinline vbFactory: (View) -> T, crossinline viewProvider: (A) -> View = ::findRootView, -): ViewBindingProperty { - return viewBinding { activity -> vbFactory(viewProvider(activity)) } -} +): ViewBindingProperty = viewBinding { activity -> vbFactory(viewProvider(activity)) } /** * Create new [ViewBinding] associated with the [Activity][this] and allow customization of how @@ -119,8 +121,7 @@ public inline fun Activity.viewBinding( public inline fun Activity.viewBinding( crossinline vbFactory: (View) -> T, @IdRes viewBindingRootId: Int, -): ViewBindingProperty { - return viewBinding { activity -> +): ViewBindingProperty = + viewBinding { activity -> vbFactory(activity.requireViewByIdCompat(viewBindingRootId)) } -} diff --git a/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindings.kt b/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindings.kt index e3d8a36..533bc37 100644 --- a/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindings.kt +++ b/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindings.kt @@ -17,11 +17,13 @@ import kotlin.reflect.KProperty private class FragmentViewBindingProperty( viewBinder: (F) -> T, ) : LazyViewBindingProperty(viewBinder) { - private var lifecycleCallbacks: FragmentManager.FragmentLifecycleCallbacks? = null private var fragmentManager: FragmentManager? = null - override fun getValue(thisRef: F, property: KProperty<*>): T { + override fun getValue( + thisRef: F, + property: KProperty<*>, + ): T { val viewBinding = super.getValue(thisRef, property) registerLifecycleCallbacksIfNeeded(thisRef) return viewBinding @@ -33,11 +35,13 @@ private class FragmentViewBindingProperty( private fun registerLifecycleCallbacksIfNeeded(fragment: Fragment) { if (lifecycleCallbacks != null) return - val fragmentManager = fragment.parentFragmentManager - .also { fm -> this.fragmentManager = fm } - lifecycleCallbacks = VBFragmentLifecycleCallback(fragment).also { callbacks -> - fragmentManager.registerFragmentLifecycleCallbacks(callbacks, false) - } + val fragmentManager = + fragment.parentFragmentManager + .also { fm -> this.fragmentManager = fm } + lifecycleCallbacks = + VBFragmentLifecycleCallback(fragment).also { callbacks -> + fragmentManager.registerFragmentLifecycleCallbacks(callbacks, false) + } } override fun clear() { @@ -55,7 +59,6 @@ private class FragmentViewBindingProperty( inner class VBFragmentLifecycleCallback( fragment: Fragment, ) : FragmentManager.FragmentLifecycleCallbacks() { - private val fragment by weakReference(fragment) override fun onFragmentViewDestroyed( @@ -72,11 +75,8 @@ private class FragmentViewBindingProperty( */ @Suppress("UnusedReceiverParameter") @JvmName("viewBindingFragmentWithCallbacks") -public fun Fragment.viewBinding( - viewBinder: (F) -> T, -): ViewBindingProperty { - return fragmentViewBinding(viewBinder = viewBinder) -} +public fun Fragment.viewBinding(viewBinder: (F) -> T): ViewBindingProperty = + fragmentViewBinding(viewBinder = viewBinder) /** * Create new [ViewBinding] associated with the [Fragment] @@ -89,9 +89,7 @@ public fun Fragment.viewBinding( public inline fun Fragment.viewBinding( crossinline vbFactory: (View) -> T, crossinline viewProvider: (F) -> View = Fragment::requireView, -): ViewBindingProperty { - return fragmentViewBinding(viewBinder = { fragment -> viewProvider(fragment).let(vbFactory) }) -} +): ViewBindingProperty = fragmentViewBinding(viewBinder = { fragment -> viewProvider(fragment).let(vbFactory) }) /** * Create new [ViewBinding] associated with the [Fragment] @@ -103,8 +101,8 @@ public inline fun Fragment.viewBinding( public inline fun Fragment.viewBinding( crossinline vbFactory: (View) -> T, @IdRes viewBindingRootId: Int, -): ViewBindingProperty { - return when (this) { +): ViewBindingProperty = + when (this) { is DialogFragment -> { fragmentViewBinding { fragment -> (fragment as DialogFragment) @@ -115,17 +113,14 @@ public inline fun Fragment.viewBinding( else -> { fragmentViewBinding { fragment -> - fragment.requireView() + fragment + .requireView() .requireViewByIdCompat(viewBindingRootId) .let(vbFactory) } } } -} @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public fun fragmentViewBinding( - viewBinder: (F) -> T, -): ViewBindingProperty { - return FragmentViewBindingProperty(viewBinder) -} +public fun fragmentViewBinding(viewBinder: (F) -> T): ViewBindingProperty = + FragmentViewBindingProperty(viewBinder) diff --git a/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindings.kt b/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindings.kt index 4271387..9776429 100644 --- a/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindings.kt +++ b/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindings.kt @@ -11,12 +11,11 @@ import dev.androidbroadcast.vbpd.internal.requireViewByIdCompat * * @param vbFactory Function that creates a new instance of [ViewBinding]. `MyViewBinding::bind` can be used */ -public inline fun VG.viewBinding( - crossinline vbFactory: (VG) -> T, -): ViewBindingProperty = when { - isInEditMode -> EagerViewBindingProperty(vbFactory(this)) - else -> LazyViewBindingProperty { viewGroup -> vbFactory(viewGroup) } -} +public inline fun VG.viewBinding(crossinline vbFactory: (VG) -> T): ViewBindingProperty = + when { + isInEditMode -> EagerViewBindingProperty(vbFactory(this)) + else -> LazyViewBindingProperty { viewGroup -> vbFactory(viewGroup) } + } /** * Create new [ViewBinding] associated with the [ViewGroup] @@ -28,9 +27,7 @@ public inline fun VG.viewBinding( public inline fun ViewGroup.viewBinding( crossinline vbFactory: (View) -> T, @IdRes viewBindingRootId: Int, -): ViewBindingProperty { - return viewBinding(viewBindingRootId, vbFactory) -} +): ViewBindingProperty = viewBinding(viewBindingRootId, vbFactory) /** * Create new [ViewBinding] associated with the [ViewGroup] @@ -41,9 +38,11 @@ public inline fun ViewGroup.viewBinding( public inline fun ViewGroup.viewBinding( @IdRes viewBindingRootId: Int, crossinline vbFactory: (View) -> T, -): ViewBindingProperty = when { - isInEditMode -> EagerViewBindingProperty(requireViewByIdCompat(viewBindingRootId)) - else -> LazyViewBindingProperty { viewGroup: ViewGroup -> - vbFactory(viewGroup.requireViewByIdCompat(viewBindingRootId)) +): ViewBindingProperty = + when { + isInEditMode -> EagerViewBindingProperty(requireViewByIdCompat(viewBindingRootId)) + else -> + LazyViewBindingProperty { viewGroup: ViewGroup -> + vbFactory(viewGroup.requireViewByIdCompat(viewBindingRootId)) + } } -} diff --git a/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindings.kt b/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindings.kt index 99f379e..f6b3726 100644 --- a/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindings.kt +++ b/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindings.kt @@ -12,11 +12,8 @@ import dev.androidbroadcast.vbpd.internal.requireViewByIdCompat * Create new [ViewBinding] associated with the [ViewHolder] */ @Suppress("UnusedReceiverParameter") -public fun ViewHolder.viewBinding( - viewBinder: (VH) -> T, -): ViewBindingProperty { - return LazyViewBindingProperty(viewBinder) -} +public fun ViewHolder.viewBinding(viewBinder: (VH) -> T): ViewBindingProperty = + LazyViewBindingProperty(viewBinder) /** * Create new [ViewBinding] associated with the [ViewHolder] @@ -28,11 +25,10 @@ public fun ViewHolder.viewBinding( public inline fun ViewHolder.viewBinding( crossinline vbFactory: (View) -> T, crossinline viewProvider: (VH) -> View = ViewHolder::itemView, -): ViewBindingProperty { - return LazyViewBindingProperty { viewHolder: VH -> +): ViewBindingProperty = + LazyViewBindingProperty { viewHolder: VH -> viewProvider(viewHolder).let(vbFactory) } -} /** * Create new [ViewBinding] associated with the [ViewHolder] @@ -44,8 +40,7 @@ public inline fun ViewHolder.viewBinding( public inline fun ViewHolder.viewBinding( crossinline vbFactory: (View) -> T, @IdRes viewBindingRootId: Int, -): ViewBindingProperty { - return LazyViewBindingProperty { viewHolder: VH -> +): ViewBindingProperty = + LazyViewBindingProperty { viewHolder: VH -> vbFactory(viewHolder.itemView.requireViewByIdCompat(viewBindingRootId)) } -} diff --git a/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/internal/VbpdUtils.kt b/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/internal/VbpdUtils.kt index 04a7209..e8c1562 100644 --- a/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/internal/VbpdUtils.kt +++ b/vbpd/src/main/kotlin/dev/androidbroadcast/vbpd/internal/VbpdUtils.kt @@ -15,14 +15,14 @@ import kotlin.properties.ReadWriteProperty import kotlin.reflect.KProperty @RestrictTo(RestrictTo.Scope.LIBRARY) -public fun View.requireViewByIdCompat(@IdRes id: Int): V { - return ViewCompat.requireViewById(this, id) -} +public fun View.requireViewByIdCompat( + @IdRes id: Int, +): V = ViewCompat.requireViewById(this, id) @RestrictTo(RestrictTo.Scope.LIBRARY) -public fun Activity.requireViewByIdCompat(@IdRes id: Int): V { - return ActivityCompat.requireViewById(this, id) -} +public fun Activity.requireViewByIdCompat( + @IdRes id: Int, +): V = ActivityCompat.requireViewById(this, id) /** * Utility to find root view for ViewBinding in Activity @@ -39,11 +39,14 @@ public fun findRootView(activity: Activity): View { } @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) -public fun DialogFragment.findRootView(@IdRes viewBindingRootId: Int): View { +public fun DialogFragment.findRootView( + @IdRes viewBindingRootId: Int, +): View { if (showsDialog) { - val dialog = checkNotNull(dialog) { - "DialogFragment doesn't have a dialog. Use viewBinding delegate after onCreateDialog" - } + val dialog = + checkNotNull(dialog) { + "DialogFragment doesn't have a dialog. Use viewBinding delegate after onCreateDialog" + } val window = checkNotNull(dialog.window) { "Fragment's Dialog has no window" } return with(window.decorView) { if (viewBindingRootId != 0) requireViewByIdCompat(viewBindingRootId) else this @@ -54,17 +57,20 @@ public fun DialogFragment.findRootView(@IdRes viewBindingRootId: Int): View { } @RestrictTo(RestrictTo.Scope.LIBRARY) -internal fun weakReference(value: T? = null): ReadWriteProperty { - return object : ReadWriteProperty { - +internal fun weakReference(value: T? = null): ReadWriteProperty = + object : ReadWriteProperty { private var weakRef = WeakReference(value) - override fun getValue(thisRef: Any, property: KProperty<*>): T? { - return weakRef.get() - } + override fun getValue( + thisRef: Any, + property: KProperty<*>, + ): T? = weakRef.get() - override fun setValue(thisRef: Any, property: KProperty<*>, value: T?) { + override fun setValue( + thisRef: Any, + property: KProperty<*>, + value: T?, + ) { weakRef = WeakReference(value) } } -} diff --git a/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindingPropertyTest.kt b/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindingPropertyTest.kt index 39fe839..8984dfb 100644 --- a/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindingPropertyTest.kt +++ b/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ActivityViewBindingPropertyTest.kt @@ -15,7 +15,6 @@ import kotlin.test.assertNotNull @RunWith(RobolectricTestRunner::class) class ActivityViewBindingPropertyTest { - class TestActivity : Activity() { val binding by viewBinding { activity: TestActivity -> mockk { @@ -33,10 +32,12 @@ class ActivityViewBindingPropertyTest { @Test fun `binding is accessible after onCreate`() { - val controller = Robolectric.buildActivity(TestActivity::class.java) - .create() - .start() - .resume() + val controller = + Robolectric + .buildActivity(TestActivity::class.java) + .create() + .start() + .resume() val activity = controller.get() assertNotNull(activity.binding) @@ -44,10 +45,12 @@ class ActivityViewBindingPropertyTest { @Test fun `binding is cleared after onDestroy`() { - val controller = Robolectric.buildActivity(TestActivity::class.java) - .create() - .start() - .resume() + val controller = + Robolectric + .buildActivity(TestActivity::class.java) + .create() + .start() + .resume() val activity = controller.get() assertNotNull(activity.binding) diff --git a/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindingPropertyTest.kt b/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindingPropertyTest.kt index f8d2a73..5255054 100644 --- a/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindingPropertyTest.kt +++ b/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/FragmentViewBindingPropertyTest.kt @@ -18,7 +18,6 @@ import kotlin.test.assertNotNull @RunWith(RobolectricTestRunner::class) class FragmentViewBindingPropertyTest { - class TestFragment : Fragment() { val binding by viewBinding { fragment: TestFragment -> mockk { @@ -29,10 +28,8 @@ class FragmentViewBindingPropertyTest { override fun onCreateView( inflater: LayoutInflater, container: ViewGroup?, - savedInstanceState: Bundle? - ): View { - return FrameLayout(requireContext()) - } + savedInstanceState: Bundle?, + ): View = FrameLayout(requireContext()) } @Test diff --git a/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindingsTest.kt b/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindingsTest.kt index 243a0ec..1262d4c 100644 --- a/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindingsTest.kt +++ b/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewGroupBindingsTest.kt @@ -10,22 +10,24 @@ import io.mockk.mockk import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import kotlin.reflect.KProperty import kotlin.test.assertEquals @RunWith(RobolectricTestRunner::class) class ViewGroupBindingsTest { - private val context: Context = ApplicationProvider.getApplicationContext() + private val mockProperty = mockk>(relaxed = true) @Test fun `viewBinding creates lazy binding for ViewGroup`() { val viewGroup = FrameLayout(context) - val expectedBinding = mockk { - every { root } returns View(context) - } + val expectedBinding = + mockk { + every { root } returns View(context) + } val property = viewGroup.viewBinding { _: FrameLayout -> expectedBinding } - val result = property.getValue(viewGroup, ::result) + val result = property.getValue(viewGroup, mockProperty) assertEquals(expectedBinding, result) } @@ -33,13 +35,14 @@ class ViewGroupBindingsTest { @Test fun `viewBinding returns same instance on subsequent calls`() { val viewGroup = FrameLayout(context) - val binding = mockk { - every { root } returns View(context) - } + val binding = + mockk { + every { root } returns View(context) + } val property = viewGroup.viewBinding { _: FrameLayout -> binding } - val first = property.getValue(viewGroup, ::first) - val second = property.getValue(viewGroup, ::second) + val first = property.getValue(viewGroup, mockProperty) + val second = property.getValue(viewGroup, mockProperty) assertEquals(first, second) } diff --git a/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindingsTest.kt b/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindingsTest.kt index 293ae1e..bcc5231 100644 --- a/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindingsTest.kt +++ b/vbpd/src/test/kotlin/dev/androidbroadcast/vbpd/ViewHolderBindingsTest.kt @@ -11,25 +11,29 @@ import io.mockk.mockk import org.junit.Test import org.junit.runner.RunWith import org.robolectric.RobolectricTestRunner +import kotlin.reflect.KProperty import kotlin.test.assertEquals @RunWith(RobolectricTestRunner::class) class ViewHolderBindingsTest { - private val context: Context = ApplicationProvider.getApplicationContext() + private val mockProperty = mockk>(relaxed = true) - class TestViewHolder(view: View) : RecyclerView.ViewHolder(view) + class TestViewHolder( + view: View, + ) : RecyclerView.ViewHolder(view) @Test fun `viewBinding creates binding from ViewHolder`() { val view = FrameLayout(context) val viewHolder = TestViewHolder(view) - val expectedBinding = mockk { - every { root } returns view - } + val expectedBinding = + mockk { + every { root } returns view + } val property = viewHolder.viewBinding { _: TestViewHolder -> expectedBinding } - val result = property.getValue(viewHolder, ::result) + val result = property.getValue(viewHolder, mockProperty) assertEquals(expectedBinding, result) } @@ -38,15 +42,17 @@ class ViewHolderBindingsTest { fun `viewBinding with factory and viewProvider`() { val view = FrameLayout(context) val viewHolder = TestViewHolder(view) - val expectedBinding = mockk { - every { root } returns view - } - - val property = viewHolder.viewBinding( - vbFactory = { expectedBinding }, - viewProvider = { it.itemView } - ) - val result = property.getValue(viewHolder, ::result) + val expectedBinding = + mockk { + every { root } returns view + } + + val property = + viewHolder.viewBinding( + vbFactory = { _: View -> expectedBinding }, + viewProvider = { vh: TestViewHolder -> vh.itemView }, + ) + val result = property.getValue(viewHolder, mockProperty) assertEquals(expectedBinding, result) } From 302b12abe144aab658d5bb8f77c5acecd7644281 Mon Sep 17 00:00:00 2001 From: kirich1409 Date: Sat, 14 Feb 2026 22:02:33 +0300 Subject: [PATCH 5/5] Address PR review: remove unnecessary Robolectric, use assertNotEquals - Remove @RunWith(RobolectricTestRunner) from ReflectionActivityViewBindingsTest as it doesn't use Android APIs (runs faster as pure JVM test) - Replace assert(!=) with assertNotEquals for better error messages Co-Authored-By: Claude Opus 4.6 --- .../vbpd/ReflectionActivityViewBindingsTest.kt | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ReflectionActivityViewBindingsTest.kt b/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ReflectionActivityViewBindingsTest.kt index 1dff790..762ef5b 100644 --- a/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ReflectionActivityViewBindingsTest.kt +++ b/vbpd-reflection/src/test/kotlin/dev/androidbroadcast/vbpd/ReflectionActivityViewBindingsTest.kt @@ -1,11 +1,9 @@ package dev.androidbroadcast.vbpd import org.junit.Test -import org.junit.runner.RunWith -import org.robolectric.RobolectricTestRunner +import kotlin.test.assertNotEquals import kotlin.test.assertNotNull -@RunWith(RobolectricTestRunner::class) class ReflectionActivityViewBindingsTest { @Test fun `CreateMethod BIND and INFLATE enum values exist`() { @@ -15,6 +13,6 @@ class ReflectionActivityViewBindingsTest { @Test fun `CreateMethod values are distinct`() { - assert(CreateMethod.BIND != CreateMethod.INFLATE) + assertNotEquals(CreateMethod.BIND, CreateMethod.INFLATE) } }