├── example-app
├── .gitignore
├── src
│ └── main
│ │ ├── res
│ │ ├── mipmap-hdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-mdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── mipmap-xxxhdpi
│ │ │ ├── ic_launcher.webp
│ │ │ └── ic_launcher_round.webp
│ │ ├── drawable
│ │ │ ├── circle.xml
│ │ │ └── ic_launcher_background.xml
│ │ ├── mipmap-anydpi-v26
│ │ │ ├── ic_launcher.xml
│ │ │ └── ic_launcher_round.xml
│ │ ├── values
│ │ │ ├── strings.xml
│ │ │ ├── colors.xml
│ │ │ └── themes.xml
│ │ ├── drawable-v24
│ │ │ ├── ic_delete.xml
│ │ │ ├── ic_favorite.xml
│ │ │ ├── ic_favorite_not.xml
│ │ │ └── ic_launcher_foreground.xml
│ │ └── layout
│ │ │ ├── item_header.xml
│ │ │ ├── activity_simple_multichoice.xml
│ │ │ ├── activity_main.xml
│ │ │ ├── item_selectable.xml
│ │ │ └── item_cat.xml
│ │ ├── java
│ │ └── com
│ │ │ └── elveum
│ │ │ └── elementadapter
│ │ │ └── app
│ │ │ ├── App.kt
│ │ │ ├── model
│ │ │ ├── Cat.kt
│ │ │ └── CatsRepository.kt
│ │ │ ├── CatListItem.kt
│ │ │ ├── MainViewModel.kt
│ │ │ ├── SimpleMultiChoiceActivity.kt
│ │ │ └── MainActivity.kt
│ │ └── AndroidManifest.xml
├── proguard-rules.pro
└── build.gradle
├── recyclerview-element-adapter
├── .gitignore
├── consumer-rules.pro
├── src
│ └── main
│ │ ├── AndroidManifest.xml
│ │ ├── res
│ │ └── values
│ │ │ └── identifiers.xml
│ │ └── java
│ │ └── com
│ │ └── elveum
│ │ └── elementadapter
│ │ ├── dsl
│ │ ├── CustomListenerScope.kt
│ │ ├── IndexScope.kt
│ │ ├── MultiAdapter.kt
│ │ ├── ItemCallbackDelegate.kt
│ │ ├── AdapterScope.kt
│ │ └── ConcreteItemTypeScope.kt
│ │ ├── CommonBindings.kt
│ │ ├── delegate
│ │ ├── DslDelegateEntry.kt
│ │ └── AdapterDelegate.kt
│ │ ├── ElementListAdapter.kt
│ │ └── DslEntry.kt
├── proguard-rules.pro
└── build.gradle
├── docs
└── screenshot.png
├── .github
└── FUNDING.yml
├── gradle
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── .gitignore
├── settings.gradle
├── gradle.properties
├── publish.gradle
├── publish-library.gradle
├── gradlew.bat
├── gradlew
├── LICENSE
└── README.md
/example-app/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/recyclerview-element-adapter/.gitignore:
--------------------------------------------------------------------------------
1 | /build
--------------------------------------------------------------------------------
/recyclerview-element-adapter/consumer-rules.pro:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/docs/screenshot.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romychab/element-adapter/HEAD/docs/screenshot.png
--------------------------------------------------------------------------------
/.github/FUNDING.yml:
--------------------------------------------------------------------------------
1 | # These are supported funding model platforms
2 |
3 | custom: ["https://www.buymeacoffee.com/romychab"]
4 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romychab/element-adapter/HEAD/gradle/wrapper/gradle-wrapper.jar
--------------------------------------------------------------------------------
/example-app/src/main/res/mipmap-hdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romychab/element-adapter/HEAD/example-app/src/main/res/mipmap-hdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/example-app/src/main/res/mipmap-mdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romychab/element-adapter/HEAD/example-app/src/main/res/mipmap-mdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/example-app/src/main/res/mipmap-xhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romychab/element-adapter/HEAD/example-app/src/main/res/mipmap-xhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/example-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romychab/element-adapter/HEAD/example-app/src/main/res/mipmap-xxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/example-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romychab/element-adapter/HEAD/example-app/src/main/res/mipmap-xxxhdpi/ic_launcher.webp
--------------------------------------------------------------------------------
/example-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romychab/element-adapter/HEAD/example-app/src/main/res/mipmap-hdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/example-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romychab/element-adapter/HEAD/example-app/src/main/res/mipmap-mdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/example-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romychab/element-adapter/HEAD/example-app/src/main/res/mipmap-xhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/example-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romychab/element-adapter/HEAD/example-app/src/main/res/mipmap-xxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/example-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/romychab/element-adapter/HEAD/example-app/src/main/res/mipmap-xxxhdpi/ic_launcher_round.webp
--------------------------------------------------------------------------------
/recyclerview-element-adapter/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/example-app/src/main/java/com/elveum/elementadapter/app/App.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter.app
2 |
3 | import android.app.Application
4 | import dagger.hilt.android.HiltAndroidApp
5 |
6 | @HiltAndroidApp
7 | class App : Application()
--------------------------------------------------------------------------------
/example-app/src/main/res/drawable/circle.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/recyclerview-element-adapter/src/main/res/values/identifiers.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/example-app/src/main/java/com/elveum/elementadapter/app/model/Cat.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter.app.model
2 |
3 | data class Cat(
4 | val id: Long,
5 | val name: String,
6 | val photoUrl: String,
7 | val description: String,
8 | val isFavorite: Boolean
9 | )
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | #Tue Sep 06 16:40:34 EEST 2022
2 | distributionBase=GRADLE_USER_HOME
3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-bin.zip
4 | distributionPath=wrapper/dists
5 | zipStorePath=wrapper/dists
6 | zipStoreBase=GRADLE_USER_HOME
7 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/caches
5 | /.idea/libraries
6 | /.idea/modules.xml
7 | /.idea/workspace.xml
8 | /.idea/navEditor.xml
9 | /.idea/assetWizardSettings.xml
10 | .DS_Store
11 | /build
12 | /captures
13 | .externalNativeBuild
14 | .cxx
15 | local.properties
16 |
--------------------------------------------------------------------------------
/example-app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/settings.gradle:
--------------------------------------------------------------------------------
1 | dependencyResolutionManagement {
2 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
3 | repositories {
4 | google()
5 | mavenCentral()
6 | }
7 | }
8 | rootProject.name = "RecyclerView Element Adapter"
9 | include ':example-app'
10 | include ':recyclerview-element-adapter'
11 |
--------------------------------------------------------------------------------
/example-app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/example-app/src/main/res/values/strings.xml:
--------------------------------------------------------------------------------
1 |
2 | Cats 😼
3 | Cats: %1$d … %2$d
4 |
5 | @string/app_name
6 | Multi Choice
7 |
8 | Total selected: %1$d
9 |
--------------------------------------------------------------------------------
/example-app/src/main/res/drawable-v24/ic_delete.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/example-app/src/main/res/layout/item_header.xml:
--------------------------------------------------------------------------------
1 |
2 |
13 |
14 |
--------------------------------------------------------------------------------
/example-app/src/main/res/drawable-v24/ic_favorite.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/example-app/src/main/res/values/colors.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 | #FFBB86FC
4 | #FF6200EE
5 | #FF3700B3
6 | #FF03DAC5
7 | #FF018786
8 | #FF000000
9 | #FFFFFFFF
10 |
11 | @color/purple_700
12 | #ababab
13 |
14 | #223700B3
15 |
--------------------------------------------------------------------------------
/example-app/src/main/res/drawable-v24/ic_favorite_not.xml:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/example-app/src/main/java/com/elveum/elementadapter/app/CatListItem.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter.app
2 |
3 | sealed class CatListItem {
4 |
5 | data class Header(
6 | val headerId: Int,
7 | val fromIndex: Int,
8 | val toIndex: Int
9 | ) : CatListItem()
10 |
11 | data class Cat(
12 | val originCat: com.elveum.elementadapter.app.model.Cat
13 | ) : CatListItem() {
14 | val id: Long get() = originCat.id
15 | val name: String get() = originCat.name
16 | val photoUrl: String get() = originCat.photoUrl
17 | val description: String get() = originCat.description
18 | val isFavorite: Boolean get() = originCat.isFavorite
19 | }
20 |
21 | }
--------------------------------------------------------------------------------
/example-app/src/main/res/layout/activity_simple_multichoice.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
8 |
13 |
14 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/example-app/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/recyclerview-element-adapter/proguard-rules.pro:
--------------------------------------------------------------------------------
1 | # Add project specific ProGuard rules here.
2 | # You can control the set of applied configuration files using the
3 | # proguardFiles setting in build.gradle.
4 | #
5 | # For more details, see
6 | # http://developer.android.com/guide/developing/tools/proguard.html
7 |
8 | # If your project uses WebView with JS, uncomment the following
9 | # and specify the fully qualified class name to the JavaScript interface
10 | # class:
11 | #-keepclassmembers class fqcn.of.javascript.interface.for.webview {
12 | # public *;
13 | #}
14 |
15 | # Uncomment this to preserve the line number information for
16 | # debugging stack traces.
17 | #-keepattributes SourceFile,LineNumberTable
18 |
19 | # If you keep the line number information, uncomment this to
20 | # hide the original source file name.
21 | #-renamesourcefileattribute SourceFile
--------------------------------------------------------------------------------
/recyclerview-element-adapter/src/main/java/com/elveum/elementadapter/dsl/CustomListenerScope.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter.dsl
2 |
3 | import android.view.View
4 | import com.elveum.elementadapter.R
5 |
6 | interface CustomListenerIndexScope {
7 |
8 | /**
9 | * Get the current index of an element for which a callback has been called.
10 | */
11 | fun index(): Int
12 | }
13 |
14 | interface CustomListenerScope : CustomListenerIndexScope {
15 | /**
16 | * Get the current item attached to the view
17 | */
18 | fun item(): T
19 | }
20 |
21 | @Suppress("UNCHECKED_CAST")
22 | internal class CustomListenerScopeImpl(
23 | private val view: View
24 | ) : CustomListenerScope {
25 | override fun item(): T = view.getTag(R.id.element_entity_tag) as T
26 | override fun index(): Int = view.getTag(R.id.element_index_tag) as Int
27 | }
28 |
--------------------------------------------------------------------------------
/example-app/src/main/res/layout/activity_main.xml:
--------------------------------------------------------------------------------
1 |
2 |
8 |
9 |
17 |
18 |
--------------------------------------------------------------------------------
/recyclerview-element-adapter/src/main/java/com/elveum/elementadapter/dsl/IndexScope.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter.dsl
2 |
3 | class ElementWithIndex(
4 | val index: Int,
5 | val element: T,
6 | )
7 |
8 | interface IndexScope {
9 | /**
10 | * Get an index of `oldItem` or `newItem`.
11 | */
12 | fun index(item: T): Int
13 | }
14 |
15 | internal class IndexScopeImpl(
16 | private val oldElementWithIndex: ElementWithIndex,
17 | private val newElementWithIndex: ElementWithIndex,
18 | ) : IndexScope {
19 | override fun index(item: T): Int {
20 | if (item === oldElementWithIndex.element) return oldElementWithIndex.index
21 | if (item === newElementWithIndex.element) return newElementWithIndex.index
22 | throw IllegalArgumentException("Unknown item! You can pass only newItem or oldItem as an argument for index() call")
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/example-app/src/main/res/values/themes.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
16 |
--------------------------------------------------------------------------------
/recyclerview-element-adapter/src/main/java/com/elveum/elementadapter/CommonBindings.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter
2 |
3 | import android.content.Context
4 | import android.content.res.ColorStateList
5 | import android.content.res.Resources
6 | import android.widget.ImageView
7 | import androidx.annotation.ColorRes
8 | import androidx.annotation.StringRes
9 | import androidx.core.content.ContextCompat
10 | import androidx.viewbinding.ViewBinding
11 |
12 | fun ImageView.setTintColor(@ColorRes colorRes: Int) {
13 | imageTintList = ColorStateList.valueOf(
14 | ContextCompat.getColor(
15 | context,
16 | colorRes
17 | )
18 | )
19 | }
20 |
21 | fun B.context(): Context = this.root.context
22 |
23 | fun B.resources(): Resources = context().resources
24 |
25 | fun B.getString(@StringRes stringRes: Int, vararg formatArgs: Any?): String {
26 | return context().getString(stringRes, *formatArgs)
27 | }
28 |
29 | fun B.getColor(@ColorRes colorRes: Int): Int {
30 | return ContextCompat.getColor(context(), colorRes)
31 | }
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | # Project-wide Gradle settings.
2 | # IDE (e.g. Android Studio) users:
3 | # Gradle settings configured through the IDE *will override*
4 | # any settings specified in this file.
5 | # For more details on how to configure your build environment visit
6 | # http://www.gradle.org/docs/current/userguide/build_environment.html
7 | # Specifies the JVM arguments used for the daemon process.
8 | # The setting is particularly useful for tweaking memory settings.
9 | org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8
10 | # When configured, Gradle will run in incubating parallel mode.
11 | # This option should only be used with decoupled projects. More details, visit
12 | # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects
13 | # org.gradle.parallel=true
14 | # AndroidX package structure to make it clearer which packages are bundled with the
15 | # Android operating system, and which are packaged with your app"s APK
16 | # https://developer.android.com/topic/libraries/support-library/androidx-rn
17 | android.useAndroidX=true
18 | # Automatically convert third-party libraries to use AndroidX
19 | android.enableJetifier=true
20 | # Kotlin code style for this project: "official" or "obsolete":
21 | kotlin.code.style=official
22 |
--------------------------------------------------------------------------------
/example-app/src/main/res/layout/item_selectable.xml:
--------------------------------------------------------------------------------
1 |
2 |
9 |
10 |
16 |
17 |
29 |
30 |
--------------------------------------------------------------------------------
/recyclerview-element-adapter/src/main/java/com/elveum/elementadapter/dsl/MultiAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter.dsl
2 |
3 | import android.view.ViewGroup
4 | import androidx.recyclerview.widget.RecyclerView
5 | import androidx.viewbinding.ViewBinding
6 | import com.elveum.elementadapter.ElementListAdapter
7 | import com.elveum.elementadapter.delegate.AdapterDelegate
8 |
9 | class BindingHolder(
10 | val binding: ViewBinding
11 | ) : RecyclerView.ViewHolder(binding.root)
12 |
13 | internal class MultiAdapter(
14 | private val adapterDelegate: AdapterDelegate
15 | ) : ElementListAdapter(adapterDelegate.itemCallback()) {
16 |
17 | override fun getItemViewType(position: Int): Int {
18 | return adapterDelegate.getItemViewType(getItem(position))
19 | }
20 |
21 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder {
22 | return adapterDelegate.onCreateViewHolder(parent, viewType)
23 | }
24 |
25 | override fun onBindViewHolder(
26 | holder: BindingHolder,
27 | position: Int,
28 | payloads: MutableList
29 | ) {
30 | adapterDelegate.onBindViewHolder(holder, position, getItem(position), payloads)
31 | }
32 |
33 | override fun onBindViewHolder(holder: BindingHolder, position: Int) {
34 | }
35 |
36 | }
37 |
38 |
--------------------------------------------------------------------------------
/publish.gradle:
--------------------------------------------------------------------------------
1 | ext["signing.keyId"] = ''
2 | ext["signing.password"] = ''
3 | ext["signing.key"] = ''
4 | ext["ossrhTokenUsername"] = ''
5 | ext["ossrhTokenPassword"] = ''
6 | ext["sonatypeStagingProfileId"] = ''
7 |
8 | File localProperties = project.rootProject.file('local.properties')
9 | if (localProperties.exists()) {
10 | Properties p = new Properties()
11 | new FileInputStream(localProperties).withCloseable { is -> p.load(is) }
12 | p.each { name, value -> ext[name] = value }
13 | } else {
14 | ext["ossrhTokenUsername"] = System.getenv('OSSRH_TOKEN_USERNAME')
15 | ext["ossrhTokenPassword"] = System.getenv('OSSRH_TOKEN_PASSWORD')
16 | ext["sonatypeStagingProfileId"] = System.getenv('SONATYPE_STAGING_PROFILE_ID')
17 | ext["signing.keyId"] = System.getenv('SIGNING_KEY_ID')
18 | ext["signing.password"] = System.getenv('SIGNING_PASSWORD')
19 | ext["signing.key"] = System.getenv('SIGNING_KEY')
20 | }
21 |
22 | nexusPublishing {
23 | repositories {
24 | sonatype {
25 | stagingProfileId = sonatypeStagingProfileId
26 | username = ossrhTokenUsername
27 | password = ossrhTokenPassword
28 | nexusUrl.set(uri("https://s01.oss.sonatype.org/service/local/"))
29 | snapshotRepositoryUrl.set(uri("https://s01.oss.sonatype.org/content/repositories/snapshots/"))
30 | }
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/example-app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 |
5 |
6 |
7 |
15 |
16 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
--------------------------------------------------------------------------------
/recyclerview-element-adapter/src/main/java/com/elveum/elementadapter/delegate/DslDelegateEntry.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter.delegate
2 |
3 | import androidx.viewbinding.ViewBinding
4 | import com.elveum.elementadapter.adapter
5 | import com.elveum.elementadapter.addBinding
6 | import com.elveum.elementadapter.dsl.AdapterScope
7 | import com.elveum.elementadapter.dsl.AdapterScopeImpl
8 | import com.elveum.elementadapter.dsl.ConcreteItemTypeScope
9 | import com.elveum.elementadapter.simpleAdapter
10 |
11 | /**
12 | * Create an adapter delegate which can be used as a bridge between this library
13 | * and either other third-party libraries or your own custom adapters.
14 | *
15 | * This method is similar to [adapter] but it returns a delegate instead of adapter.
16 | */
17 | fun adapterDelegate(block: AdapterScope.() -> Unit): AdapterDelegate {
18 | val adapterScope = AdapterScopeImpl()
19 | adapterScope.block()
20 | return adapterScope.toAdapterDelegate()
21 | }
22 |
23 | /**
24 | * Create an adapter delegate which can be used as a bridge between this library
25 | * and either other third-party libraries or your own custom adapters.
26 | *
27 | * This method is similar to [simpleAdapter] but it returns a delegate instead of adapter.
28 | */
29 | inline fun simpleAdapterDelegate(
30 | noinline block: ConcreteItemTypeScope.() -> Unit
31 | ): AdapterDelegate {
32 | return adapterDelegate { addBinding(block) }
33 | }
34 |
--------------------------------------------------------------------------------
/recyclerview-element-adapter/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.library'
3 | id 'org.jetbrains.kotlin.android'
4 | id 'org.jetbrains.dokka'
5 | }
6 |
7 | android {
8 | compileSdk 35
9 | namespace "com.elveum.elementadapter"
10 | defaultConfig {
11 | minSdk 21
12 | targetSdk 35
13 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
14 | consumerProguardFiles "consumer-rules.pro"
15 | }
16 | buildTypes {
17 | release {
18 | minifyEnabled false
19 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
20 | }
21 | }
22 | compileOptions {
23 | sourceCompatibility JavaVersion.VERSION_1_8
24 | targetCompatibility JavaVersion.VERSION_1_8
25 | }
26 | kotlinOptions {
27 | jvmTarget = '1.8'
28 | }
29 | buildFeatures {
30 | viewBinding true
31 | }
32 | publishing {
33 | singleVariant("release") {
34 | withSourcesJar()
35 | withJavadocJar()
36 | }
37 | }
38 | }
39 |
40 | kotlin {
41 | jvmToolchain(17)
42 | }
43 |
44 | dependencies {
45 | compileOnly 'androidx.recyclerview:recyclerview:1.3.2'
46 | }
47 |
48 | ext {
49 | PUBLISH_GROUP_ID = 'com.elveum'
50 | PUBLISH_VERSION = '0.7'
51 | PUBLISH_ARTIFACT_ID = 'element-adapter'
52 | PUBLISH_DESCRIPTION = "Another one easy-to-use adapter for RecyclerView"
53 | }
54 |
55 | apply from: "${rootProject.projectDir}/publish-library.gradle"
--------------------------------------------------------------------------------
/recyclerview-element-adapter/src/main/java/com/elveum/elementadapter/ElementListAdapter.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter
2 |
3 | import androidx.recyclerview.widget.*
4 | import com.elveum.elementadapter.dsl.BindingHolder
5 | import com.elveum.elementadapter.dsl.ElementWithIndex
6 |
7 | abstract class ElementListAdapter(
8 | diffCallback: DiffUtil.ItemCallback>,
9 | ) : RecyclerView.Adapter() {
10 |
11 | private val differ: AsyncListDiffer> = AsyncListDiffer(
12 | AdapterListUpdateCallback(this),
13 | AsyncDifferConfig.Builder(diffCallback).build()
14 | )
15 |
16 | private val listener =
17 | AsyncListDiffer.ListListener { previousList, currentList ->
18 | this@ElementListAdapter.onCurrentListChanged(
19 | previousList,
20 | currentList
21 | )
22 | }
23 |
24 | val currentList: List get() = differ.currentList.map { it.element }
25 |
26 | fun submitList(list: List?) {
27 | differ.submitList(list?.mapIndexed { index, t -> ElementWithIndex(index, t) })
28 | }
29 |
30 | fun submitList(list: List?, commitCallback: Runnable?) {
31 | differ.submitList(list?.mapIndexed { index, t -> ElementWithIndex(index, t) }, commitCallback)
32 | }
33 |
34 | protected fun getItem(position: Int): T {
35 | return differ.currentList[position].element
36 | }
37 |
38 | override fun getItemCount(): Int {
39 | return differ.currentList.size
40 | }
41 |
42 | open fun onCurrentListChanged(previousList: List, currentList: List) {}
43 |
44 | }
--------------------------------------------------------------------------------
/example-app/build.gradle:
--------------------------------------------------------------------------------
1 | plugins {
2 | id 'com.android.application'
3 | id 'kotlin-android'
4 | id 'dagger.hilt.android.plugin'
5 | id 'kotlin-kapt'
6 | }
7 |
8 | android {
9 | compileSdk 35
10 | namespace "com.elveum.elementadapter.app"
11 | defaultConfig {
12 | applicationId "com.elveum.elementadapter.app"
13 | minSdk 21
14 | targetSdk 35
15 | versionCode 1
16 | versionName "1.0"
17 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
18 | }
19 |
20 | buildTypes {
21 | release {
22 | minifyEnabled false
23 | proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
24 | }
25 | }
26 | compileOptions {
27 | sourceCompatibility JavaVersion.VERSION_1_8
28 | targetCompatibility JavaVersion.VERSION_1_8
29 | }
30 | kotlinOptions {
31 | jvmTarget = '1.8'
32 | }
33 | buildFeatures {
34 | viewBinding true
35 | }
36 | }
37 |
38 | kotlin {
39 | jvmToolchain(17)
40 | }
41 |
42 | dependencies {
43 | implementation 'androidx.core:core-ktx:1.15.0'
44 | implementation 'androidx.appcompat:appcompat:1.7.0'
45 | implementation 'com.google.android.material:material:1.12.0'
46 | implementation 'androidx.constraintlayout:constraintlayout:2.2.0'
47 | implementation "com.google.dagger:hilt-android:$hilt_version"
48 | kapt "com.google.dagger:hilt-compiler:$hilt_version"
49 | implementation 'com.github.javafaker:javafaker:1.0.2'
50 | implementation 'androidx.activity:activity-ktx:1.9.3'
51 | implementation 'io.coil-kt:coil:2.7.0'
52 | implementation project(':recyclerview-element-adapter')
53 | }
--------------------------------------------------------------------------------
/example-app/src/main/java/com/elveum/elementadapter/app/MainViewModel.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter.app
2 |
3 | import androidx.lifecycle.LiveData
4 | import androidx.lifecycle.MutableLiveData
5 | import androidx.lifecycle.ViewModel
6 | import androidx.lifecycle.viewModelScope
7 | import com.elveum.elementadapter.app.model.Cat
8 | import com.elveum.elementadapter.app.model.CatsRepository
9 | import dagger.hilt.android.lifecycle.HiltViewModel
10 | import kotlinx.coroutines.flow.collectLatest
11 | import kotlinx.coroutines.launch
12 | import javax.inject.Inject
13 |
14 | @HiltViewModel
15 | class MainViewModel @Inject constructor(
16 | private val catsRepository: CatsRepository
17 | ) : ViewModel() {
18 |
19 | private val _catsLiveData = MutableLiveData>()
20 | val catsLiveData: LiveData> = _catsLiveData
21 |
22 | init {
23 | viewModelScope.launch {
24 | catsRepository.getCats().collectLatest { catsList ->
25 | _catsLiveData.value = mapCats(catsList)
26 | }
27 | }
28 | }
29 |
30 | fun deleteCat(cat: CatListItem.Cat) {
31 | catsRepository.delete(cat.originCat)
32 | }
33 |
34 | fun toggleFavorite(cat: CatListItem.Cat) {
35 | catsRepository.toggleIsFavorite(cat.originCat)
36 | }
37 |
38 | private fun mapCats(cats: List): List {
39 | val size = 10
40 | return cats
41 | .chunked(size)
42 | .mapIndexed { index, list ->
43 | val fromIndex = index * size + 1
44 | val toIndex = fromIndex + list.size - 1
45 | val header: CatListItem = CatListItem.Header(index, fromIndex, toIndex)
46 | listOf(header) + list.map { CatListItem.Cat(it) }
47 | }
48 | .flatten()
49 | }
50 |
51 | }
--------------------------------------------------------------------------------
/example-app/src/main/res/drawable-v24/ic_launcher_foreground.xml:
--------------------------------------------------------------------------------
1 |
7 |
8 |
9 |
15 |
18 |
21 |
22 |
23 |
24 |
30 |
--------------------------------------------------------------------------------
/recyclerview-element-adapter/src/main/java/com/elveum/elementadapter/dsl/ItemCallbackDelegate.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter.dsl
2 |
3 | import androidx.recyclerview.widget.DiffUtil
4 | import androidx.viewbinding.ViewBinding
5 |
6 | typealias CompareItemCallback = IndexScope.(oldItem: T, newItem: T) -> Boolean
7 |
8 | typealias ChangePayloadCallback = IndexScope.(oldItem: T, newItem: T) -> Any?
9 |
10 | internal class ItemCallbackDelegate(
11 | private val adapterScope: AdapterScope,
12 | private val concreteItemTypeScopes: List>
13 | ) : DiffUtil.ItemCallback>() {
14 |
15 | override fun areItemsTheSame(oldItem: ElementWithIndex, newItem: ElementWithIndex): Boolean {
16 | val oldScope = findScope(oldItem.element)
17 | val newScope = findScope(newItem.element)
18 | val indexScope = IndexScopeImpl(oldItem, newItem)
19 | if (oldScope !== newScope) {
20 | return adapterScope.defaultAreItemsSame.invoke(indexScope, oldItem.element, newItem.element)
21 | }
22 | return newScope.areItemsSame(indexScope, oldItem.element, newItem.element)
23 | }
24 |
25 | override fun areContentsTheSame(oldItem: ElementWithIndex, newItem: ElementWithIndex): Boolean {
26 | val oldScope = findScope(oldItem.element)
27 | val newScope = findScope(newItem.element)
28 | val indexScope = IndexScopeImpl(oldItem, newItem)
29 | if (oldScope !== newScope) {
30 | return adapterScope.defaultAreContentsSame.invoke(indexScope, oldItem.element, newItem.element)
31 | }
32 | return newScope.areContentsSame(indexScope, oldItem.element, newItem.element)
33 | }
34 |
35 | override fun getChangePayload(oldItem: ElementWithIndex, newItem: ElementWithIndex): Any? {
36 | val oldScope = findScope(oldItem.element)
37 | val newScope = findScope(newItem.element)
38 | val indexScope = IndexScopeImpl(oldItem, newItem)
39 | if (oldScope !== newScope) {
40 | return adapterScope.defaultChangePayload.invoke(indexScope, oldItem.element, newItem.element)
41 | }
42 | return newScope.changePayload(indexScope, oldItem.element, newItem.element)
43 | }
44 |
45 | private fun findScope(item: T): ConcreteItemTypeScopeImpl {
46 | return concreteItemTypeScopes.first {
47 | it.predicate(item)
48 | }
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/publish-library.gradle:
--------------------------------------------------------------------------------
1 | apply plugin: 'maven-publish'
2 | apply plugin: 'signing'
3 |
4 | task androidSourcesJar(type: Jar) {
5 | archiveClassifier.set('sources')
6 | if (project.plugins.findPlugin("com.android.library")) {
7 | from android.sourceSets.main.java.srcDirs
8 | from android.sourceSets.main.kotlin.srcDirs
9 | } else {
10 | from sourceSets.main.java.srcDirs
11 | from sourceSets.main.kotlin.srcDirs
12 | }
13 | }
14 |
15 | artifacts {
16 | archives androidSourcesJar
17 | }
18 |
19 | group = PUBLISH_GROUP_ID
20 | version = PUBLISH_VERSION
21 |
22 | afterEvaluate {
23 | publishing {
24 | publications {
25 | release(MavenPublication) {
26 | groupId PUBLISH_GROUP_ID
27 | artifactId PUBLISH_ARTIFACT_ID
28 | version PUBLISH_VERSION
29 | if (project.plugins.findPlugin("com.android.library")) {
30 | from components.release
31 | } else {
32 | from components.java
33 | }
34 |
35 | pom {
36 | name = PUBLISH_ARTIFACT_ID
37 | description = 'Another one elementary and easy-to-use adapter for RecyclerView'
38 | url = 'https://github.com/romychab/element-adapter'
39 | licenses {
40 | license {
41 | name = 'Apache License 2.0'
42 | url = 'https://github.com/romychab/element-adapter/blob/main/LICENSE'
43 | }
44 | }
45 | developers {
46 | developer {
47 | id = 'romychab'
48 | name = 'Roman Andrushchenko'
49 | email = 'rom.andrushchenko@gmail.com'
50 | }
51 | }
52 | scm {
53 | connection = 'scm:git:github.com/romychab/element-adapter.git'
54 | developerConnection = 'scm:git:ssh://github.com/romychab/element-adapter.git'
55 | url = 'https://github.com/romychab/element-adapter/tree/main'
56 | }
57 | }
58 | }
59 | }
60 | }
61 | }
62 |
63 | signing {
64 | useInMemoryPgpKeys(
65 | rootProject.ext["signing.keyId"],
66 | rootProject.ext["signing.key"],
67 | rootProject.ext["signing.password"],
68 | )
69 | sign publishing.publications
70 | }
--------------------------------------------------------------------------------
/gradlew.bat:
--------------------------------------------------------------------------------
1 | @rem
2 | @rem Copyright 2015 the original author or authors.
3 | @rem
4 | @rem Licensed under the Apache License, Version 2.0 (the "License");
5 | @rem you may not use this file except in compliance with the License.
6 | @rem You may obtain a copy of the License at
7 | @rem
8 | @rem https://www.apache.org/licenses/LICENSE-2.0
9 | @rem
10 | @rem Unless required by applicable law or agreed to in writing, software
11 | @rem distributed under the License is distributed on an "AS IS" BASIS,
12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 | @rem See the License for the specific language governing permissions and
14 | @rem limitations under the License.
15 | @rem
16 |
17 | @if "%DEBUG%" == "" @echo off
18 | @rem ##########################################################################
19 | @rem
20 | @rem Gradle startup script for Windows
21 | @rem
22 | @rem ##########################################################################
23 |
24 | @rem Set local scope for the variables with windows NT shell
25 | if "%OS%"=="Windows_NT" setlocal
26 |
27 | set DIRNAME=%~dp0
28 | if "%DIRNAME%" == "" set DIRNAME=.
29 | set APP_BASE_NAME=%~n0
30 | set APP_HOME=%DIRNAME%
31 |
32 | @rem Resolve any "." and ".." in APP_HOME to make it shorter.
33 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
34 |
35 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
36 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
37 |
38 | @rem Find java.exe
39 | if defined JAVA_HOME goto findJavaFromJavaHome
40 |
41 | set JAVA_EXE=java.exe
42 | %JAVA_EXE% -version >NUL 2>&1
43 | if "%ERRORLEVEL%" == "0" goto execute
44 |
45 | echo.
46 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
47 | echo.
48 | echo Please set the JAVA_HOME variable in your environment to match the
49 | echo location of your Java installation.
50 |
51 | goto fail
52 |
53 | :findJavaFromJavaHome
54 | set JAVA_HOME=%JAVA_HOME:"=%
55 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe
56 |
57 | if exist "%JAVA_EXE%" goto execute
58 |
59 | echo.
60 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
61 | echo.
62 | echo Please set the JAVA_HOME variable in your environment to match the
63 | echo location of your Java installation.
64 |
65 | goto fail
66 |
67 | :execute
68 | @rem Setup the command line
69 |
70 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
71 |
72 |
73 | @rem Execute Gradle
74 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
75 |
76 | :end
77 | @rem End local scope for the variables with windows NT shell
78 | if "%ERRORLEVEL%"=="0" goto mainEnd
79 |
80 | :fail
81 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
82 | rem the _cmd.exe /c_ return code!
83 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1
84 | exit /b 1
85 |
86 | :mainEnd
87 | if "%OS%"=="Windows_NT" endlocal
88 |
89 | :omega
90 |
--------------------------------------------------------------------------------
/example-app/src/main/res/layout/item_cat.xml:
--------------------------------------------------------------------------------
1 |
2 |
12 |
13 |
19 |
20 |
35 |
36 |
49 |
50 |
60 |
61 |
71 |
72 |
--------------------------------------------------------------------------------
/recyclerview-element-adapter/src/main/java/com/elveum/elementadapter/dsl/AdapterScope.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter.dsl
2 |
3 | import android.view.LayoutInflater
4 | import android.view.ViewGroup
5 | import androidx.viewbinding.ViewBinding
6 | import com.elveum.elementadapter.delegate.AdapterDelegate
7 | import com.elveum.elementadapter.delegate.AdapterDelegateImpl
8 |
9 | interface AdapterScope {
10 |
11 | /**
12 | * A callback for checking whether items are the same or not.
13 | * Usually the callback should compare identifiers
14 | */
15 | var defaultAreItemsSame: CompareItemCallback
16 |
17 | /**
18 | * A callback for checking whether items' contents are equal or
19 | * not.
20 | */
21 | var defaultAreContentsSame: CompareItemCallback
22 |
23 | /**
24 | * A callback for creating payloads which indicate the concrete difference
25 | * between an old item and a new item. May be useful for animation,
26 | * optimizations, etc.
27 | */
28 | var defaultChangePayload: ChangePayloadCallback
29 |
30 | }
31 |
32 | class AdapterScopeImpl internal constructor() : AdapterScope {
33 |
34 | private val concreteTypeScopes = mutableListOf>()
35 |
36 | override var defaultAreItemsSame: CompareItemCallback = { oldItem, newItem -> oldItem === newItem }
37 | override var defaultAreContentsSame: CompareItemCallback = { oldItem, newItem -> oldItem == newItem }
38 | override var defaultChangePayload: ChangePayloadCallback = { _, _ -> null }
39 |
40 | fun addBinding(
41 | clazz: Class,
42 | predicate: (T) -> Boolean,
43 | block: ConcreteItemTypeScope.() -> Unit
44 | ) {
45 | val concreteItemTypeScopeImpl = ConcreteItemTypeScopeImpl(
46 | areItemsSame = this.defaultAreItemsSame as CompareItemCallback,
47 | areContentsSame = this.defaultAreContentsSame as CompareItemCallback,
48 | changePayload = this.defaultChangePayload as ChangePayloadCallback,
49 | bindingCreator = { inflater, parent ->
50 | instantiateBinding(clazz, inflater, parent)
51 | },
52 | predicate = predicate
53 | )
54 | concreteItemTypeScopeImpl.block()
55 | concreteTypeScopes.add(
56 | concreteItemTypeScopeImpl as ConcreteItemTypeScopeImpl
57 | )
58 | }
59 |
60 | fun toAdapterDelegate(): AdapterDelegate {
61 | if (concreteTypeScopes.isEmpty()) {
62 | throw IllegalStateException("Have you added at least one addBinding { ... } / universalBinding { ... } section?")
63 | }
64 |
65 | return AdapterDelegateImpl(
66 | concreteTypeScopes,
67 | ItemCallbackDelegate(this, concreteTypeScopes)
68 | )
69 | }
70 |
71 | private fun instantiateBinding(
72 | clazz: Class,
73 | inflater: LayoutInflater,
74 | parent: ViewGroup
75 | ): B {
76 | val method = clazz.getMethod("inflate", LayoutInflater::class.java, ViewGroup::class.java, Boolean::class.java)
77 | return method.invoke(null, inflater, parent, false) as B
78 | }
79 |
80 | }
81 |
--------------------------------------------------------------------------------
/example-app/src/main/java/com/elveum/elementadapter/app/SimpleMultiChoiceActivity.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter.app
2 |
3 | import android.graphics.drawable.ColorDrawable
4 | import android.os.Bundle
5 | import androidx.appcompat.app.AppCompatActivity
6 | import androidx.recyclerview.widget.DefaultItemAnimator
7 | import androidx.recyclerview.widget.LinearLayoutManager
8 | import com.elveum.elementadapter.SimpleBindingAdapter
9 | import com.elveum.elementadapter.app.databinding.ActivitySimpleMultichoiceBinding
10 | import com.elveum.elementadapter.app.databinding.ItemSelectableBinding
11 | import com.elveum.elementadapter.getColor
12 | import com.elveum.elementadapter.simpleAdapter
13 |
14 | /*
15 | This is a very simple example of multi-choice list.
16 |
17 | Please note that in real projects it's better to:
18 | - use immutable entities ('val isChecked' instead of 'var isChecked')
19 | - implement multi-choice logic e.g. in the view-model
20 | - hold data list at least in the view-model, not in the activity
21 | */
22 |
23 | data class SelectableItem(
24 | val id: Long,
25 | val name: String,
26 | var isChecked: Boolean = false
27 | )
28 |
29 | class SimpleMultiChoiceActivity : AppCompatActivity() {
30 |
31 | private val items = listOf(
32 | SelectableItem(1, "Charlie"),
33 | SelectableItem(2, "Millie"),
34 | SelectableItem(3, "Lucky"),
35 | SelectableItem(4, "Poppy"),
36 | SelectableItem(5, "Oliver"),
37 | SelectableItem(6, "Sam"),
38 | SelectableItem(7, "Tiger"),
39 | )
40 |
41 | private val adapter: SimpleBindingAdapter by lazy { createAdapter() }
42 |
43 | private val binding by lazy {
44 | ActivitySimpleMultichoiceBinding.inflate(layoutInflater)
45 | }
46 |
47 | override fun onCreate(savedInstanceState: Bundle?) {
48 | super.onCreate(savedInstanceState)
49 | setContentView(binding.root)
50 | with(binding) {
51 | multiChoiceRecyclerView.layoutManager = LinearLayoutManager(this@SimpleMultiChoiceActivity)
52 | (multiChoiceRecyclerView.itemAnimator as? DefaultItemAnimator)?.supportsChangeAnimations = false
53 | multiChoiceRecyclerView.adapter = adapter
54 | }
55 | adapter.submitList(items)
56 | updateTotalSelected()
57 | }
58 |
59 | private fun updateTotalSelected() {
60 | val count = items.count { it.isChecked }
61 | binding.totalSelectedTextView.text = getString(R.string.total_selected, count)
62 | }
63 |
64 | private fun createAdapter() = simpleAdapter {
65 | areContentsSame = { oldItem, newItem -> oldItem == newItem } // this works fine for data classes
66 | areItemsSame = { oldItem, newItem -> oldItem.id == newItem.id }
67 |
68 | bind { item ->
69 | nameTextView.text = item.name
70 | checkbox.isChecked = item.isChecked
71 | if (item.isChecked) {
72 | root.background = ColorDrawable(getColor(R.color.selected_background))
73 | } else {
74 | root.background = null
75 | }
76 | }
77 |
78 | listeners {
79 | checkbox.onClick { item ->
80 | item.isChecked = !item.isChecked
81 | val indexToUpdate = adapter.currentList.indexOfFirst { item.id == it.id }
82 | adapter.notifyItemChanged(indexToUpdate)
83 | updateTotalSelected()
84 | }
85 | }
86 | }
87 | }
--------------------------------------------------------------------------------
/example-app/src/main/java/com/elveum/elementadapter/app/model/CatsRepository.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter.app.model
2 |
3 | import com.github.javafaker.Faker
4 | import kotlinx.coroutines.flow.*
5 | import java.util.*
6 | import javax.inject.Inject
7 | import javax.inject.Singleton
8 |
9 | @Singleton
10 | class CatsRepository @Inject constructor() {
11 |
12 | private val photos = listOf(
13 | "https://images.unsplash.com/photo-1615454299901-de13b71ecaae?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8Y2F0fHx8fHx8MTY2MjQ3ODAyNw&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
14 | "https://images.unsplash.com/photo-1604916287784-c324202b3205?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8Y2F0fHx8fHx8MTY2MjQ3ODAzMQ&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
15 | "https://images.unsplash.com/photo-1596854372407-baba7fef6e51?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8Y2F0fHx8fHx8MTY2MjQ3ODAzOA&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
16 | "https://images.unsplash.com/photo-1608032364895-0da67af36cd2?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8Y2F0fHx8fHx8MTY2MjQ3ODA0Mw&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
17 | "https://images.unsplash.com/photo-1571988840298-3b5301d5109b?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8Y2F0fHx8fHx8MTY2MjQ3ODA0Ng&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
18 | "https://images.unsplash.com/photo-1494256997604-768d1f608cac?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8Y2F0fHx8fHx8MTY2MjQ3ODEyMQ&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
19 | "https://images.unsplash.com/flagged/photo-1557427161-4701a0fa2f42?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8Y2F0fHx8fHx8MTY2MjQ3ODEyNQ&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
20 | "https://images.unsplash.com/photo-1640384974326-3e72680e0fb3?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8Y2F0fHx8fHx8MTY2MjQ3ODI0MA&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080",
21 | "https://images.unsplash.com/photo-1623876159473-5e79be88f7ac?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=MnwxfDB8MXxyYW5kb218MHx8Y2F0fHx8fHx8MTY2MjQ3ODIzMw&ixlib=rb-1.2.1&q=80&utm_campaign=api-credit&utm_medium=referral&utm_source=unsplash_source&w=1080"
22 | )
23 |
24 | private val random = Random(42)
25 | private val faker = Faker.instance(random)
26 |
27 | private val catsFlow = MutableStateFlow(
28 | List(100) { index -> randomCat(id = index + 1L) }
29 | )
30 |
31 | fun getCats(): Flow> {
32 | return catsFlow
33 | }
34 |
35 | fun delete(cat: Cat) {
36 | catsFlow.update { oldList ->
37 | oldList.filter { it.id != cat.id }
38 | }
39 | }
40 |
41 | fun toggleIsFavorite(cat: Cat) {
42 | catsFlow.update { oldList ->
43 | oldList.map {
44 | if (it.id == cat.id) {
45 | cat.copy(isFavorite = !cat.isFavorite)
46 | } else {
47 | it
48 | }
49 | }
50 | }
51 | }
52 |
53 | private fun randomCat(id: Long): Cat = Cat(
54 | id = id,
55 | name = faker.cat().name(),
56 | photoUrl = photos[(id.rem(photos.size)).toInt()],
57 | isFavorite = false,
58 | description = faker.lorem().sentences(2).joinToString(" ")
59 | )
60 |
61 | }
--------------------------------------------------------------------------------
/recyclerview-element-adapter/src/main/java/com/elveum/elementadapter/DslEntry.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter
2 |
3 | import androidx.recyclerview.widget.ListAdapter
4 | import androidx.viewbinding.ViewBinding
5 | import com.elveum.elementadapter.delegate.adapterDelegate
6 | import com.elveum.elementadapter.dsl.*
7 | import com.elveum.elementadapter.dsl.AdapterScopeImpl
8 |
9 |
10 | typealias SimpleBindingAdapter = ElementListAdapter
11 |
12 | /**
13 | * Crate an instance of a [ListAdapter] for the specified base type [T].
14 | * This method should be used when you need to support more than 1 view binding type.
15 | * Otherwise it's better to use [simpleAdapter] instead.
16 | *
17 | * Usage example. Let's image you have a base class `BaseListItem` and the following
18 | * subclasses:
19 | * - `ListHeaderItem`
20 | * - `ListContentItem`
21 | *
22 | * And you want to create a list which displays those items in a different way: like
23 | * you are going to use 2 layouts:
24 | * - one for header items (`R.layout.item_header` aka `ItemHeaderBinding`)
25 | * - and one for content items (`R.layout.item_content` aka `ItemContentBinding`)
26 |
27 | * Let's do this:
28 | *
29 | * ```
30 | * val adapter = adapter {
31 | * addBinding {
32 | * areItemsSame = { oldHeader, newHeader -> oldHeader.id == newHeader.id }
33 | * areContentsSame = { oldHeader, newHeader -> oldHeader == newHeader }
34 | * bind { header ->
35 | * titleTextView.text = header.title
36 | * descriptionTextView.text = header.description
37 | * }
38 | * listeners {
39 | * root.onClick { header ->
40 | * viewModel.toggle(header)
41 | * }
42 | * }
43 | * }
44 | *
45 | * addBinding {
46 | * areItemsSame = { oldContent, newContent -> oldContent.id == newContent.id }
47 | * areContentsSame = { oldContent, newContent -> oldContent == newContent }
48 | * bind { contentItem ->
49 | * contentTextView.text = contentItem.content
50 | * previewImageView.setImageResource(contentItem.previewImageRes)
51 | * }
52 | * listeners {
53 | * deleteButton.onClick { contentItem ->
54 | * viewModel.delete(contentItem)
55 | * }
56 | * root.onClick { contentItem ->
57 | * viewModel.openDetails(contentItem)
58 | * }
59 | * }
60 | * }
61 | * }
62 | * ```
63 | */
64 | fun adapter(block: AdapterScope.() -> Unit): SimpleBindingAdapter {
65 | return MultiAdapter(adapterDelegate(block))
66 | }
67 |
68 | /**
69 | * Add a new view binding type to the adapter.
70 | */
71 | inline fun AdapterScope.addBinding(
72 | noinline block: ConcreteItemTypeScope.() -> Unit
73 | ) {
74 | (this as AdapterScopeImpl).addBinding(
75 | B::class.java,
76 | { item -> item is T },
77 | block
78 | )
79 | }
80 |
81 | /**
82 | * Create a [ListAdapter] which binds items of the type [T] to
83 | * the specified view binding of the type [B].
84 | * Usage example:
85 | * ```
86 | * val adapter = simpleAdapter {
87 | * areItemsSame = { oldCat, newCat -> oldCat.id == newCat.id }
88 | * areContentsSame = { oldCat, newCat -> oldCat == newCat }
89 | * bind { cat ->
90 | * catNameTextView.text = cat.name
91 | * catDescriptionTextView.text = cat.description
92 | * }
93 | * listeners {
94 | * root.onClick { cat ->
95 | * showCatDetails(cat)
96 | * }
97 | * }
98 | * }
99 | *
100 | * recyclerView.adapter = adapter
101 | *
102 | * viewModel.catsLiveData.observer(viewLifecycleOwner) { list ->
103 | * adapter.submitList(list)
104 | * }
105 | * ```
106 | */
107 | inline fun simpleAdapter(
108 | noinline block: ConcreteItemTypeScope.() -> Unit
109 | ): SimpleBindingAdapter {
110 | return adapter { addBinding(block) }
111 | }
112 |
--------------------------------------------------------------------------------
/recyclerview-element-adapter/src/main/java/com/elveum/elementadapter/delegate/AdapterDelegate.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter.delegate
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import androidx.recyclerview.widget.DiffUtil
7 | import androidx.recyclerview.widget.DiffUtil.ItemCallback
8 | import androidx.recyclerview.widget.RecyclerView
9 | import androidx.viewbinding.ViewBinding
10 | import com.elveum.elementadapter.R
11 | import com.elveum.elementadapter.dsl.BindingHolder
12 | import com.elveum.elementadapter.dsl.ConcreteItemTypeScopeImpl
13 | import com.elveum.elementadapter.dsl.ElementWithIndex
14 |
15 | interface AdapterDelegate {
16 |
17 | /**
18 | * Use the [DiffUtil.ItemCallback] returned by this method in your own
19 | * adapter.
20 | */
21 | fun itemCallback(): DiffUtil.ItemCallback>
22 |
23 | /**
24 | * Use this item callback instead of [itemCallback] if you don't want to
25 | * support index() method.
26 | */
27 | fun noIndexItemCallback(): DiffUtil.ItemCallback
28 |
29 | /**
30 | * Call this method from [RecyclerView.Adapter.getItemViewType]
31 | */
32 | fun getItemViewType(item: T): Int
33 |
34 | /**
35 | * Call this method from [RecyclerView.Adapter.onCreateViewHolder]
36 | */
37 | fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder
38 |
39 | /**
40 | * Call this method from [RecyclerView.Adapter.onBindViewHolder]
41 | */
42 | fun onBindViewHolder(holder: BindingHolder, position: Int, item: T, payloads: List = emptyList())
43 |
44 | }
45 |
46 | internal class AdapterDelegateImpl(
47 | private val concreteItemTypeScopesImpl: List>,
48 | private val itemCallback: DiffUtil.ItemCallback>
49 | ) : AdapterDelegate {
50 |
51 | override fun itemCallback(): DiffUtil.ItemCallback> {
52 | return itemCallback
53 | }
54 |
55 | override fun noIndexItemCallback(): DiffUtil.ItemCallback {
56 | return itemWithoutIndexCallback
57 | }
58 |
59 | override fun getItemViewType(item: T): Int {
60 | val index = concreteItemTypeScopesImpl.indexOfFirst { it.predicate(item) }
61 | if (index == -1) {
62 | throw IllegalStateException("Have you covered all subtypes in the adapter { ... } " +
63 | "by using addBinding<${item::class.java.canonicalName}, YOUR_BINDING> { ... } section?")
64 | }
65 | return index
66 | }
67 |
68 | override fun onCreateViewHolder(
69 | parent: ViewGroup,
70 | viewType: Int
71 | ): BindingHolder {
72 | val inflater = LayoutInflater.from(parent.context)
73 | val concreteTypeScope = concreteItemTypeScopesImpl[viewType]
74 | val viewBinding = concreteTypeScope.bindingCreator(inflater, parent)
75 | concreteTypeScope.uponCreating = true
76 | concreteTypeScope.listenersBlock?.invoke(viewBinding)
77 | concreteTypeScope.uponCreating = false
78 | return BindingHolder(viewBinding)
79 | }
80 |
81 | override fun onBindViewHolder(holder: BindingHolder, position: Int, item: T, payloads: List) {
82 | holder.binding.root.setTag(R.id.element_entity_tag, item)
83 | holder.binding.root.setTag(R.id.element_index_tag, position)
84 | val concreteTypeScope = concreteItemTypeScopesImpl.first { it.predicate(item) }
85 | concreteTypeScope.viewsWithListeners.forEach {
86 | holder.binding.root.findViewById(it)?.setTag(R.id.element_entity_tag, item)
87 | holder.binding.root.findViewById(it)?.setTag(R.id.element_index_tag, position)
88 | }
89 | concreteTypeScope.currentIndex = position
90 | concreteTypeScope.bindBlock?.invoke(holder.binding, item, payloads)
91 | concreteTypeScope.currentIndex = null
92 | }
93 |
94 | private val itemWithoutIndexCallback = object : ItemCallback() {
95 | override fun areItemsTheSame(oldItem: T, newItem: T): Boolean {
96 | val oldIndexItem = ElementWithIndex(-1, oldItem)
97 | val newIndexItem = ElementWithIndex(-1, newItem)
98 | return itemCallback.areItemsTheSame(oldIndexItem, newIndexItem)
99 | }
100 |
101 | override fun areContentsTheSame(oldItem: T, newItem: T): Boolean {
102 | val oldIndexItem = ElementWithIndex(-1, oldItem)
103 | val newIndexItem = ElementWithIndex(-1, newItem)
104 | return itemCallback.areContentsTheSame(oldIndexItem, newIndexItem)
105 | }
106 | }
107 |
108 | }
--------------------------------------------------------------------------------
/recyclerview-element-adapter/src/main/java/com/elveum/elementadapter/dsl/ConcreteItemTypeScope.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter.dsl
2 |
3 | import android.view.LayoutInflater
4 | import android.view.View
5 | import android.view.ViewGroup
6 | import androidx.viewbinding.ViewBinding
7 |
8 | interface ConcreteItemTypeScope {
9 |
10 | /**
11 | * A callback for checking whether items are the same or not.
12 | * Usually the callback should compare identifiers
13 | */
14 | var areItemsSame: CompareItemCallback
15 |
16 | /**
17 | * A callback for checking whether items' contents are equal or
18 | * not.
19 | */
20 | var areContentsSame: CompareItemCallback
21 |
22 | /**
23 | * A callback for creating payloads which indicate the concrete difference
24 | * between an old item and a new item. May be useful for animation,
25 | * optimizations, etc.
26 | */
27 | var changePayload: ChangePayloadCallback
28 |
29 | /**
30 | * Start a binding section where you can assign data from your model
31 | * list item to the view binding.
32 | */
33 | fun bind(block: B.(T) -> Unit)
34 |
35 | /**
36 | * Start a binding section where you can:
37 | * 1) assign data from your model list item to the view binding.
38 | * 2) use RecyclerView payloads for updating/animating views
39 | */
40 | fun bindWithPayloads(block: B.(item: T, payloads: List) -> Unit)
41 |
42 | /**
43 | * Start a listeners section where you can assign click listeners. Now
44 | * [onClick], [onLongClick] and [onCustomListener] are supported.
45 | */
46 | fun listeners(block: B.() -> Unit)
47 |
48 | fun View.onClick(listener: CustomListenerIndexScope.(T) -> Unit)
49 |
50 | fun View.onLongClick(listener: CustomListenerIndexScope.(T) -> Boolean)
51 |
52 | /**
53 | * Setup custom view listener.
54 | *
55 | * Usage example:
56 | *
57 | * ```
58 | * view.onCustomListener {
59 | * view.setOnDoubleTapListener {
60 | * val item = item() // get the data attached to this view
61 | * // do something here
62 | * }
63 | * }
64 | * ```
65 | */
66 | fun View.onCustomListener(block: CustomListenerScope.() -> Unit)
67 |
68 | /**
69 | * Get the index of an element being rendered.
70 | * This method can be called within bind { ... } block.
71 | *
72 | * Please note that you need to customize `areContentsSame` block and
73 | * take into account index changes there if you decide to use this method
74 | * in your `bind { ... }` blocks.
75 | */
76 | fun index(): Int
77 |
78 | }
79 |
80 | internal class ConcreteItemTypeScopeImpl(
81 | override var areContentsSame: CompareItemCallback,
82 | override var areItemsSame: CompareItemCallback,
83 | override var changePayload: ChangePayloadCallback,
84 | val bindingCreator: (inflater: LayoutInflater, parent: ViewGroup) -> B,
85 | val predicate: (T) -> Boolean
86 | ) : ConcreteItemTypeScope {
87 |
88 | var bindBlock: (B.(item: T, payloads: List) -> Unit)? = null
89 | var listenersBlock: (B.() -> Unit)? = null
90 | var uponCreating: Boolean = false
91 | var currentIndex: Int? = null
92 |
93 | val viewsWithListeners = mutableSetOf()
94 |
95 | override fun bind(block: B.(T) -> Unit) {
96 | this.bindBlock = { item, _ ->
97 | block(item)
98 | }
99 | }
100 |
101 | override fun bindWithPayloads(block: B.(item: T, payloads: List) -> Unit) {
102 | this.bindBlock = block
103 | }
104 |
105 | override fun listeners(block: B.() -> Unit) {
106 | this.listenersBlock = block
107 | }
108 |
109 | override fun index(): Int {
110 | return currentIndex ?: throw IllegalStateException("index() can be called only within bind { ... } block")
111 | }
112 |
113 | override fun View.onClick(listener: CustomListenerIndexScope.(T) -> Unit) {
114 | onCustomListener {
115 | setOnClickListener {
116 | listener(item())
117 | }
118 | }
119 | }
120 |
121 | override fun View.onLongClick(listener: CustomListenerIndexScope.(T) -> Boolean) {
122 | onCustomListener {
123 | setOnLongClickListener {
124 | listener(item())
125 | }
126 | }
127 | }
128 |
129 | override fun View.onCustomListener(block: CustomListenerScope.() -> Unit) {
130 | assertListenerCall()
131 | val scope = CustomListenerScopeImpl(this)
132 | scope.block()
133 | }
134 |
135 | private fun View.assertListenerCall() {
136 | if (!uponCreating) throw IllegalStateException("View.onClick() should be called only within listeners { ... } section.")
137 | viewsWithListeners.add(id)
138 | }
139 |
140 | }
141 |
--------------------------------------------------------------------------------
/example-app/src/main/java/com/elveum/elementadapter/app/MainActivity.kt:
--------------------------------------------------------------------------------
1 | package com.elveum.elementadapter.app
2 |
3 | import android.os.Bundle
4 | import android.view.animation.Animation
5 | import android.view.animation.AnimationSet
6 | import android.view.animation.ScaleAnimation
7 | import android.widget.Toast
8 | import androidx.activity.viewModels
9 | import androidx.appcompat.app.AppCompatActivity
10 | import androidx.recyclerview.widget.DefaultItemAnimator
11 | import androidx.recyclerview.widget.LinearLayoutManager
12 | import coil.load
13 | import coil.transform.CircleCropTransformation
14 | import com.elveum.elementadapter.*
15 | import com.elveum.elementadapter.app.databinding.ActivityMainBinding
16 | import com.elveum.elementadapter.app.databinding.ItemCatBinding
17 | import com.elveum.elementadapter.app.databinding.ItemHeaderBinding
18 | import com.elveum.elementadapter.app.model.Cat
19 | import dagger.hilt.android.AndroidEntryPoint
20 |
21 | @AndroidEntryPoint
22 | class MainActivity : AppCompatActivity() {
23 |
24 | private val viewModel by viewModels()
25 |
26 | override fun onCreate(savedInstanceState: Bundle?) {
27 | super.onCreate(savedInstanceState)
28 | val binding = ActivityMainBinding.inflate(layoutInflater)
29 | setContentView(binding.root)
30 |
31 | val adapter = createCatsAdapter()
32 | (binding.catsRecyclerView.itemAnimator as? DefaultItemAnimator)?.supportsChangeAnimations = false
33 | binding.catsRecyclerView.layoutManager = LinearLayoutManager(this)
34 | binding.catsRecyclerView.adapter = adapter
35 | viewModel.catsLiveData.observe(this, adapter::submitList)
36 | }
37 |
38 | // Example of adapter { ... } usage
39 | private fun createCatsAdapter() = adapter {
40 | addBinding {
41 | areItemsSame = { oldCat, newCat -> oldCat.id == newCat.id }
42 | changePayload = { oldCat, newCat ->
43 | if (!oldCat.isFavorite && newCat.isFavorite) {
44 | FAVORITE_FLAG_CHANGED
45 | } else {
46 | NO_ANIMATION
47 | }
48 | }
49 |
50 | bindWithPayloads { cat, payloads ->
51 | catNameTextView.text = cat.name
52 | catDescriptionTextView.text = cat.description
53 | catImageView.load(cat.photoUrl) {
54 | transformations(CircleCropTransformation())
55 | placeholder(R.drawable.circle)
56 | }
57 | favoriteImageView.setImageResource(
58 | if (cat.isFavorite) R.drawable.ic_favorite
59 | else R.drawable.ic_favorite_not
60 | )
61 | favoriteImageView.setTintColor(
62 | if (cat.isFavorite) R.color.highlighted_action
63 | else R.color.action
64 | )
65 | if (payloads.any { it == FAVORITE_FLAG_CHANGED }) {
66 | favoriteImageView.startAnimation(animationForFavoriteFlag)
67 | }
68 | }
69 |
70 | listeners {
71 | deleteImageView.onClick { viewModel.deleteCat(it) }
72 | favoriteImageView.onClick { viewModel.toggleFavorite(it) }
73 | root.onClick { cat ->
74 | Toast.makeText(context(), "${cat.name} meow-meows, index: ${index()}", Toast.LENGTH_SHORT).show()
75 | }
76 | }
77 | }
78 |
79 | addBinding {
80 | areItemsSame = { oldHeader, newHeader -> oldHeader.headerId == newHeader.headerId }
81 | bind { header ->
82 | titleTextView.text = getString(R.string.cats, header.fromIndex, header.toIndex)
83 | }
84 | }
85 |
86 | }
87 |
88 | // Example of simpleAdapter { ... } usage:
89 | private fun createOnlyCatsAdapter() = simpleAdapter {
90 | areItemsSame = { oldCat, newCat -> oldCat.id == newCat.id }
91 | areContentsSame = { oldCat, newCat -> oldCat == newCat }
92 | bind { cat ->
93 | catNameTextView.text = cat.name
94 | catDescriptionTextView.text = cat.description
95 | }
96 | listeners {
97 | deleteImageView.onClick { cat ->
98 | // delete the cat here
99 | }
100 | root.onClick { cat ->
101 | Toast.makeText(context(), "${cat.name} meow-meows", Toast.LENGTH_SHORT).show()
102 | }
103 | }
104 | }
105 |
106 | private val animationForFavoriteFlag by lazy(LazyThreadSafetyMode.NONE) {
107 | val toSmall = ScaleAnimation(1f, 0.8f, 1f, 0.8f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
108 | val smallToLarge = ScaleAnimation(1f, 1.5f, 1f, 1.5f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
109 | val largeToNormal = ScaleAnimation(1f, 0.83f, 1f, 0.83f, Animation.RELATIVE_TO_SELF, 0.5f, Animation.RELATIVE_TO_SELF, 0.5f)
110 | val animationSet = AnimationSet(true).apply {
111 | addAnimation(toSmall)
112 | addAnimation(smallToLarge)
113 | addAnimation(largeToNormal)
114 | }
115 | animationSet.animations.forEachIndexed { index, animation ->
116 | animation.duration = 100L
117 | animation.startOffset = index * 100L
118 | }
119 | animationSet
120 | }
121 |
122 | private companion object {
123 | val FAVORITE_FLAG_CHANGED = Any()
124 | val NO_ANIMATION = Any()
125 | }
126 | }
--------------------------------------------------------------------------------
/example-app/src/main/res/drawable/ic_launcher_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
7 |
10 |
15 |
20 |
25 |
30 |
35 |
40 |
45 |
50 |
55 |
60 |
65 |
70 |
75 |
80 |
85 |
90 |
95 |
100 |
105 |
110 |
115 |
120 |
125 |
130 |
135 |
140 |
145 |
150 |
155 |
160 |
165 |
170 |
171 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env sh
2 |
3 | #
4 | # Copyright 2015 the original author or authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 |
19 | ##############################################################################
20 | ##
21 | ## Gradle start up script for UN*X
22 | ##
23 | ##############################################################################
24 |
25 | # Attempt to set APP_HOME
26 | # Resolve links: $0 may be a link
27 | PRG="$0"
28 | # Need this for relative symlinks.
29 | while [ -h "$PRG" ] ; do
30 | ls=`ls -ld "$PRG"`
31 | link=`expr "$ls" : '.*-> \(.*\)$'`
32 | if expr "$link" : '/.*' > /dev/null; then
33 | PRG="$link"
34 | else
35 | PRG=`dirname "$PRG"`"/$link"
36 | fi
37 | done
38 | SAVED="`pwd`"
39 | cd "`dirname \"$PRG\"`/" >/dev/null
40 | APP_HOME="`pwd -P`"
41 | cd "$SAVED" >/dev/null
42 |
43 | APP_NAME="Gradle"
44 | APP_BASE_NAME=`basename "$0"`
45 |
46 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
47 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
48 |
49 | # Use the maximum available, or set MAX_FD != -1 to use that value.
50 | MAX_FD="maximum"
51 |
52 | warn () {
53 | echo "$*"
54 | }
55 |
56 | die () {
57 | echo
58 | echo "$*"
59 | echo
60 | exit 1
61 | }
62 |
63 | # OS specific support (must be 'true' or 'false').
64 | cygwin=false
65 | msys=false
66 | darwin=false
67 | nonstop=false
68 | case "`uname`" in
69 | CYGWIN* )
70 | cygwin=true
71 | ;;
72 | Darwin* )
73 | darwin=true
74 | ;;
75 | MINGW* )
76 | msys=true
77 | ;;
78 | NONSTOP* )
79 | nonstop=true
80 | ;;
81 | esac
82 |
83 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
84 |
85 |
86 | # Determine the Java command to use to start the JVM.
87 | if [ -n "$JAVA_HOME" ] ; then
88 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
89 | # IBM's JDK on AIX uses strange locations for the executables
90 | JAVACMD="$JAVA_HOME/jre/sh/java"
91 | else
92 | JAVACMD="$JAVA_HOME/bin/java"
93 | fi
94 | if [ ! -x "$JAVACMD" ] ; then
95 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
96 |
97 | Please set the JAVA_HOME variable in your environment to match the
98 | location of your Java installation."
99 | fi
100 | else
101 | JAVACMD="java"
102 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
103 |
104 | Please set the JAVA_HOME variable in your environment to match the
105 | location of your Java installation."
106 | fi
107 |
108 | # Increase the maximum file descriptors if we can.
109 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then
110 | MAX_FD_LIMIT=`ulimit -H -n`
111 | if [ $? -eq 0 ] ; then
112 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then
113 | MAX_FD="$MAX_FD_LIMIT"
114 | fi
115 | ulimit -n $MAX_FD
116 | if [ $? -ne 0 ] ; then
117 | warn "Could not set maximum file descriptor limit: $MAX_FD"
118 | fi
119 | else
120 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT"
121 | fi
122 | fi
123 |
124 | # For Darwin, add options to specify how the application appears in the dock
125 | if $darwin; then
126 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\""
127 | fi
128 |
129 | # For Cygwin or MSYS, switch paths to Windows format before running java
130 | if [ "$cygwin" = "true" -o "$msys" = "true" ] ; then
131 | APP_HOME=`cygpath --path --mixed "$APP_HOME"`
132 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"`
133 |
134 | JAVACMD=`cygpath --unix "$JAVACMD"`
135 |
136 | # We build the pattern for arguments to be converted via cygpath
137 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null`
138 | SEP=""
139 | for dir in $ROOTDIRSRAW ; do
140 | ROOTDIRS="$ROOTDIRS$SEP$dir"
141 | SEP="|"
142 | done
143 | OURCYGPATTERN="(^($ROOTDIRS))"
144 | # Add a user-defined pattern to the cygpath arguments
145 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then
146 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)"
147 | fi
148 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
149 | i=0
150 | for arg in "$@" ; do
151 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -`
152 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option
153 |
154 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition
155 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"`
156 | else
157 | eval `echo args$i`="\"$arg\""
158 | fi
159 | i=`expr $i + 1`
160 | done
161 | case $i in
162 | 0) set -- ;;
163 | 1) set -- "$args0" ;;
164 | 2) set -- "$args0" "$args1" ;;
165 | 3) set -- "$args0" "$args1" "$args2" ;;
166 | 4) set -- "$args0" "$args1" "$args2" "$args3" ;;
167 | 5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;;
168 | 6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;;
169 | 7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;;
170 | 8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;;
171 | 9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;;
172 | esac
173 | fi
174 |
175 | # Escape application args
176 | save () {
177 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done
178 | echo " "
179 | }
180 | APP_ARGS=`save "$@"`
181 |
182 | # Collect all arguments for the java command, following the shell quoting and substitution rules
183 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS"
184 |
185 | exec "$JAVACMD" "$@"
186 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [yyyy] [name of copyright owner]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Elementary RecyclerView Adapter
2 |
3 | [](https://elveum.com/sh/adapter)
4 | 
5 | [](LICENSE)
6 |
7 | > Another one easy-to-use adapter for `RecyclerView` :rocket:
8 |
9 | __Features:__
10 |
11 | - DSL-like methods for building adapters similar to Jetpack Compose but designed for `RecyclerView`
12 | - no view holders; bind any model object directly to auto-generated [view bindings](https://developer.android.com/topic/libraries/view-binding)
13 | - support of multiple item types
14 | - build-in click listeners
15 | - the library uses `DiffUtil` under the hood for fast updating of your list
16 | - support of integration either with your own adapters or with third-party adapters
17 | - upd: now payloads are supported starting from v0.4
18 |
19 | 
20 |
21 | ## Installation
22 |
23 | - Add [View Binding](https://developer.android.com/topic/libraries/view-binding) to
24 | your `build.gradle` file:
25 |
26 | ```
27 | android {
28 | ...
29 | buildFeatures {
30 | viewBinding true
31 | }
32 | ...
33 | }
34 | ```
35 |
36 | - Add the library to the `dependencies` section of your `build.gradle` script:
37 |
38 | ```
39 | dependencies {
40 | ...
41 | implementation 'com.elveum:element-adapter:0.7'
42 | }
43 | ```
44 |
45 | ## Usage example
46 |
47 | This library adds a couple of methods for easier implementation of `ListAdapter`. It relies on [View Binding](https://developer.android.com/topic/libraries/view-binding) so you don't need to create view holders.
48 |
49 | __Simple example (1 item type)__
50 |
51 | Let's image you have `Cat` model class and `R.layout.item_cat` ([View Binding](https://developer.android.com/topic/libraries/view-binding) generates `ItemCatBinding` class for this layout). Then you can write the following code:
52 |
53 | ```kotlin
54 | val adapter = simpleAdapter {
55 | areItemsSame = { oldCat, newCat -> oldCat.id == newCat.id }
56 | bind { cat ->
57 | catNameTextView.text = cat.name
58 | catDescriptionTextView.text = cat.description
59 | }
60 | listeners {
61 | root.onClick { cat ->
62 | showCatDetails(cat)
63 | }
64 | }
65 | }
66 |
67 | recyclerView.adapter = adapter
68 |
69 | viewModel.catsLiveData.observe(viewLifecycleOwner) { list ->
70 | adapter.submitList(list)
71 | }
72 | ```
73 |
74 | As you see, `simpleAdapter- ` accepts 2 types:
75 | - any type of your model (`Cat`)
76 | - an implementation of `ViewBinding` which you don't need to write because the official [View Binding](https://developer.android.com/topic/libraries/view-binding) library can do it.
77 |
78 | Then use `bind` and `listeners` methods to bind your item to views and assign listeners respectively. You can access all views from you binding class inside the `bind` and the `listeners` sections by `this` reference (which can be also omitted):
79 |
80 | ```kotlin
81 | val adapter = simpleAdapter {
82 | bind { cat -> // <--- your item to bind
83 | // access views by 'this' reference
84 | this.myTextView.text = cat.name
85 | // or directly by name in the generated binding class:
86 | myTextView.text = cat.name
87 | }
88 | }
89 | ```
90 |
91 | It's highly recommended to use a separate `listeners` section to assign click and long-click listeners to your views to avoid unnecessary object creation during item binding:
92 |
93 | ```kotlin
94 | val adapter = simpleAdapter {
95 | // ...
96 | listeners {
97 | // onClick for clicks
98 | deleteButton.onClick { cat ->
99 | viewModel.delete(cat)
100 | }
101 | // onLongClick for long clicks
102 | root.onLongClick { cat ->
103 | Toast.makeText(requireContext(), "Oooops", Toast.LENGTH_SHORT).show()
104 | true
105 | }
106 | }
107 | }
108 |
109 | ```
110 |
111 | Optionally you can adjust the logic of comparing old and new items by using `areItemsSame` and `areContentsSame` properties. They work in the same way as methods of `DiffUtil.ItemCallback` ([click here](https://developer.android.com/reference/androidx/recyclerview/widget/DiffUtil.ItemCallback) for details). By default `areItemsSame` and `areContentsSame` compare items in terms of `equals`/`hashCode` so usually you don't need to use `areContentsSame` for data classes. But it's recommended to implement at least `areItemsSame` to compare your items by identifiers.
112 |
113 | Typical example:
114 |
115 | ```kotlin
116 | val adapter = simpleAdapter {
117 | // compare by ID
118 | areItemsSame = { oldCat, newCat -> oldCat.id == newCat.id }
119 | // compare content
120 | areContentsSame = { oldCat, newCat -> oldCat == newCat }
121 | }
122 | ```
123 |
124 | __Another example (2 item types)__
125 |
126 | Let's add headers after every 10th cat to the list. For example, we can define the following structure:
127 |
128 | ```kotlin
129 | sealed class ListItem {
130 |
131 | data class Header(
132 | val id: Int,
133 | val fromIndex: Int,
134 | val toIndex: Int
135 | ) : ListItem()
136 |
137 | data class Cat(
138 | val id: Long,
139 | val name: String,
140 | val description: String
141 | ) : ListItem()
142 |
143 | }
144 | ```
145 |
146 | Add layout for each item type: `R.layout.item_cat` (`ItemCatBinding` will be generated) and `R.layout.item_header` (`ItemHeaderBinding` will be generated).
147 |
148 | Then we can write an adapter by using `adapter` and `addBinding` methods:
149 |
150 | ```kotlin
151 | val adapter = adapter { // <--- Base type
152 |
153 | // map concrete subtype ListItem.Cat to the ItemCatBinding:
154 | addBinding {
155 | areItemsSame = { oldCat, newCat -> oldCat.id == newCat.id }
156 | bind { cat ->
157 | catNameTextView.text = cat.name
158 | catDescriptionTextView.text = cat.description
159 | }
160 | listeners {
161 | deleteImageView.onClick(viewModel::deleteCat)
162 | root.onClick { cat ->
163 | viewModel.openDetails(cat)
164 | }
165 | }
166 | }
167 |
168 | // map concrete subtype ListItem.Header to the ItemHeaderBinding:
169 | addBinding {
170 | areItemsSame = { oldHeader, newHeader -> oldHeader.id == newHeader.id }
171 | bind { header ->
172 | titleTextView.text = "Cats ${header.fromIndex}...${header.toIndex}"
173 | }
174 | }
175 | }
176 | ```
177 |
178 | Then assign the list with cats and headers to the adapter by using `submitList` method:
179 |
180 | ```kotlin
181 | val list: List = getListFromSomewhere()
182 | adapter.submitList(list)
183 | ```
184 |
185 | ## Advanced usage
186 |
187 | ### Working with indexes
188 |
189 | You can take into account an element's index within `bind { ... }` block and
190 | within event callbacks such as `onClick { ... }` and so on.
191 |
192 | :warning: Please note if you want to render items differently depending on element index then
193 | you need to specify `areContentsSame` callback which should take into account index changes.
194 |
195 | For example:
196 |
197 | ```kotlin
198 | val adapter = simpleAdapter {
199 | areContentsSame = { oldCat, newCat ->
200 | // here you should pass an argument to the index() because
201 | // indexes may be different for old and new items.
202 | oldCat == newCat && index(oldCat) == index(newCat)
203 | }
204 | bind { item ->
205 | // here index() is called without args because it refers to the current item being rendered
206 | root.background = if (index() % 2 == 0) Color.GRAY else Color.WHITE
207 | // ... render other properties
208 | }
209 | }
210 | ```
211 |
212 | Referencing to indexes within event callbacks is very simple (for this case you don't
213 | need to check indexes in `areContentsSame`):
214 |
215 | ```kotlin
216 | val adapter = simpleAdapter {
217 | areContentsSame = { oldCat, newCat -> oldCat == newCat }
218 | bind {
219 | // ... render item
220 | }
221 | listeners {
222 | button.onClick {
223 | val elementIndex = index()
224 | Toast.makeText(context(), "Clicked on index: ${elementIndex}", Toast.LENGTH_SHORT).show()
225 | }
226 | customView.onCustomListener {
227 | customView.setOnMyCustomListener {
228 | val elementIndex = index()
229 | Toast.makeText(context(), "Custom event on index: ${elementIndex}", Toast.LENGTH_SHORT).show()
230 | }
231 | }
232 | }
233 | }
234 | ```
235 |
236 |
237 | ### Multi-choice / single-choice
238 |
239 | We recommend to implement multi-choice, single-choice, expand/collapse logic and so on
240 | in the view-model. And then just submit the result list to the adapter via
241 | either `LiveData` or `StateFlow`.
242 |
243 | But in case if you don't care about this, you can check the example of simple multi-choice
244 | implementation in the example app module (see [SimpleMultiChoiceActivity](example-app/src/main/java/com/elveum/elementadapter/app/SimpleMultiChoiceActivity.kt)).
245 |
246 | ### Payloads
247 |
248 | Sometimes you need to implement custom animations in your list or update only
249 | specific views. In this case you can use *payloads*.
250 |
251 | 1. Specify `changePayload` property:
252 | - in the `addBinding` block (for `adapter` method)
253 | - directly in the `simpleAdapter` block
254 | 2. Then use `bindWithPayload` instead of `bind`. The `bindWithPayload` block sends you 2 arguments
255 | instead of one: the second argument is a payload list which is exactly the same as in a typical
256 | `RecyclerView.Adapter.onBindViewHolder` method:
257 |
258 | ```kotlin
259 | val adapter = simpleAdapter {
260 | bindWithPayload { cat, payloads ->
261 | // draw cat
262 | // use payloads
263 | }
264 | }
265 | ```
266 | 3. Usage example with `adapter` (see `example-add` module in the sources for more details):
267 |
268 | ```kotlin
269 | val catsAdapter = adapter {
270 | addBinding {
271 |
272 | // ... areItemsSame, areContentsSame here ...
273 |
274 | // payloads callback:
275 | changePayload = { oldCat, newCat ->
276 | if (!oldCat.isFavorite && newCat.isFavorite) {
277 | FAVORITE_FLAG_CHANGED
278 | } else {
279 | NO_ANIMATION
280 | }
281 | }
282 |
283 | // bind with payloads
284 | bindWithPayloads { cat, payloads ->
285 |
286 | // ... render the cat here ...
287 |
288 | // if the payload list contains FAVORITE_FLAG_CHANGED:
289 | if (payloads.any { it == FAVORITE_FLAG_CHANGED }) {
290 | // render changes with animation
291 | favoriteImageView.startAnimation(buildMyAwesomeAnimation())
292 | }
293 | }
294 |
295 | }
296 |
297 | // ... bind some other item types here
298 |
299 | }
300 | ```
301 |
302 | ### Custom listeners
303 |
304 | Sometimes simple clicks and long clicks are not enough for your list items.
305 | To integrate custom listeners, you can use `onCustomListener { ... }` method.
306 |
307 | Usage example (let's assume some view can accept a double tap listener):
308 |
309 | ```kotlin
310 | val adapter = simpleAdapter {
311 | // ...
312 | listeners {
313 | someDoubleTapView.onCustomListener {
314 | someDoubleTapView.setOnDoubleTapListener { // <-- this is a method of the view
315 | // use item() call for getting the current item data
316 | val cat = item()
317 | viewModel.onDoubleTap(cat)
318 | }
319 | }
320 | }
321 | }
322 | ```
323 |
324 | ### Integration with other libraries
325 |
326 | It's possible to tie together your own adapters or adapters from other third-party libraries
327 | with this library. You can use `adapterDelegate()` or `simpleAdapterDelegate()` calls in order
328 | to create a bridge between libraries.
329 |
330 | For example, you can tie the `PagingDataAdapter` (see [Paging Library V3](https://developer.android.com/topic/libraries/architecture/paging/v3-overview))
331 | and this library.
332 |
333 | Usage example:
334 |
335 | 1. Implement a subclass of `PagingDataAdapter` (add `AdapterDelegate` to the constructor):
336 |
337 | ```kotlin
338 | class PagingDataAdapterBridge(
339 | private val delegate: AdapterDelegate
340 | ) : PagingDataAdapter(
341 | delegate.noIndexItemCallback()
342 | ) {
343 |
344 | override fun onBindViewHolder(holder: BindingHolder, position: Int, payloads: MutableList) {
345 | // please note, NULL values are not supported!
346 | val item = getItem(position) ?: return
347 | delegate.onBindViewHolder(holder, position, item, payloads)
348 | }
349 |
350 | override fun onBindViewHolder(holder: BindingHolder, position: Int) {
351 | }
352 |
353 | override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): BindingHolder {
354 | return delegate.onCreateViewHolder(parent, viewType)
355 | }
356 |
357 | override fun getItemViewType(position: Int): Int {
358 | // please note, NULL values are not supported!
359 | val item = getItem(position) ?: return 0
360 | return delegate.getItemViewType(item)
361 | }
362 |
363 | }
364 | ```
365 |
366 | 2. Write a method for creating instances of `PagingDataAdapter`:
367 |
368 | ```kotlin
369 | inline fun pagingAdapter(
370 | noinline block: ConcreteItemTypeScope.() -> Unit
371 | ): PagingDataAdapter {
372 | val delegate = simpleAdapterDelegate(block)
373 | return PagingDataAdapterBridge(delegate)
374 | }
375 | ```
376 |
377 | 3. Now you can use `pagingAdapter { ... }` call for creating instances of `PagingDataAdapter` from
378 | [Paging Library V3](https://developer.android.com/topic/libraries/architecture/paging/v3-overview)
379 |
380 | ```kotlin
381 | val adapter = pagingAdapter {
382 | areItemsSame = { oldCat, newCat -> oldCat.id == newCat.id }
383 | bind { cat ->
384 | catNameTextView.text = cat.name
385 | catDescriptionTextView.text = cat.description
386 | }
387 | listeners {
388 | root.onClick { cat ->
389 | Toast.makeText(context(), "${cat.name} meow-meows", Toast.LENGTH_SHORT).show()
390 | }
391 | }
392 | }
393 |
394 | recyclerView.adapter = adapter
395 |
396 | lifecycleScope.launch {
397 | viewModel.catsPagingDataFlow.collectLatest {
398 | adapter.submitData(it)
399 | }
400 | }
401 |
402 | ```
403 |
404 | ## Changelog
405 |
406 | ### v0.7
407 |
408 | - Updated plugins, targetSdk and libraries
409 |
410 | ### v0.6
411 |
412 | - Upgraded gradle plugin and dependencies
413 | - Changed target SDK to 33
414 | - Now you can specify `defaultAreItemsSame`, `defaultAreContentsSame` and `defaultChangePayload` callbacks
415 | directly in the `adapter { ... }` block. They will be used as default callbacks for
416 | all `addBinding { ... }` sub-blocks.
417 | - Default implementation of `areItemsSame` now compares items by reference
418 | (e.g. `oldItem === newItem` instead of `oldItem == newItem`)
419 |
420 | ### v0.5
421 |
422 | - Added `index()` method which can be called within:
423 | - `bind { ... }` block
424 | - `onClick { ... }`, `onLongClick { ... }` blocks
425 | - `onCustomListener { view.onMyListener { ... } }` block
426 | - Added `index(item)` method to `areContentsSame { ... }`,
427 | `areItemsSame { ... }` and `changePayload { ... }` blocks. For these blocks
428 | you should call `index()` with arg because there is a need to specify for
429 | which item (`oldItem` or `newItem`) you want to get an index.
430 |
431 | ### v0.4
432 |
433 | - Added support of RecyclerView payloads
434 |
435 | ### v0.3
436 |
437 | - Added a couple of extension methods for getting resources to the `bind` and `listeners` block
438 | - Added `onCustomListener { ... }` method for assigning custom listeners
439 | - Added `adapterDelegate { ... }` and `simpleAdapterDelegate { ... }` methods
440 | for easier integration with third-party adapters
441 |
442 | ### v0.2
443 |
444 | - Added `context()` extension method
445 | - Updated minSDK from 23 to 21
446 |
447 | ### v0.1
448 |
449 | - The first release
450 |
451 | ## License
452 |
453 | [Apache License 2.0](LICENSE)
--------------------------------------------------------------------------------