From 16551b11f842f4fa5420de9543e6e33f9df218aa Mon Sep 17 00:00:00 2001 From: Hannes Dorfmann Date: Thu, 18 Jul 2019 01:34:38 +0200 Subject: [PATCH] Add Kotlin DSL to create Adapters (#68) * First draft * New ListAdapterDelegateDsl implementatino * Moved dsl stuff in own submodule * Removed unusded files * Code duplication for adapter delegates * Tests added * Added dsl for other recyclerview releavant callbacks * Updated README * Updated Main Example * Update * More Docs * Fix a test * More assertions * More Readme * Better tests --- README.md | 104 +++++- app/build.gradle | 12 + .../sample/MainActivity.java | 3 +- .../adapterdelegates4/sample/MainAdapter.java | 81 ----- .../sample/MainListAdapter.kt | 18 + .../adapterdelegates4/sample/dsl/DslSample.kt | 20 ++ .../sample/model/Animal.java | 22 +- .../adapterdelegates4/sample/model/Cat.java | 2 - build.gradle | 4 +- gradle.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 4 +- kotlin-dsl-layoutcontainer/.gitignore | 1 + kotlin-dsl-layoutcontainer/build.gradle | 55 +++ kotlin-dsl-layoutcontainer/gradle.properties | 18 + kotlin-dsl-layoutcontainer/proguard-rules.pro | 21 ++ .../src/main/AndroidManifest.xml | 2 + .../LayoutContainerListAdapterDelegateDsl.kt | 241 +++++++++++++ .../src/main/res/values/strings.xml | 3 + ...youtContainerListAdapterDelegateDslTest.kt | 329 ++++++++++++++++++ kotlin-dsl/.gitignore | 1 + kotlin-dsl/build.gradle | 50 +++ kotlin-dsl/gradle.properties | 18 + kotlin-dsl/proguard-rules.pro | 21 ++ kotlin-dsl/src/main/AndroidManifest.xml | 2 + .../dsl/ListAdapterDelegateDsl.kt | 237 +++++++++++++ .../ListAdapterDelegateDslTest.kt | 326 +++++++++++++++++ library/build.gradle | 2 +- .../AbsDelegationAdapter.java | 13 + .../ListDelegationAdapter.java | 11 + .../AbsDelegationAdapterTest.java | 2 +- .../ListDelegationAdapterTest.java | 71 ++-- .../paging/PagedListDelegationAdapter.java | 13 + settings.gradle | 2 +- 33 files changed, 1579 insertions(+), 132 deletions(-) delete mode 100644 app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/MainAdapter.java create mode 100644 app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/MainListAdapter.kt create mode 100644 app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/dsl/DslSample.kt create mode 100644 kotlin-dsl-layoutcontainer/.gitignore create mode 100644 kotlin-dsl-layoutcontainer/build.gradle create mode 100644 kotlin-dsl-layoutcontainer/gradle.properties create mode 100644 kotlin-dsl-layoutcontainer/proguard-rules.pro create mode 100644 kotlin-dsl-layoutcontainer/src/main/AndroidManifest.xml create mode 100644 kotlin-dsl-layoutcontainer/src/main/java/com/hannesdorfmann/adapterdelegates4/dsl/LayoutContainerListAdapterDelegateDsl.kt create mode 100644 kotlin-dsl-layoutcontainer/src/main/res/values/strings.xml create mode 100644 kotlin-dsl-layoutcontainer/src/test/java/com/hannesdorfmann/adapterdelegates4/LayoutContainerListAdapterDelegateDslTest.kt create mode 100644 kotlin-dsl/.gitignore create mode 100644 kotlin-dsl/build.gradle create mode 100644 kotlin-dsl/gradle.properties create mode 100644 kotlin-dsl/proguard-rules.pro create mode 100644 kotlin-dsl/src/main/AndroidManifest.xml create mode 100644 kotlin-dsl/src/main/java/com/hannesdorfmann/adapterdelegates4/dsl/ListAdapterDelegateDsl.kt create mode 100644 kotlin-dsl/src/test/java/com/hannesdorfmann/adapterdelegates4/ListAdapterDelegateDslTest.kt diff --git a/README.md b/README.md index bba8c49..dc73f87 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,13 @@ # AdapterDelegates Read the motivation for this project in [my blog post](http://hannesdorfmann.com/android/adapter-delegates). +For Kotlin, there is a convenient DSL. Check out that section in the documentation below. + ## Dependencies This library is available on maven central: ```groovy -compile 'com.hannesdorfmann:adapterdelegates4:4.0.0' +implementation 'com.hannesdorfmann:adapterdelegates4:4.1.0' ``` [![Build Status](https://travis-ci.org/sockeqwe/AdapterDelegates.svg?branch=master)](https://travis-ci.org/sockeqwe/AdapterDelegates) @@ -14,7 +16,7 @@ Please note that since 4.0 the group id has been changed to `adapterdelegates4`. ### Snapshot ```groovy -compile 'com.hannesdorfmann:adapterdelegates4:4.0.1-SNAPSHOT' +implementation 'com.hannesdorfmann:adapterdelegates4:4.1.1-SNAPSHOT' ``` You also have to add the url to the snapshot repository: @@ -200,6 +202,104 @@ public class DiffAdapter extends AsyncListDifferDelegationAdapter { } ``` +## Kotlin DSL +There are 2 more artifacts for kotlin users that allow you to write Adapter Delegates more convenient by providing a `DSL`: + +``` +implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl:4.1.0' + +// If you use Kotlin Android Extensions and synthetic properties (alternative to findViewById()) +implementation 'com.hannesdorfmann:adapterdelegates4-kotlin-dsl-layoutcontainer:4.1.0' +``` + +Now instead of creating your own class which extends `AdapterDelegate` and implement the `onCreateViewHolder` and `onBindViewHolder` you can use the following Kotlin DSL to write the same `CatListItemAdapterDelegate` shown in the example above: + + +```kotlin +fun catAdapterDelegate(itemClickedListener : (Cat) -> Unit) = adapterDelegate(R.layout.item_cat) { + + // This is the initializer block where you initialize the ViewHolder. + // Its called one time only in onCreateViewHolder. + // this is where you can call findViewById() and setup click listeners etc. + + val name : TextView = findViewById(R.id.name) + name.setClickListener { itemClickedListener(item) } // Item is automatically set for you. It's set lazily though (set in onBindViewHolder()). So only use it for deferred calls like clickListeners. + + bind { diffPayloads -> // diffPayloads is a List containing the Payload from your DiffUtils + // This is called anytime onBindViewHolder() is called + name.text = item.name // Item is of type Cat and is the current bound item. + } +} +``` + +In case you want to use kotlin android extensions and synthetic properties (as alternative to findViewById()) use `adapterDelegateLayoutContainer` instead of `adapterDelegate` like this: + +```kotlin +fun catAdapterDelegate(itemClickedListener : (Cat) -> Unit) = adapterDelegateLayoutContainer(R.layout.item_cat) { + + name.setClickListener { itemClickedListener(item) } // no need for findViewById(). Name is imported as synthetic property from kotlinx.android.synthetic.main.item_cat + + bind { diffPayloads -> + name.text = item.name + } +} +``` + +As you see, thanks to Kotlin DSL you can write the same adapter in much less code. +`isForViewType()` is implemented by checking the two generic parameters. +In the example above it is `Cat instanceof Animal`. +If you want to provide your own `isForViewType()` implementation you have to provide a parameter `on` and return true or false: + +```kotlin +adapterDelegate ( + layout = R.layout.item_cat, + on = { item: Animal, items: List, position: Int -> + if (item is Cat && position == 0) + true // return true: this adapterDelegate handles it + else + false // return false + } +){ + ... + bind { ... } +} +``` + +The same `on` parameter is available for `adapterDelegateLayoutContainer()` DSL. + + +### Danger: Memory leaks! +Never ever use a top level `val` to hold a reference as top level `val` are static and will hold a reference to the adapter delegate and underlying ViewHolder and underlying android context (like activity) forever. +**Don't do this:** + + +```kotlin +// top level property inside CatDelegate.kt +val catDelegate = adapterDelegate { + ... + bind { ... } +} +``` + +**Instead use top level functions:** + +```kotlin +// top level function inside CatDelegate.kt +fun catAdapterDelegate() = adapterDelegate { + ... + bind { ... } +} +``` + +## Pagination +There is an additional artifact for the pagination library: + +```gradle +implementation 'com.hannesdorfmann:adapterdelegates4-pagination:4.1.0' +``` + +Use `PagedListDelegationAdapter`. + ## Fallback AdapterDelegate What if your adapter's data source contains a certain element you don't have registered an `AdapterDelegate` for? In this case the `AdapterDelegateManager` will throw an exception at runtime. However, this is not always what you want. You can specify a fallback `AdapterDelegate` that will be used if no other `AdapterDelegate` has been found to handle a certain view type. diff --git a/app/build.gradle b/app/build.gradle index 22bfda6..df6845d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -1,4 +1,10 @@ apply plugin: 'com.android.application' +apply plugin: 'kotlin-android' +apply plugin: 'kotlin-android-extensions' + +androidExtensions { + experimental = true +} android { compileSdkVersion rootProject.ext.compileSdk @@ -36,5 +42,11 @@ dependencies { implementation project(':library') implementation project(':paging') + implementation project(':kotlin-dsl') + implementation project(':kotlin-dsl-layoutcontainer') implementation 'com.android.support.constraint:constraint-layout:1.1.3' + implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version" +} +repositories { + mavenCentral() } diff --git a/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/MainActivity.java b/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/MainActivity.java index 3e66234..9db01dd 100644 --- a/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/MainActivity.java +++ b/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/MainActivity.java @@ -31,7 +31,8 @@ protected void onCreate(Bundle savedInstanceState) { RecyclerView rv = (RecyclerView) findViewById(R.id.recyclerView); rv.setLayoutManager(new LinearLayoutManager(this)); - MainAdapter adapter = new MainAdapter(this, getAnimals()); + MainListAdapter adapter = new MainListAdapter(this); + adapter.setItems(getAnimals()); rv.setAdapter(adapter); diff --git a/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/MainAdapter.java b/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/MainAdapter.java deleted file mode 100644 index dd302e8..0000000 --- a/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/MainAdapter.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2015 Hannes Dorfmann. - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package com.hannesdorfmann.adapterdelegates4.sample; - -import android.app.Activity; -import android.view.ViewGroup; - -import com.hannesdorfmann.adapterdelegates4.AdapterDelegatesManager; -import com.hannesdorfmann.adapterdelegates4.sample.adapterdelegates.AdvertisementAdapterDelegate; -import com.hannesdorfmann.adapterdelegates4.sample.adapterdelegates.CatAdapterDelegate; -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.model.DisplayableItem; - -import java.util.List; - -import androidx.annotation.NonNull; -import androidx.recyclerview.widget.RecyclerView; - -/** - * @author Hannes Dorfmann - */ -public class MainAdapter extends RecyclerView.Adapter { - - private AdapterDelegatesManager> delegatesManager; - private List items; - - public MainAdapter(Activity activity, List items) { - this.items = items; - - // Delegates - delegatesManager = new AdapterDelegatesManager<>(); - delegatesManager.addDelegate(new AdvertisementAdapterDelegate(activity)); - delegatesManager.addDelegate(new CatAdapterDelegate(activity)); - delegatesManager.addDelegate(new DogAdapterDelegate(activity)); - delegatesManager.addDelegate(new GeckoAdapterDelegate(activity)); - delegatesManager.addDelegate(new SnakeListItemAdapterDelegate(activity)); - - } - - @Override - public int getItemViewType(int position) { - return delegatesManager.getItemViewType(items, position); - } - - @NonNull - @Override - public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { - return delegatesManager.onCreateViewHolder(parent, viewType); - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position) { - delegatesManager.onBindViewHolder(items, position, holder); - } - - @Override - public void onBindViewHolder(@NonNull RecyclerView.ViewHolder holder, int position, @NonNull List payloads) { - delegatesManager.onBindViewHolder(items, position, holder, payloads); - } - - @Override - public int getItemCount() { - return items.size(); - } -} diff --git a/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/MainListAdapter.kt b/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/MainListAdapter.kt new file mode 100644 index 0000000..7e2be2c --- /dev/null +++ b/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/MainListAdapter.kt @@ -0,0 +1,18 @@ +package com.hannesdorfmann.adapterdelegates4.sample + +import android.app.Activity +import com.hannesdorfmann.adapterdelegates4.ListDelegationAdapter +import com.hannesdorfmann.adapterdelegates4.sample.adapterdelegates.AdvertisementAdapterDelegate +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.model.DisplayableItem + +class MainListAdapter(activity: Activity) : ListDelegationAdapter>( + AdvertisementAdapterDelegate(activity), + catAdapterDelegate(), + DogAdapterDelegate(activity), + GeckoAdapterDelegate(activity), + SnakeListItemAdapterDelegate(activity) +) \ No newline at end of file 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 new file mode 100644 index 0000000..c8a4da4 --- /dev/null +++ b/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/dsl/DslSample.kt @@ -0,0 +1,20 @@ +package com.hannesdorfmann.adapterdelegates4.sample.dsl + +import android.util.Log +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer +import com.hannesdorfmann.adapterdelegates4.sample.R +import com.hannesdorfmann.adapterdelegates4.sample.model.Cat +import com.hannesdorfmann.adapterdelegates4.sample.model.DisplayableItem +import kotlinx.android.synthetic.main.item_cat.* + +// Example +fun catAdapterDelegate() = adapterDelegateLayoutContainer(R.layout.item_cat) { + + name.setOnClickListener { + Log.d("Click", "Click on $item") + } + + bind { + name.text = item.name + } +} diff --git a/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/model/Animal.java b/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/model/Animal.java index cefb30b..a6a4e10 100644 --- a/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/model/Animal.java +++ b/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/model/Animal.java @@ -16,18 +16,26 @@ package com.hannesdorfmann.adapterdelegates4.sample.model; +import androidx.annotation.NonNull; + /** * @author Hannes Dorfmann */ public class Animal implements DisplayableItem { - private String name; + private String name; + + public Animal(String name) { + this.name = name; + } - public Animal(String name) { - this.name = name; - } + public String getName() { + return name; + } - public String getName() { - return name; - } + @NonNull + @Override + public String toString() { + return name + " " + super.toString(); + } } diff --git a/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/model/Cat.java b/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/model/Cat.java index 58100f7..5fb90ad 100644 --- a/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/model/Cat.java +++ b/app/src/main/java/com/hannesdorfmann/adapterdelegates4/sample/model/Cat.java @@ -24,6 +24,4 @@ public class Cat extends Animal { public Cat(String name) { super(name); } - - } diff --git a/build.gradle b/build.gradle index 3b8d3c2..0f1b1d5 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,15 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. buildscript { + ext.kotlin_version = '1.3.40' repositories { google() jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:3.3.2' + classpath 'com.android.tools.build:gradle:3.4.1' classpath 'com.vanniktech:gradle-maven-publish-plugin:0.6.0' + classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" // NOTE: Do not place your application dependencies here; they belong // in the individual module build.gradle files diff --git a/gradle.properties b/gradle.properties index ec92270..0c682d8 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,7 +17,7 @@ # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -VERSION_NAME=4.0.1-SNAPSHOT +VERSION_NAME=4.1.0-SNAPSHOT VERSION_CODE=401 GROUP=com.hannesdorfmann diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 739e62b..f6b0e9f 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Sun Mar 31 17:59:48 CEST 2019 +#Wed Jul 03 19:42:09 CEST 2019 distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-4.10.1-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-5.1.1-all.zip diff --git a/kotlin-dsl-layoutcontainer/.gitignore b/kotlin-dsl-layoutcontainer/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/kotlin-dsl-layoutcontainer/.gitignore @@ -0,0 +1 @@ +/build diff --git a/kotlin-dsl-layoutcontainer/build.gradle b/kotlin-dsl-layoutcontainer/build.gradle new file mode 100644 index 0000000..49c2899 --- /dev/null +++ b/kotlin-dsl-layoutcontainer/build.gradle @@ -0,0 +1,55 @@ +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 + } +} + +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-layoutcontainer/gradle.properties b/kotlin-dsl-layoutcontainer/gradle.properties new file mode 100644 index 0000000..c55a9af --- /dev/null +++ b/kotlin-dsl-layoutcontainer/gradle.properties @@ -0,0 +1,18 @@ +# +# Copyright (c) 2015 Hannes Dorfmann. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +POM_NAME = AdapterDelegates-Kotlin-Dsl +POM_ARTIFACT_ID = adapterdelegates4-kotlin-dsl-layoutcontainer +POM_PACKAGING = aar diff --git a/kotlin-dsl-layoutcontainer/proguard-rules.pro b/kotlin-dsl-layoutcontainer/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/kotlin-dsl-layoutcontainer/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-layoutcontainer/src/main/AndroidManifest.xml b/kotlin-dsl-layoutcontainer/src/main/AndroidManifest.xml new file mode 100644 index 0000000..34f1e1f --- /dev/null +++ b/kotlin-dsl-layoutcontainer/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + 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 new file mode 100644 index 0000000..861a369 --- /dev/null +++ b/kotlin-dsl-layoutcontainer/src/main/java/com/hannesdorfmann/adapterdelegates4/dsl/LayoutContainerListAdapterDelegateDsl.kt @@ -0,0 +1,241 @@ +package com.hannesdorfmann.adapterdelegates4.dsl + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.IdRes +import androidx.annotation.LayoutRes +import androidx.recyclerview.widget.RecyclerView +import com.hannesdorfmann.adapterdelegates4.AbsListItemAdapterDelegate +import com.hannesdorfmann.adapterdelegates4.AdapterDelegate +import kotlinx.android.extensions.LayoutContainer +import java.lang.IllegalStateException +import kotlin.IllegalArgumentException + +/** + * Simple DSL builder to create an [AdapterDelegate] that is backed by a [List] as dataset. + * This DSL builds on top of [LayoutContainer] so that no findViewById is needed anymore. + * + * @param layout The android xml layout resource that contains the layout 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 adapterDelegateLayoutContainer( + @LayoutRes layout: Int, + noinline on: (item: T, items: List, position: Int) -> Boolean = { item, _, _ -> item is I }, + noinline layoutInflater: (parent: ViewGroup, layoutRes: Int) -> View = { parent, layout -> + LayoutInflater.from(parent.context).inflate( + layout, + parent, + false + ) + }, + noinline block: AdapterDelegateLayoutContainerViewHolder.() -> Unit +): AdapterDelegate> { + + return DslLayoutContainerListAdapterDelegate( + layout = layout, + on = on, + intializerBlock = block, + layoutInflater = layoutInflater + ) +} + +class DslLayoutContainerListAdapterDelegate( + @LayoutRes private val layout: Int, + private val on: (item: T, items: List, position: Int) -> Boolean, + private val intializerBlock: AdapterDelegateLayoutContainerViewHolder.() -> Unit, + private val layoutInflater: (parent: ViewGroup, layoutRes: Int) -> View +) : AbsListItemAdapterDelegate>() { + + override fun isForViewType(item: T, items: MutableList, position: Int): Boolean = on( + item, items, position + ) + + override fun onCreateViewHolder(parent: ViewGroup): AdapterDelegateLayoutContainerViewHolder = + AdapterDelegateLayoutContainerViewHolder( + layoutInflater(parent, layout) + ).also { + intializerBlock(it) + } + + override fun onBindViewHolder( + item: I, + holder: AdapterDelegateLayoutContainerViewHolder, + 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 AdapterDelegateLayoutContainerViewHolder) + + vh._onViewRecycled?.invoke() + } + + override fun onFailedToRecycleView(holder: RecyclerView.ViewHolder): Boolean { + @Suppress("UNCHECKED_CAST") + val vh = (holder as AdapterDelegateLayoutContainerViewHolder) + 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 AdapterDelegateLayoutContainerViewHolder) + vh._onViewAttachedToWindow?.invoke() + } + + override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { + @Suppress("UNCHECKED_CAST") + val vh = (holder as AdapterDelegateLayoutContainerViewHolder) + vh._onViewDetachedFromWindow?.invoke() + } +} + +/** + * ViewHolder that is used internally if you use [adapterDelegate] DSL to create your Adapter + */ +class AdapterDelegateLayoutContainerViewHolder( + override val containerView: View +) : RecyclerView.ViewHolder(containerView), LayoutContainer { + + 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 + } + + /** + * 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-layoutcontainer/src/main/res/values/strings.xml b/kotlin-dsl-layoutcontainer/src/main/res/values/strings.xml new file mode 100644 index 0000000..2d2c947 --- /dev/null +++ b/kotlin-dsl-layoutcontainer/src/main/res/values/strings.xml @@ -0,0 +1,3 @@ + + kotlindsllayoutcontainer + diff --git a/kotlin-dsl-layoutcontainer/src/test/java/com/hannesdorfmann/adapterdelegates4/LayoutContainerListAdapterDelegateDslTest.kt b/kotlin-dsl-layoutcontainer/src/test/java/com/hannesdorfmann/adapterdelegates4/LayoutContainerListAdapterDelegateDslTest.kt new file mode 100644 index 0000000..2f687f8 --- /dev/null +++ b/kotlin-dsl-layoutcontainer/src/test/java/com/hannesdorfmann/adapterdelegates4/LayoutContainerListAdapterDelegateDslTest.kt @@ -0,0 +1,329 @@ +package com.hannesdorfmann.adapterdelegates4 + +import android.view.View +import android.view.ViewGroup +import androidx.annotation.LayoutRes +import androidx.recyclerview.widget.RecyclerView +import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateLayoutContainerViewHolder +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegateLayoutContainer +import org.junit.Assert +import org.junit.Test +import org.mockito.Mockito +import java.lang.IllegalStateException + +class LayoutContainerListAdapterDelegateDslTest { + + data class Item(val name: String) + + private fun fakeLayoutInflater(layoutToInflate: Int): Pair<(ViewGroup, Int) -> View, ViewGroup> { + val viewGroup = Mockito.mock(ViewGroup::class.java) + val view = Mockito.mock(View::class.java) + + val inflater = { parent: ViewGroup, layoutRes: Int -> + Assert.assertSame(viewGroup, parent) + Assert.assertEquals(layoutToInflate, layoutRes) + view + } + + return Pair(inflater, viewGroup) + } + + @Test + fun `init block is called once - bind multiple times`() { + + val layoutToInflate = 999 + val (inflater, viewGroup) = fakeLayoutInflater(layoutToInflate) + val items = listOf(Any(), Any()) + var payload = emptyList() + + var viewHolder: AdapterDelegateLayoutContainerViewHolder? = null + + var initCalled = 0 + var bindCalled = 0 + var bindPayload = emptyList() + var boundItemInBindBlock: Any? = null + + val delegate = adapterDelegateLayoutContainer( + layoutToInflate, + layoutInflater = inflater + ) { + 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 delegate = adapterDelegateLayoutContainer(0) { + } + + 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`() { + + var onBlockCalled = 0 + var onBlockItem: Any? = null + var onBlockList: List? = null + var onBlockPosition = -1 + + val delegate = adapterDelegateLayoutContainer( + layout = 0, + 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 layoutToInflate = 0 + val (inflater, viewGroup) = fakeLayoutInflater(layoutToInflate) + + try { + val delegate = adapterDelegateLayoutContainer( + layout = layoutToInflate, + layoutInflater = inflater + ) { + 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 (inflater, viewGroup) = fakeLayoutInflater(0) + var called = 0 + var viewHolder: AdapterDelegateLayoutContainerViewHolder? = null + val delegate = adapterDelegateLayoutContainer( + layout = 0, + layoutInflater = inflater + ) { + viewHolder = this + onViewRecycled { + called++ + } + } + + delegate.onCreateViewHolder(viewGroup) + Assert.assertNotNull(viewHolder) + delegate.onViewRecycled(viewHolder!!) + Assert.assertEquals(1, called) + } + + @Test + fun `multiple onViewRecycled throws exception`() { + val layoutToInflate = 0 + val (inflater, viewGroup) = fakeLayoutInflater(layoutToInflate) + + try { + val delegate = adapterDelegateLayoutContainer( + layout = layoutToInflate, + layoutInflater = inflater + ) { + 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 (inflater, viewGroup) = fakeLayoutInflater(0) + var called = 0 + var viewHolder: AdapterDelegateLayoutContainerViewHolder? = null + val delegate = adapterDelegateLayoutContainer( + layout = 0, + layoutInflater = inflater + ) { + 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 layoutToInflate = 0 + val (inflater, viewGroup) = fakeLayoutInflater(layoutToInflate) + + try { + val delegate = adapterDelegateLayoutContainer( + layout = layoutToInflate, + layoutInflater = inflater + ) { + 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 (inflater, viewGroup) = fakeLayoutInflater(0) + var called = 0 + var viewHolder: AdapterDelegateLayoutContainerViewHolder? = null + val delegate = adapterDelegateLayoutContainer( + layout = 0, + layoutInflater = inflater + ) { + viewHolder = this + onViewAttachedToWindow { + called++ + } + } + + delegate.onCreateViewHolder(viewGroup) + Assert.assertNotNull(viewHolder) + delegate.onViewAttachedToWindow(viewHolder!!) + Assert.assertEquals(1, called) + } + + @Test + fun `multiple onViewAttachedToWindow throws exception`() { + val layoutToInflate = 0 + val (inflater, viewGroup) = fakeLayoutInflater(layoutToInflate) + + try { + val delegate = adapterDelegateLayoutContainer( + layout = layoutToInflate, + layoutInflater = inflater + ) { + 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 (inflater, viewGroup) = fakeLayoutInflater(0) + var called = 0 + var viewHolder: AdapterDelegateLayoutContainerViewHolder? = null + val delegate = adapterDelegateLayoutContainer( + layout = 0, + layoutInflater = inflater + ) { + viewHolder = this + onViewDetachedFromWindow { + called++ + } + } + + delegate.onCreateViewHolder(viewGroup) + Assert.assertNotNull(viewHolder) + delegate.onViewDetachedFromWindow(viewHolder!!) + Assert.assertEquals(1, called) + } + + @Test + fun `multiple onViewDetachedFromWindow throws exception`() { + val layoutToInflate = 0 + val (inflater, viewGroup) = fakeLayoutInflater(layoutToInflate) + + try { + val delegate = adapterDelegateLayoutContainer( + layout = layoutToInflate, + layoutInflater = inflater + ) { + 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/kotlin-dsl/.gitignore b/kotlin-dsl/.gitignore new file mode 100644 index 0000000..796b96d --- /dev/null +++ b/kotlin-dsl/.gitignore @@ -0,0 +1 @@ +/build diff --git a/kotlin-dsl/build.gradle b/kotlin-dsl/build.gradle new file mode 100644 index 0000000..c63c9c7 --- /dev/null +++ b/kotlin-dsl/build.gradle @@ -0,0 +1,50 @@ +apply plugin: 'com.android.library' +apply plugin: 'kotlin-android' + +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 + } +} + +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/gradle.properties b/kotlin-dsl/gradle.properties new file mode 100644 index 0000000..1fa8e33 --- /dev/null +++ b/kotlin-dsl/gradle.properties @@ -0,0 +1,18 @@ +# +# Copyright (c) 2015 Hannes Dorfmann. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +POM_NAME = AdapterDelegates-Kotlin-Dsl +POM_ARTIFACT_ID = adapterdelegates4-kotlin-dsl +POM_PACKAGING = aar diff --git a/kotlin-dsl/proguard-rules.pro b/kotlin-dsl/proguard-rules.pro new file mode 100644 index 0000000..f1b4245 --- /dev/null +++ b/kotlin-dsl/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/src/main/AndroidManifest.xml b/kotlin-dsl/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a4c20c3 --- /dev/null +++ b/kotlin-dsl/src/main/AndroidManifest.xml @@ -0,0 +1,2 @@ + diff --git a/kotlin-dsl/src/main/java/com/hannesdorfmann/adapterdelegates4/dsl/ListAdapterDelegateDsl.kt b/kotlin-dsl/src/main/java/com/hannesdorfmann/adapterdelegates4/dsl/ListAdapterDelegateDsl.kt new file mode 100644 index 0000000..c60fbbb --- /dev/null +++ b/kotlin-dsl/src/main/java/com/hannesdorfmann/adapterdelegates4/dsl/ListAdapterDelegateDsl.kt @@ -0,0 +1,237 @@ +package com.hannesdorfmann.adapterdelegates4.dsl + +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import androidx.annotation.IdRes +import androidx.annotation.LayoutRes +import androidx.recyclerview.widget.RecyclerView +import com.hannesdorfmann.adapterdelegates4.AbsListItemAdapterDelegate +import com.hannesdorfmann.adapterdelegates4.AdapterDelegate +import java.lang.IllegalStateException +import kotlin.IllegalArgumentException + +/** + * Simple DSL builder to create an [AdapterDelegate] that is backed by a [List] as dataset. + * + * @param layout The android xml layout resource that contains the layout 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 adapterDelegate( + @LayoutRes layout: Int, + noinline on: (item: T, items: List, position: Int) -> Boolean = { item, _, _ -> item is I }, + noinline layoutInflater: (parent: ViewGroup, layoutRes: Int) -> View = { parent, layout -> + LayoutInflater.from(parent.context).inflate( + layout, + parent, + false + ) + }, + noinline block: AdapterDelegateViewHolder.() -> Unit +): AdapterDelegate> { + + return DslListAdapterDelegate( + layout = layout, + on = on, + intializerBlock = block, + layoutInflater = layoutInflater + ) +} + +class DslListAdapterDelegate( + @LayoutRes private val layout: Int, + private val on: (item: T, items: List, position: Int) -> Boolean, + private val intializerBlock: AdapterDelegateViewHolder.() -> Unit, + private val layoutInflater: (parent: ViewGroup, layout: Int) -> View +) : AbsListItemAdapterDelegate>() { + + override fun isForViewType(item: T, items: MutableList, position: Int): Boolean = on( + item, items, position + ) + + override fun onCreateViewHolder(parent: ViewGroup): AdapterDelegateViewHolder = + AdapterDelegateViewHolder( + layoutInflater(parent, layout) + ).also { + intializerBlock(it) + } + + override fun onBindViewHolder( + item: I, + holder: AdapterDelegateViewHolder, + 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 AdapterDelegateViewHolder) + + vh._onViewRecycled?.invoke() + } + + override fun onFailedToRecycleView(holder: RecyclerView.ViewHolder): Boolean { + @Suppress("UNCHECKED_CAST") + val vh = (holder as AdapterDelegateViewHolder) + 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 AdapterDelegateViewHolder) + vh._onViewAttachedToWindow?.invoke() + } + + override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) { + @Suppress("UNCHECKED_CAST") + val vh = (holder as AdapterDelegateViewHolder) + vh._onViewDetachedFromWindow?.invoke() + } +} + +/** + * ViewHolder that is used internally if you use [adapterDelegate] DSL to create your Adapter + */ +class AdapterDelegateViewHolder(view: View) : 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 + } + + /** + * 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.") + } + this._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/src/test/java/com/hannesdorfmann/adapterdelegates4/ListAdapterDelegateDslTest.kt b/kotlin-dsl/src/test/java/com/hannesdorfmann/adapterdelegates4/ListAdapterDelegateDslTest.kt new file mode 100644 index 0000000..2e6ed3b --- /dev/null +++ b/kotlin-dsl/src/test/java/com/hannesdorfmann/adapterdelegates4/ListAdapterDelegateDslTest.kt @@ -0,0 +1,326 @@ +package com.hannesdorfmann.adapterdelegates4 + +import android.view.View +import android.view.ViewGroup +import androidx.recyclerview.widget.RecyclerView +import com.hannesdorfmann.adapterdelegates4.dsl.AdapterDelegateViewHolder +import com.hannesdorfmann.adapterdelegates4.dsl.adapterDelegate +import org.junit.Assert +import org.junit.Test +import org.mockito.Mockito +import java.lang.IllegalStateException + +class ListAdapterDelegateDslTest { + + data class Item(val name: String) + + private fun fakeLayoutInflater(layoutToInflate: Int): Pair<(ViewGroup, Int) -> View, ViewGroup> { + val viewGroup = Mockito.mock(ViewGroup::class.java) + val view = Mockito.mock(View::class.java) + + val inflater = { parent: ViewGroup, layoutRes: Int -> + Assert.assertSame(viewGroup, parent) + Assert.assertEquals(layoutToInflate, layoutRes) + view + } + + return Pair(inflater, viewGroup) + } + + @Test + fun `init block is called once - bind multiple times`() { + val layoutToInflate = 999 + val (inflater, viewGroup) = fakeLayoutInflater(layoutToInflate) + val items = listOf(Any(), Any()) + var payload = emptyList() + + var viewHolder: AdapterDelegateViewHolder? = null + + var initCalled = 0 + var bindCalled = 0 + var bindPayload = emptyList() + var boundItemInBindBlock: Any? = null + + val delegate = adapterDelegate( + layoutToInflate, + layoutInflater = inflater + ) { + 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 delegate = adapterDelegate(0) { + } + + 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`() { + + var onBlockCalled = 0 + var onBlockItem: Any? = null + var onBlockList: List? = null + var onBlockPosition = -1 + + val delegate = adapterDelegate( + layout = 0, + 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 layoutToInflate = 0 + val (inflater, viewGroup) = fakeLayoutInflater(layoutToInflate) + + try { + val delegate = adapterDelegate( + layout = 0, + layoutInflater = inflater + ) { + 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 (inflater, viewGroup) = fakeLayoutInflater(0) + var called = 0 + var viewHolder: AdapterDelegateViewHolder? = null + val delegate = adapterDelegate( + layout = 0, + layoutInflater = inflater + ) { + viewHolder = this + onViewRecycled { + called++ + } + } + + delegate.onCreateViewHolder(viewGroup) + Assert.assertNotNull(viewHolder) + delegate.onViewRecycled(viewHolder!!) + Assert.assertEquals(1, called) + } + + @Test + fun `multiple onViewRecycled throws exception`() { + val layoutToInflate = 0 + val (inflater, viewGroup) = fakeLayoutInflater(layoutToInflate) + + try { + val delegate = adapterDelegate( + layout = 0, + layoutInflater = inflater + ) { + 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 (inflater, viewGroup) = fakeLayoutInflater(0) + var called = 0 + var viewHolder: AdapterDelegateViewHolder? = null + val delegate = adapterDelegate( + layout = 0, + layoutInflater = inflater + ) { + 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 layoutToInflate = 0 + val (inflater, viewGroup) = fakeLayoutInflater(layoutToInflate) + + try { + val delegate = adapterDelegate( + layout = 0, + layoutInflater = inflater + ) { + 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 (inflater, viewGroup) = fakeLayoutInflater(0) + var called = 0 + var viewHolder: AdapterDelegateViewHolder? = null + val delegate = adapterDelegate( + layout = 0, + layoutInflater = inflater + ) { + viewHolder = this + onViewAttachedToWindow { + called++ + } + } + + delegate.onCreateViewHolder(viewGroup) + Assert.assertNotNull(viewHolder) + delegate.onViewAttachedToWindow(viewHolder!!) + Assert.assertEquals(1, called) + } + + @Test + fun `multiple onViewAttachedToWindow throws exception`() { + val layoutToInflate = 0 + val (inflater, viewGroup) = fakeLayoutInflater(layoutToInflate) + + try { + val delegate = adapterDelegate( + layout = 0, + layoutInflater = inflater + ) { + 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 (inflater, viewGroup) = fakeLayoutInflater(0) + var called = 0 + var viewHolder: AdapterDelegateViewHolder? = null + val delegate = adapterDelegate( + layout = 0, + layoutInflater = inflater + ) { + viewHolder = this + onViewDetachedFromWindow { + called++ + } + } + + delegate.onCreateViewHolder(viewGroup) + Assert.assertNotNull(viewHolder) + delegate.onViewDetachedFromWindow(viewHolder!!) + Assert.assertEquals(1, called) + } + + @Test + fun `multiple onViewDetachedFromWindow throws exception`() { + val layoutToInflate = 0 + val (inflater, viewGroup) = fakeLayoutInflater(layoutToInflate) + + try { + val delegate = adapterDelegate( + layout = 0, + layoutInflater = inflater + ) { + 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/build.gradle b/library/build.gradle index 67840a9..086921b 100644 --- a/library/build.gradle +++ b/library/build.gradle @@ -46,4 +46,4 @@ dependencies { } // 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' +// apply from: 'https://raw.githubusercontent.com/Tickaroo/findbugs-script/master/findbugs.gradle' diff --git a/library/src/main/java/com/hannesdorfmann/adapterdelegates4/AbsDelegationAdapter.java b/library/src/main/java/com/hannesdorfmann/adapterdelegates4/AbsDelegationAdapter.java index 8c47b29..0768bd7 100644 --- a/library/src/main/java/com/hannesdorfmann/adapterdelegates4/AbsDelegationAdapter.java +++ b/library/src/main/java/com/hannesdorfmann/adapterdelegates4/AbsDelegationAdapter.java @@ -72,6 +72,19 @@ public AbsDelegationAdapter(@NonNull AdapterDelegatesManager delegatesManager this.delegatesManager = delegatesManager; } + /** + * Adds a list of {@link AdapterDelegate}s + * + * @param delegates Items to add + * @since 4.1.0 + */ + public AbsDelegationAdapter(@NonNull AdapterDelegate... delegates) { + this(); + for (int i = 0; i < delegates.length; i++) { + delegatesManager.addDelegate(delegates[i]); + } + } + @NonNull @Override public RecyclerView.ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) { diff --git a/library/src/main/java/com/hannesdorfmann/adapterdelegates4/ListDelegationAdapter.java b/library/src/main/java/com/hannesdorfmann/adapterdelegates4/ListDelegationAdapter.java index 57b9085..a66ad4e 100644 --- a/library/src/main/java/com/hannesdorfmann/adapterdelegates4/ListDelegationAdapter.java +++ b/library/src/main/java/com/hannesdorfmann/adapterdelegates4/ListDelegationAdapter.java @@ -42,12 +42,23 @@ public class ListDelegationAdapter> extends AbsDelegationAdapter { public ListDelegationAdapter() { + super(); } public ListDelegationAdapter(@NonNull AdapterDelegatesManager delegatesManager) { super(delegatesManager); } + /** + * Adds a list of {@link AdapterDelegate}s + * + * @param delegates + * @since 4.1.0 + */ + public ListDelegationAdapter(@NonNull AdapterDelegate... delegates) { + super(delegates); + } + @Override public int getItemCount() { return items == null ? 0 : items.size(); diff --git a/library/src/test/java/com/hannesdorfmann/adapterdelegates4/AbsDelegationAdapterTest.java b/library/src/test/java/com/hannesdorfmann/adapterdelegates4/AbsDelegationAdapterTest.java index d969e73..135ee49 100644 --- a/library/src/test/java/com/hannesdorfmann/adapterdelegates4/AbsDelegationAdapterTest.java +++ b/library/src/test/java/com/hannesdorfmann/adapterdelegates4/AbsDelegationAdapterTest.java @@ -16,7 +16,7 @@ public class AbsDelegationAdapterTest { @Test public void delegatesManagerNull() { try { - AbsDelegationAdapter adapter = new AbsDelegationAdapter(null) { + AbsDelegationAdapter adapter = new AbsDelegationAdapter((AdapterDelegatesManager)null) { @Override public int getItemCount() { return 0; diff --git a/library/src/test/java/com/hannesdorfmann/adapterdelegates4/ListDelegationAdapterTest.java b/library/src/test/java/com/hannesdorfmann/adapterdelegates4/ListDelegationAdapterTest.java index 1cfe67f..ba636f0 100644 --- a/library/src/test/java/com/hannesdorfmann/adapterdelegates4/ListDelegationAdapterTest.java +++ b/library/src/test/java/com/hannesdorfmann/adapterdelegates4/ListDelegationAdapterTest.java @@ -1,6 +1,7 @@ package com.hannesdorfmann.adapterdelegates4; import java.util.List; + import org.junit.Assert; import org.junit.Test; @@ -9,46 +10,52 @@ */ public class ListDelegationAdapterTest { - @Test public void delegatesManagerNull() { - try { - ListDelegationAdapter> adapter = new ListDelegationAdapter>(null) { - @Override public int getItemCount() { - return 0; + @Test + public void delegatesManagerNull() { + try { + ListDelegationAdapter> adapter = new ListDelegationAdapter>((AdapterDelegatesManager) null) { + @Override + public int getItemCount() { + return 0; + } + }; + Assert.fail("Expected NullPointerException"); + } catch (NullPointerException e) { + Assert.assertEquals("AdapterDelegatesManager is null", e.getMessage()); } - }; - Assert.fail("Expected NullPointerException"); - } catch (NullPointerException e) { - Assert.assertEquals("AdapterDelegatesManager is null", e.getMessage()); } - } - @Test public void checkDelegatesManagerInstance() { + @Test + public void checkDelegatesManagerInstance() { - final AdapterDelegatesManager> manager = new AdapterDelegatesManager<>(); + final AdapterDelegatesManager> manager = new AdapterDelegatesManager<>(); - ListDelegationAdapter> adapter = new ListDelegationAdapter>(manager) { - @Override public int getItemCount() { - // Hacky but does the job - Assert.assertTrue(manager == this.delegatesManager); - return 0; - } - }; + ListDelegationAdapter> adapter = new ListDelegationAdapter>(manager) { + @Override + public int getItemCount() { + // Hacky but does the job + Assert.assertTrue(manager == this.delegatesManager); + return 0; + } + }; - adapter.getItemCount(); - } + adapter.getItemCount(); + } - @Test public void checkNewAdapterDelegatesManagerInstanceNotNull() { + @Test + public void checkNewAdapterDelegatesManagerInstanceNotNull() { - // Empty constructor should produce a new instance of AdapterDelegatesManager - ListDelegationAdapter> adapter = new ListDelegationAdapter>() { - @Override public int getItemCount() { - // Hacky but does the job - Assert.assertNotNull(this.delegatesManager); - return 0; - } - }; + // Empty constructor should produce a new instance of AdapterDelegatesManager + ListDelegationAdapter> adapter = new ListDelegationAdapter>() { + @Override + public int getItemCount() { + // Hacky but does the job + Assert.assertNotNull(this.delegatesManager); + return 0; + } + }; - adapter.getItemCount(); - } + adapter.getItemCount(); + } } diff --git a/paging/src/main/java/com/hannesdorfmann/adapterdelegates4/paging/PagedListDelegationAdapter.java b/paging/src/main/java/com/hannesdorfmann/adapterdelegates4/paging/PagedListDelegationAdapter.java index 676220f..69ceb74 100644 --- a/paging/src/main/java/com/hannesdorfmann/adapterdelegates4/paging/PagedListDelegationAdapter.java +++ b/paging/src/main/java/com/hannesdorfmann/adapterdelegates4/paging/PagedListDelegationAdapter.java @@ -2,6 +2,7 @@ import android.view.ViewGroup; +import com.hannesdorfmann.adapterdelegates4.AdapterDelegate; import com.hannesdorfmann.adapterdelegates4.AdapterDelegatesManager; import java.util.List; @@ -23,6 +24,18 @@ public class PagedListDelegationAdapter extends PagedListAdapter> delegatesManager; + /** + * @param diffCallback The Callback + * @param delegates The {@link AdapterDelegate}s that should be added + * @since 4.1.0 + */ + public PagedListDelegationAdapter(@NonNull DiffUtil.ItemCallback diffCallback, AdapterDelegate>... delegates) { + this(new AdapterDelegatesManager>(), diffCallback); + for (int i = 0; i < delegates.length; i++) { + delegatesManager.addDelegate(delegates[i]); + } + } + public PagedListDelegationAdapter(@NonNull DiffUtil.ItemCallback diffCallback) { this(new AdapterDelegatesManager>(), diffCallback); } diff --git a/settings.gradle b/settings.gradle index 7ed44a9..9d58c2d 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -include ':app', ':library', ':paging' +include ':app', ':library', ':paging', ':kotlin-dsl', ':kotlin-dsl-layoutcontainer'