diff --git a/README.md b/README.md index 8f0ac2b..dc85d2d 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,9 @@ implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.2.0' // If you use Kotlin Android Extensions and synthetic properties (alternative to findViewById()) implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:4.2.0' + +// If you use ViewBinding +implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-viewbinding:4.2.0' ``` ## How to use it @@ -57,6 +60,21 @@ fun catAdapterDelegate(itemClickedListener : (Cat) -> Unit) = adapterDelegateLay } ``` +In case you want to use ViewBinding\DataBinding use `adapterDelegateViewBinding` instead of `adapterDelegate` like this: + +```kotlin +fun cat2AdapterDelegate() = adapterDelegateViewBinding( + { layoutInflater, root -> ItemCatBinding.inflate(layoutInflater, root, false) } +) { + binding.name.setOnClickListener { + Log.d("Click", "Click on $item") + } + bind { + binding.name.text = item.name + } +} +``` + You have to specify if a specific AdapterDelegate is responsible for a specific item. Per default this is done with an `instanceof` check like `Cat instanceof Animal`. You can override this if you want to handle it in a custom way by setting the `on` lambda diff --git a/app/build.gradle b/app/build.gradle index df6845d..8b9b4c6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -32,6 +32,10 @@ android { disable 'GoogleAppIndexingWarning' abortOnError false } + + viewBinding { + enabled = true + } } @@ -44,6 +48,7 @@ dependencies { implementation project(':paging') implementation project(':kotlin-dsl') implementation project(':kotlin-dsl-layoutcontainer') + implementation project(':kotlin-dsl-viewbinding') implementation 'com.android.support.constraint:constraint-layout:1.1.3' implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" } diff --git a/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/MainListAdapter.kt b/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/MainListAdapter.kt index 7e2be2c..730aeb5 100644 --- a/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/MainListAdapter.kt +++ b/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/MainListAdapter.kt @@ -6,12 +6,12 @@ import com.hannesdorfmann.adapterdelegates4.sample.adapterdelegates.Advertisemen import com.hannesdorfmann.adapterdelegates4.sample.adapterdelegates.DogAdapterDelegate import com.hannesdorfmann.adapterdelegates4.sample.adapterdelegates.GeckoAdapterDelegate import com.hannesdorfmann.adapterdelegates4.sample.adapterdelegates.SnakeListItemAdapterDelegate -import com.hannesdorfmann.adapterdelegates4.sample.dsl.catAdapterDelegate +import com.hannesdorfmann.adapterdelegates4.sample.dsl.cat2AdapterDelegate import com.hannesdorfmann.adapterdelegates4.sample.model.DisplayableItem class MainListAdapter(activity: Activity) : ListDelegationAdapter>( AdvertisementAdapterDelegate(activity), - catAdapterDelegate(), + cat2AdapterDelegate(), DogAdapterDelegate(activity), GeckoAdapterDelegate(activity), SnakeListItemAdapterDelegate(activity) diff --git a/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/dsl/DslSample.kt b/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/dsl/DslSample.kt index c8a4da4..fc5126c 100644 --- a/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/dsl/DslSample.kt +++ b/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/dsl/DslSample.kt @@ -2,13 +2,16 @@ package com.hannesdorfmann.adapterdelegates4.sample.dsl import android.util.Log import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding import com.hannesdorfmann.adapterdelegates4.sample.R +import com.hannesdorfmann.adapterdelegates4.sample.databinding.ItemCatBinding import com.hannesdorfmann.adapterdelegates4.sample.model.Cat import com.hannesdorfmann.adapterdelegates4.sample.model.DisplayableItem -import kotlinx.android.synthetic.main.item_cat.* +import kotlinx.android.synthetic.main.item_cat.name // Example -fun catAdapterDelegate() = adapterDelegateLayoutContainer(R.layout.item_cat) { +fun catAdapterDelegate() = adapterDelegateLayoutContainer(R.layout.item_cat) { name.setOnClickListener { Log.d("Click", "Click on $item") @@ -18,3 +21,14 @@ fun catAdapterDelegate() = adapterDelegateLayoutContainer( name.text = item.name } } + +fun cat2AdapterDelegate() = adapterDelegateViewBinding( + { layoutInflater, root -> ItemCatBinding.inflate(layoutInflater, root, false) } +) { + binding.name.setOnClickListener { + Log.d("Click", "Click on $item") + } + bind { + binding.name.text = item.name + } +} diff --git a/build.gradle b/build.gradle index 0f1b1d5..eb482e2 100644 --- a/build.gradle +++ b/build.gradle @@ -7,7 +7,7 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.4.1' + classpath 'com.android.tools.build:gradle:3.6.1' classpath 'com.vanniktech:gradle-maven-publish-plugin:0.6.0' classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" diff --git a/deploy_snapshot.sh b/deploy_snapshot.sh index a0b8948..50ae198 100755 --- a/deploy_snapshot.sh +++ b/deploy_snapshot.sh @@ -28,7 +28,7 @@ else echo "signing.secretKeyRingFile=/home/travis/.gnupg/secring.gpg" >> library/gradle.properties echo "org.gradle.parallel=false" >> gradle.properties echo "org.gradle.configureondemand=false" >> gradle.properties - ./gradlew --no-daemon :library:uploadArchives :paging:uploadArchives :kotlin-dsl:uploadArchives :kotlin-dsl-layoutcontainer:uploadArchives -Dorg.gradle.parallel=false -Dorg.gradle.configureondemand=false + ./gradlew --no-daemon :library:uploadArchives :paging:uploadArchives :kotlin-dsl:uploadArchives :kotlin-dsl-layoutcontainer:uploadArchives :kotlin-dsl-viewbinding:uploadArchives -Dorg.gradle.parallel=false -Dorg.gradle.configureondemand=false rm key.gpg git reset --hard echo "Snapshot deployed!" diff --git a/gradle.properties b/gradle.properties index f545611..98f146d 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,8 +17,8 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=4.2.1-SNAPSHOT -VERSION_CODE=421-SNAPSHOT +VERSION_NAME=4.2.2-SNAPSHOT +VERSION_CODE=422-SNAPSHOT GROUP=com.hannesdorfmann @@ -34,4 +34,7 @@ POM_DEVELOPER_ID=hannesdorfmann POM_DEVELOPER_NAME=Hannes Dorfmann signing.keyId=E508C045 - +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index f6b0e9f..d3b792f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Wed Jul 03 19:42:09 CEST 2019 +#Tue Feb 04 08:47:41 SAMT 2020 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.4-all.zip diff --git a/kotlin-dsl-layoutcontainer/src/main/java/com/hannesdorfmann/adapterdelegates4/dsl/LayoutContainerListAdapterDelegateDsl.kt b/kotlin-dsl-layoutcontainer/src/main/java/com/hannesdorfmann/adapterdelegates4/dsl/LayoutContainerListAdapterDelegateDsl.kt index 71157f7..fdd8c8f 100644 --- a/kotlin-dsl-layoutcontainer/src/main/java/com/hannesdorfmann/adapterdelegates4/dsl/LayoutContainerListAdapterDelegateDsl.kt +++ b/kotlin-dsl-layoutcontainer/src/main/java/com/hannesdorfmann/adapterdelegates4/dsl/LayoutContainerListAdapterDelegateDsl.kt @@ -16,6 +16,8 @@ import androidx.annotation.StringRes import androidx.recyclerview.widget.RecyclerView import com.hannesdorfmann.adapterdelegates4.AbsListItemAdapterDelegate import com.hannesdorfmann.adapterdelegates4.AdapterDelegate +import kotlinx.android.extensions.CacheImplementation +import kotlinx.android.extensions.ContainerOptions import kotlinx.android.extensions.LayoutContainer /** @@ -115,6 +117,7 @@ internal class DslLayoutContainerListAdapterDelegate( * * @since 4.1.0 */ +@ContainerOptions(cache = CacheImplementation.SPARSE_ARRAY) class AdapterDelegateLayoutContainerViewHolder( override val containerView: View ) : RecyclerView.ViewHolder(containerView), LayoutContainer { diff --git a/kotlin-dsl-viewbinding/.gitignore b/kotlin-dsl-viewbinding/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/kotlin-dsl-viewbinding/.gitignore @@ -0,0 +1 @@ +/build diff --git a/kotlin-dsl-viewbinding/build.gradle b/kotlin-dsl-viewbinding/build.gradle new file mode 100644 index 0000000..aa172c3 --- /dev/null +++ b/kotlin-dsl-viewbinding/build.gradle @@ -0,0 +1,59 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +androidExtensions { + experimental = true +} + +gradle.ext.isCiServer = System.getenv().containsKey("CI") +logger.warn("Running on CI: ${gradle.ext.isCiServer}") + +if (gradle.ext.isCiServer) { + apply plugin: "com.vanniktech.maven.publish" + mavenPublish { + releaseRepositoryUrl = "https://oss.sonatype.org/service/local/staging/deploy/maven2/" + snapshotRepositoryUrl = "https://oss.sonatype.org/content/repositories/snapshots/" + } +} + +android { + compileSdkVersion rootProject.ext.compileSdk + + defaultConfig { + minSdkVersion 14 + targetSdkVersion 28 + versionCode 2 + versionName "4.0" + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + } + } + + compileOptions { + sourceCompatibility rootProject.ext.javaSourceCompatibility + targetCompatibility rootProject.ext.javaTargetCompatibility + } + + libraryVariants.all { + it.generateBuildConfig.enabled = false + } + + viewBinding { + enabled = true + } +} + +dependencies { + api project(":library") + + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" + testImplementation 'junit:junit:4.12' + testImplementation 'org.mockito:mockito-inline:2.21.0' +} + +// apply from: 'https://raw.github.com/sockeqwe/gradle-mvn-push/master/gradle-mvn-push.gradle' +// apply from: 'https://raw.githubusercontent.com/Tickaroo/findbugs-script/master/findbugs.gradle' diff --git a/kotlin-dsl-viewbinding/consumer-rules.pro b/kotlin-dsl-viewbinding/consumer-rules.pro new file mode 100644 index 0000000..e69de29 diff --git a/kotlin-dsl-viewbinding/gradle.properties b/kotlin-dsl-viewbinding/gradle.properties new file mode 100644 index 0000000..5f5165b --- /dev/null +++ b/kotlin-dsl-viewbinding/gradle.properties @@ -0,0 +1,8 @@ +POM_NAME = AdapterDelegates-Kotlin-Dsl +POM_ARTIFACT_ID = adapterdelegates4-kotlin-dsl-viewbinding +POM_PACKAGING = aar + +# AndroidX package structure to make it clearer which packages are bundled with the +# Android operating system, and which are packaged with your app's APK +# https://developer.android.com/topic/libraries/support-library/androidx-rn +android.useAndroidX=true \ No newline at end of file diff --git a/kotlin-dsl-viewbinding/proguard-rules.pro b/kotlin-dsl-viewbinding/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/kotlin-dsl-viewbinding/proguard-rules.pro @@ -0,0 +1,21 @@ +# Add project specific ProGuard rules here. +# You can control the set of applied configuration files using the +# proguardFiles setting in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Uncomment this to preserve the line number information for +# debugging stack traces. +#-keepattributes SourceFile,LineNumberTable + +# If you keep the line number information, uncomment this to +# hide the original source file name. +#-renamesourcefileattribute SourceFile diff --git a/kotlin-dsl-viewbinding/src/main/AndroidManifest.xml b/kotlin-dsl-viewbinding/src/main/AndroidManifest.xml new file mode 100644 index 0000000..49070b3 --- /dev/null +++ b/kotlin-dsl-viewbinding/src/main/AndroidManifest.xml @@ -0,0 +1 @@ + diff --git a/kotlin-dsl-viewbinding/src/main/java/com/hannesdorfmann/adapterdelegates4/dsl/ViewBindingListAdapterDelegateDsl.kt b/kotlin-dsl-viewbinding/src/main/java/com/hannesdorfmann/adapterdelegates4/dsl/ViewBindingListAdapterDelegateDsl.kt new file mode 100644 index 0000000..add3b8e --- /dev/null +++ b/kotlin-dsl-viewbinding/src/main/java/com/hannesdorfmann/adapterdelegates4/dsl/ViewBindingListAdapterDelegateDsl.kt @@ -0,0 +1,332 @@ +package com.hannesdorfmann.adapterdelegates4.dsl + +import android.content.Context +import android.content.res.ColorStateList +import android.graphics.drawable.Drawable +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.ColorInt +import androidx.annotation.ColorRes +import androidx.annotation.DrawableRes +import androidx.annotation.IdRes +import androidx.annotation.StringRes +import androidx.core.content.ContextCompat +import androidx.recyclerview.widget.RecyclerView +import androidx.viewbinding.ViewBinding +import com.hannesdorfmann.adapterdelegates4.AbsListItemAdapterDelegate +import com.hannesdorfmann.adapterdelegates4.AdapterDelegate + +/** + * Simple DSL builder to create an [AdapterDelegate] that is backed by a [List] as dataset. + * This DSL builds on top of [ViewBinding] so that no findViewById is needed anymore. + * + * @param viewBinding ViewBinding for this adapter delegate. + * @param on The check that should be run if the AdapterDelegate is for the corresponding Item in the datasource. + * In other words its the implementation of [AdapterDelegate.isForViewType]. + * @param block The DSL block. Specify here what to do when the ViewHolder gets created. Think of it as some kind of + * initializer block. For example, you would setup a click listener on a Ui widget in that block followed by specifying + * what to do once the ViewHolder binds to the data by specifying a bind block for + * @since 4.1.0 + */ +inline fun adapterDelegateViewBinding( + noinline viewBinding: (layoutInflater: LayoutInflater, parent: ViewGroup) -> V, + noinline on: (item: T, items: List, position: Int) -> Boolean = { item, _, _ -> item is I }, + noinline layoutInflater: (parent: ViewGroup) -> LayoutInflater = { parent -> LayoutInflater.from(parent.context) }, + noinline block: AdapterDelegateViewBindingViewHolder.() -> Unit +): AdapterDelegate> { + + return DslViewBindingListAdapterDelegate( + binding = viewBinding, + on = on, + initializerBlock = block, + layoutInflater = layoutInflater) +} + +@PublishedApi +internal class DslViewBindingListAdapterDelegate( + private val binding: (layoutInflater: LayoutInflater, parent: ViewGroup) -> V, + private val on: (item: T, items: List, position: Int) -> Boolean, + private val initializerBlock: AdapterDelegateViewBindingViewHolder.()->Unit, + private val layoutInflater: (parent: ViewGroup) -> LayoutInflater + ) : AbsListItemAdapterDelegate>() { + + override fun isForViewType(item: T, items: MutableList, position: Int): Boolean = on( + item, items, position + ) + + override fun onCreateViewHolder(parent: ViewGroup): AdapterDelegateViewBindingViewHolder { + val binding = binding(layoutInflater(parent), parent) + return AdapterDelegateViewBindingViewHolder( + binding + ).also { + initializerBlock(it) + } + } + + override fun onBindViewHolder( + item: I, + holder: AdapterDelegateViewBindingViewHolder, + payloads: MutableList + ) { + holder._item = item as Any + holder._bind?.invoke(payloads) // It's ok to have an AdapterDelegate without binding block (i.e. static content) + } + + override fun onViewRecycled(holder: RecyclerView.ViewHolder) { + @Suppress("UNCHECKED_CAST") + val vh = (holder as AdapterDelegateViewBindingViewHolder) + + vh._onViewRecycled?.invoke() + } + + override fun onFailedToRecycleView(holder: RecyclerView.ViewHolder): Boolean { + @Suppress("UNCHECKED_CAST") + val vh = (holder as AdapterDelegateViewBindingViewHolder) + val block = vh._onFailedToRecycleView + return if (block == null) { + super.onFailedToRecycleView(holder) + } else { + block() + } + } + + override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) { + @Suppress("UNCHECKED_CAST") + val vh = (holder as AdapterDelegateViewBindingViewHolder) + vh._onViewAttachedToWindow?.invoke() + } + + override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { + @Suppress("UNCHECKED_CAST") + val vh = (holder as AdapterDelegateViewBindingViewHolder) + vh._onViewDetachedFromWindow?.invoke() + } +} + +/** + * ViewHolder that is used internally if you use [adapterDelegateViewBinding] DSL to create your AdapterDelegate + * + * @since 4.1.0 + */ +class AdapterDelegateViewBindingViewHolder( + val binding: V, view: View = binding.root +) : RecyclerView.ViewHolder(view) { + + private object Uninitialized + + /** + * Used only internally to set the item. + * The outside consumer should always access [item]. + * We do that to trick the user for his own convenience since the Item is only available later and is actually var + * (not val) but we rely on mechanisms from RecyclerView and assume that only the main thread can access and set + * this field (as the user scrolls) so that we make [item] look like a val. + */ + internal var _item: Any = Uninitialized + + /** + * Get the current bound item. + */ + val item: T + get() = if (_item === Uninitialized) { + throw IllegalArgumentException( + "Item has not been set yet. That is an internal issue. " + + "Please report at https://github.com/sockeqwe/AdapterDelegates" + ) + } else { + @Suppress("UNCHECKED_CAST") + _item as T + } + + /** + * Get the context. + * + * @since 4.1.1 + */ + val context: Context = view.context + + /** + * Returns a localized string from the application's package's + * default string table. + * + * @param resId Resource id for the string + * @return The string data associated with the resource, stripped of styled + * text information. + * + * @since 4.1.1 + */ + fun getString(@StringRes resId: Int): String { + return context.getString(resId) + } + + /** + * Returns a localized formatted string from the application's package's + * default string table, substituting the format arguments as defined in + * [java.util.Formatter] and [java.lang.String.format]. + * + * @param resId Resource id for the format string + * @param formatArgs The format arguments that will be used for + * substitution. + * @return The string data associated with the resource, formatted and + * stripped of styled text information. + * + * @since 4.1.1 + */ + fun getString(@StringRes resId: Int, vararg formatArgs: Any): String { + return context.getString(resId, *formatArgs) + } + + /** + * Returns a color associated with a particular resource ID and styled for + * the current theme. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @return A single color value in the form 0xAARRGGBB. + * @throws android.content.res.Resources.NotFoundException if the given ID + * does not exist. + * + * @since 4.1.1 + */ + @ColorInt + fun getColor(@ColorRes id: Int): Int { + return ContextCompat.getColor(context, id) + } + + /** + * Returns a drawable object associated with a particular resource ID and + * styled for the current theme. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @return An object that can be used to draw this resource. + * @throws android.content.res.Resources.NotFoundException if the given ID + * does not exist. + * + * @since 4.1.1 + */ + fun getDrawable(@DrawableRes id: Int): Drawable? { + return ContextCompat.getDrawable(context, id) + } + + /** + * Returns a color state list associated with a particular resource ID and + * styled for the current theme. + * + * @param id The desired resource identifier, as generated by the aapt + * tool. This integer encodes the package, type, and resource + * entry. The value 0 is an invalid identifier. + * @return A color state list. + * @throws android.content.res.Resources.NotFoundException if the given ID + * does not exist. + */ + fun getColorStateList(@ColorRes id: Int): ColorStateList? { + return ContextCompat.getColorStateList(context, id) + } + + /** + * This should never be called directly. + * Use [bind] instead which internally sets this field. + */ + internal var _bind: ((payloads: List) -> Unit)? = null + private set + + /** + * This should never be called directly (only called internally) + * Use [onViewRecycled] instead + */ + internal var _onViewRecycled: (() -> Unit)? = null + private set + + /** + * This should never be called directly (only called internally) + * Use [onFailedToRecycleView] instead. + */ + internal var _onFailedToRecycleView: (() -> Boolean)? = null + private set + + /** + * This should never be called directly (only called internally) + * Use [onViewAttachedToWindow] instead. + */ + internal var _onViewAttachedToWindow: (() -> Unit)? = null + private set + + /** + * This should never be called directly (only called internally) + * Use [onViewDetachedFromWindow] instead. + */ + internal var _onViewDetachedFromWindow: (() -> Unit)? = null + private set + + /** + * Define here the block that should be run whenever the viewholder get binded. + * You can access the current bound item with [item]. In case you need the position of the bound item inside the + * adapters dataset use [getAdapterPosition]. + */ + fun bind(bindingBlock: (payloads: List) -> Unit) { + if (_bind != null) { + throw IllegalStateException("bind { ... } is already defined. Only one bind { ... } is allowed.") + } + _bind = bindingBlock + } + + /** + * @see AdapterDelegate.onViewRecycled + */ + fun onViewRecycled(block: () -> Unit) { + if (_onViewRecycled != null) { + throw IllegalStateException( + "onViewRecycled { ... } is already defined. " + + "Only one onViewRecycled { ... } is allowed." + ) + } + _onViewRecycled = block + } + + /** + * @see AdapterDelegate.onFailedToRecycleView + */ + fun onFailedToRecycleView(block: () -> Boolean) { + if (_onFailedToRecycleView != null) { + throw IllegalStateException( + "onFailedToRecycleView { ... } is already defined. " + + "Only one onFailedToRecycleView { ... } is allowed." + ) + } + _onFailedToRecycleView = block + } + + /** + * @see AdapterDelegate.onViewAttachedToWindow + */ + fun onViewAttachedToWindow(block: () -> Unit) { + if (_onViewAttachedToWindow != null) { + throw IllegalStateException( + "onViewAttachedToWindow { ... } is already defined. " + + "Only one onViewAttachedToWindow { ... } is allowed." + ) + } + _onViewAttachedToWindow = block + } + + /** + * @see AdapterDelegate.onViewDetachedFromWindow + */ + fun onViewDetachedFromWindow(block: () -> Unit) { + if (_onViewDetachedFromWindow != null) { + throw IllegalStateException( + "onViewDetachedFromWindow { ... } is already defined. " + + "Only one onViewDetachedFromWindow { ... } is allowed." + ) + } + _onViewDetachedFromWindow = block + } + + /** + * Convenience method find a given view with the given id inside the layout + */ + fun findViewById(@IdRes id: Int): V = itemView.findViewById(id) as V +} diff --git a/kotlin-dsl-viewbinding/src/test/java/com/hannesdorfmann/adapterdelegates4/ViewBindingListAdapterDelegateDslTest.kt b/kotlin-dsl-viewbinding/src/test/java/com/hannesdorfmann/adapterdelegates4/ViewBindingListAdapterDelegateDslTest.kt new file mode 100644 index 0000000..112f77d --- /dev/null +++ b/kotlin-dsl-viewbinding/src/test/java/com/hannesdorfmann/adapterdelegates4/ViewBindingListAdapterDelegateDslTest.kt @@ -0,0 +1,400 @@ +package com.hannesdorfmann.adapterdelegates4 + +import android.content.Context +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.viewbinding.ViewBinding +import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewBindingViewHolder +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateViewBinding +import org.junit.Assert +import org.junit.Test +import org.mockito.Mockito +import org.mockito.Mockito.`when` as whenever + +class ViewBindingListAdapterDelegateDslTest { + + data class Item(val name: String) + + private fun fakeLayoutInflater(): LayoutInflater { + return Mockito.mock(LayoutInflater::class.java) + } + + private fun fakeView(): View { + val view = Mockito.mock(View::class.java) + val context = Mockito.mock(Context::class.java) + whenever(view.context).thenReturn(context) + return view + } + + private fun fakeViewGroup(): ViewGroup { + return Mockito.mock(ViewGroup::class.java) + } + + @Test + fun `init block is called once - bind multiple times`() { + + val items = listOf(Any(), Any()) + var payload = emptyList() + + var viewHolder: AdapterDelegateViewBindingViewHolder? = null + + var initCalled = 0 + var bindCalled = 0 + var bindPayload = emptyList() + var boundItemInBindBlock: Any? = null + val view = fakeView() + val viewGroup = fakeViewGroup() + val layoutInflater = fakeLayoutInflater() + + val binding = ViewBinding { + view + } + val delegate = + adapterDelegateViewBinding( + viewBinding = { _, _ -> binding }, + layoutInflater = { layoutInflater } + ) { + initCalled++ + viewHolder = this + bind { + bindCalled++ + bindPayload = it + boundItemInBindBlock = item + } + } + + // Assert init block is called + delegate.onCreateViewHolder(viewGroup) + Assert.assertEquals(1, initCalled) + Assert.assertNotNull(viewHolder) + + // Assert binding at position 0 + delegate.onBindViewHolder(items, 0, viewHolder!!, payload) + Assert.assertEquals(1, bindCalled) + Assert.assertSame(payload, bindPayload) + Assert.assertSame(items[0], viewHolder!!.item) + Assert.assertSame(items[0], boundItemInBindBlock) + + // Assert binding at position 1 + payload = listOf(Any()) + delegate.onBindViewHolder(items, 1, viewHolder!!, payload) + Assert.assertEquals(2, bindCalled) + Assert.assertSame(payload, bindPayload) + Assert.assertSame(items[1], viewHolder!!.item) + Assert.assertSame(items[1], boundItemInBindBlock) + } + + @Test + fun `isForViewType is determined from generics correctly`() { + val view = fakeView() + + val binding = ViewBinding { + view + } + val delegate = + adapterDelegateViewBinding( + viewBinding = { _, _ -> binding }) { + } + + val item = Item("foo") + val items = listOf(item, Any()) + + Assert.assertTrue(delegate.isForViewType(items, 0)) + Assert.assertFalse(delegate.isForViewType(items, 1)) + } + + @Test + fun `custom on block is used for isForViewType`() { + val view = fakeView() + + var onBlockCalled = 0 + var onBlockItem: Any? = null + var onBlockList: List? = null + var onBlockPosition = -1 + + val binding = ViewBinding { + view + } + val delegate = + adapterDelegateViewBinding( + viewBinding = { _, _ -> binding }, + on = { item, list, position -> + onBlockItem = item + onBlockList = list + onBlockPosition = position + onBlockCalled++ + false + }) { + + } + + val item = Item("foo") + val items = listOf(item, Any()) + + Assert.assertFalse(delegate.isForViewType(items, 0)) + Assert.assertEquals(1, onBlockCalled) + Assert.assertSame(item, onBlockItem) + Assert.assertSame(items, onBlockList) + Assert.assertEquals(0, onBlockPosition) + + Assert.assertFalse(delegate.isForViewType(items, 1)) + Assert.assertEquals(2, onBlockCalled) + Assert.assertSame(items[1], onBlockItem) + Assert.assertSame(items, onBlockList) + Assert.assertEquals(1, onBlockPosition) + } + + @Test + fun `multiple binds throws exception`() { + val view = fakeView() + val viewGroup = fakeViewGroup() + val layoutInflater = fakeLayoutInflater() + + val binding = ViewBinding { + view + } + try { + val delegate = + adapterDelegateViewBinding( + viewBinding = { _, _ -> binding }, + layoutInflater = { layoutInflater } + ) { + bind { } + + bind { } + } + delegate.onCreateViewHolder(viewGroup) + Assert.fail("Exception expected") + } catch (e: IllegalStateException) { + val expectedMsg = "bind { ... } is already defined. Only one bind { ... } is allowed." + Assert.assertEquals(expectedMsg, e.message) + } + } + + @Test + fun `onViewRecycled called`() { + val view = fakeView() + val viewGroup = fakeViewGroup() + val layoutInflater = fakeLayoutInflater() + var called = 0 + var viewHolder: AdapterDelegateViewBindingViewHolder? = null + val binding = ViewBinding { + view + } + val delegate = + adapterDelegateViewBinding( + viewBinding = { _, _ -> binding }, + layoutInflater = { layoutInflater } + ) { + viewHolder = this + onViewRecycled { + called++ + } + } + + delegate.onCreateViewHolder(viewGroup) + Assert.assertNotNull(viewHolder) + delegate.onViewRecycled(viewHolder!!) + Assert.assertEquals(1, called) + } + + @Test + fun `multiple onViewRecycled throws exception`() { + val view = fakeView() + val viewGroup = fakeViewGroup() + val layoutInflater = fakeLayoutInflater() + + val binding = ViewBinding { + view + } + try { + val delegate = + adapterDelegateViewBinding( + viewBinding = { _, _ -> binding }, + layoutInflater = { layoutInflater } + ) { + onViewRecycled { } + + onViewRecycled { } + } + delegate.onCreateViewHolder(viewGroup) + Assert.fail("Exception expected") + } catch (e: IllegalStateException) { + val expectedMsg = + "onViewRecycled { ... } is already defined. Only one onViewRecycled { ... } is allowed." + Assert.assertEquals(expectedMsg, e.message) + } + } + + @Test + fun `onFailedToRecycleView called`() { + val view = fakeView() + val viewGroup = fakeViewGroup() + val layoutInflater = fakeLayoutInflater() + + var called = 0 + var viewHolder: AdapterDelegateViewBindingViewHolder? = null + val binding = ViewBinding { + view + } + val delegate = + adapterDelegateViewBinding( + viewBinding = { _, _ -> binding }, + layoutInflater = { layoutInflater } + ) { + viewHolder = this + onFailedToRecycleView { + called++ + true + } + } + + delegate.onCreateViewHolder(viewGroup) + Assert.assertNotNull(viewHolder) + val ret = delegate.onFailedToRecycleView(viewHolder!!) + Assert.assertEquals(1, called) + Assert.assertTrue(ret) + } + + @Test + fun `multiple onFailedToRecycleView throws exception`() { + val view = fakeView() + val viewGroup = fakeViewGroup() + val layoutInflater = fakeLayoutInflater() + + val binding = ViewBinding { + view + } + try { + val delegate = + adapterDelegateViewBinding( + viewBinding = { _, _ -> binding }, + layoutInflater = { layoutInflater } + ) { + onFailedToRecycleView { false } + + onFailedToRecycleView { false } + } + delegate.onCreateViewHolder(viewGroup) + Assert.fail("Exception expected") + } catch (e: IllegalStateException) { + val expectedMsg = + "onFailedToRecycleView { ... } is already defined. Only one onFailedToRecycleView { ... } is allowed." + Assert.assertEquals(expectedMsg, e.message) + } + } + + @Test + fun `onViewAttachedToWindow called`() { + val view = fakeView() + val viewGroup = fakeViewGroup() + val layoutInflater = fakeLayoutInflater() + + var called = 0 + var viewHolder: AdapterDelegateViewBindingViewHolder? = null + val binding = ViewBinding { + view + } + val delegate = + adapterDelegateViewBinding( + viewBinding = { _, _ -> binding }, + layoutInflater = { layoutInflater } + ) { + viewHolder = this + onViewAttachedToWindow { + called++ + } + } + + delegate.onCreateViewHolder(viewGroup) + Assert.assertNotNull(viewHolder) + delegate.onViewAttachedToWindow(viewHolder!!) + Assert.assertEquals(1, called) + } + + @Test + fun `multiple onViewAttachedToWindow throws exception`() { + val view = fakeView() + val viewGroup = fakeViewGroup() + val layoutInflater = fakeLayoutInflater() + + val binding = ViewBinding { + view + } + try { + val delegate = + adapterDelegateViewBinding( + viewBinding = { _, _ -> binding }, + layoutInflater = { layoutInflater } + ) { + onViewAttachedToWindow { } + + onViewAttachedToWindow { } + } + delegate.onCreateViewHolder(viewGroup) + Assert.fail("Exception expected") + } catch (e: IllegalStateException) { + val expectedMsg = + "onViewAttachedToWindow { ... } is already defined. Only one onViewAttachedToWindow { ... } is allowed." + Assert.assertEquals(expectedMsg, e.message) + } + } + + + @Test + fun `onViewDetachedFromWindow called`() { + val view = fakeView() + val viewGroup = fakeViewGroup() + val layoutInflater = fakeLayoutInflater() + + var called = 0 + var viewHolder: AdapterDelegateViewBindingViewHolder? = null + val binding = ViewBinding { + view + } + val delegate = + adapterDelegateViewBinding( + viewBinding = { _, _ -> binding }, + layoutInflater = { layoutInflater } + ) { + viewHolder = this + onViewDetachedFromWindow { + called++ + } + } + + delegate.onCreateViewHolder(viewGroup) + Assert.assertNotNull(viewHolder) + delegate.onViewDetachedFromWindow(viewHolder!!) + Assert.assertEquals(1, called) + } + + @Test + fun `multiple onViewDetachedFromWindow throws exception`() { + val view = fakeView() + val viewGroup = fakeViewGroup() + val layoutInflater = fakeLayoutInflater() + + val binding = ViewBinding { + view + } + try { + val delegate = + adapterDelegateViewBinding( + viewBinding = { _, _ -> binding }, + layoutInflater = { layoutInflater } + ) { + onViewDetachedFromWindow { } + + onViewDetachedFromWindow { } + } + delegate.onCreateViewHolder(viewGroup) + Assert.fail("Exception expected") + } catch (e: IllegalStateException) { + val expectedMsg = + "onViewDetachedFromWindow { ... } is already defined. Only one onViewDetachedFromWindow { ... } is allowed." + Assert.assertEquals(expectedMsg, e.message) + } + } +} \ No newline at end of file diff --git a/library/src/main/java/com/hannesdorfmann/adapterdelegates4/AdapterDelegate.java b/library/src/main/java/com/hannesdorfmann/adapterdelegates4/AdapterDelegate.java index 3c7ee44..7cf1bbf 100644 --- a/library/src/main/java/com/hannesdorfmann/adapterdelegates4/AdapterDelegate.java +++ b/library/src/main/java/com/hannesdorfmann/adapterdelegates4/AdapterDelegate.java @@ -18,11 +18,11 @@ import android.view.ViewGroup; -import java.util.List; - import androidx.annotation.NonNull; import androidx.recyclerview.widget.RecyclerView; +import java.util.List; + /** * This delegate provide method to hook in this delegate to {@link RecyclerView.Adapter} lifecycle. * This "hook in" mechanism is provided by {@link AdapterDelegatesManager} and that is the diff --git a/library/src/main/java/com/hannesdorfmann/adapterdelegates4/AdapterDelegatesManager.java b/library/src/main/java/com/hannesdorfmann/adapterdelegates4/AdapterDelegatesManager.java index 7cd9a77..eae4236 100644 --- a/library/src/main/java/com/hannesdorfmann/adapterdelegates4/AdapterDelegatesManager.java +++ b/library/src/main/java/com/hannesdorfmann/adapterdelegates4/AdapterDelegatesManager.java @@ -18,14 +18,14 @@ import android.view.ViewGroup; -import java.util.Collections; -import java.util.List; - import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.collection.SparseArrayCompat; import androidx.recyclerview.widget.RecyclerView; +import java.util.Collections; +import java.util.List; + /** * This class is the element that ties {@link RecyclerView.Adapter} together with {@link * AdapterDelegate}. diff --git a/library/src/test/java/com/hannesdorfmann/adapterdelegates4/AdapterDelegatesManagerTest.java b/library/src/test/java/com/hannesdorfmann/adapterdelegates4/AdapterDelegatesManagerTest.java index 1d7d667..01b84d3 100644 --- a/library/src/test/java/com/hannesdorfmann/adapterdelegates4/AdapterDelegatesManagerTest.java +++ b/library/src/test/java/com/hannesdorfmann/adapterdelegates4/AdapterDelegatesManagerTest.java @@ -2,6 +2,10 @@ import android.view.ViewGroup; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.recyclerview.widget.RecyclerView; + import junit.framework.Assert; import org.junit.Test; @@ -10,10 +14,6 @@ import java.util.Arrays; import java.util.List; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.recyclerview.widget.RecyclerView; - /** * @author Hannes Dorfmann */ diff --git a/settings.gradle b/settings.gradle index 9d58c2d..a573242 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':library', ':paging', ':kotlin-dsl', ':kotlin-dsl-layoutcontainer' +include ':app', ':library', ':paging', ':kotlin-dsl', ':kotlin-dsl-layoutcontainer', ':kotlin-dsl-viewbinding'