Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add binding paging adapter #43

Merged
merged 10 commits into from
Mar 2, 2022
Merged
1 change: 1 addition & 0 deletions buildSrc/src/main/kotlin/Dependencies.kt
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ object Dependencies {
const val viewPager2 = "androidx.viewpager2:viewpager2:${Versions.viewPager2}"
const val databinding = "androidx.databinding:databinding-runtime:${Versions.databinding}"
const val material = "com.google.android.material:material:${Versions.material}"
const val paging = "androidx.paging:paging-runtime:${Versions.paging}"
}

/* ============================= TEST ================================ */
Expand Down
3 changes: 2 additions & 1 deletion buildSrc/src/main/kotlin/Versions.kt
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@

object Versions {
const val gradle = "7.1"
const val gradleBuildTools = "4.2.0"
Expand All @@ -14,7 +15,7 @@ object Versions {
const val viewPager2 = "1.0.0"
const val databinding = "4.2.1"
const val material = "1.3.0"

const val paging = "3.0.0"
const val retrofit = "2.9.0"

const val timber = "4.7.1"
Expand Down
12 changes: 4 additions & 8 deletions kaal-core/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
id("com.android.library")
kotlin("android")
Expand Down Expand Up @@ -42,6 +40,10 @@ android {

}

kotlinOptions {
jvmTarget = "1.8"
}

lintOptions {
lintConfig = rootProject.file("lint.xml")
}
Expand All @@ -56,12 +58,6 @@ dependencies {
testImplementation(Dependencies.Test.kotlinTest)
}

tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "1.8"
}
}

tasks.dokkaHtml.configure {
moduleName.set("kaal-core")
outputDirectory.set(buildDir.resolve("dokka/html"))
Expand Down
1 change: 0 additions & 1 deletion kaal-domain/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import org.jetbrains.dokka.gradle.DokkaTask
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
Expand Down
12 changes: 4 additions & 8 deletions kaal-infrastructure/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
id("com.android.library")
kotlin("android")
Expand Down Expand Up @@ -41,6 +39,10 @@ android {
targetCompatibility = Android.targetCompatibilityJava
}

kotlinOptions {
jvmTarget = "1.8"
}

lintOptions {
lintConfig = rootProject.file("lint.xml")
}
Expand All @@ -65,12 +67,6 @@ dependencies {
testImplementation(Dependencies.Test.kotlinTest)
}

tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "1.8"
}
}

tasks.dokkaHtml.configure {
moduleName.set("kaal-infrastructure")
outputDirectory.set(buildDir.resolve("dokka/html"))
Expand Down
12 changes: 4 additions & 8 deletions kaal-presentation/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -1,5 +1,3 @@
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile

plugins {
id("com.android.library")
kotlin("android")
Expand Down Expand Up @@ -40,7 +38,10 @@ android {
compileOptions {
sourceCompatibility = Android.sourceCompatibilityJava
targetCompatibility = Android.targetCompatibilityJava
}

kotlinOptions {
jvmTarget = "1.8"
}

lintOptions {
Expand All @@ -52,12 +53,6 @@ android {
}
}

tasks.withType<KotlinCompile> {
kotlinOptions {
jvmTarget = "1.8"
}
}
Del-S marked this conversation as resolved.
Show resolved Hide resolved

dependencies {
api(project(":kaal-domain"))

Expand All @@ -73,6 +68,7 @@ dependencies {
compileOnly(Dependencies.Android.material)
compileOnly(Dependencies.Android.recyclerView)
compileOnly(Dependencies.Android.viewPager2)
compileOnly(Dependencies.Android.paging)

// Timber
implementation(Dependencies.timber)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,213 @@
package cz.eman.kaal.presentation.adapter

import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.annotation.IntRange
import androidx.annotation.LayoutRes
import androidx.annotation.MainThread
import androidx.databinding.DataBindingUtil
import androidx.databinding.ViewDataBinding
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.recyclerview.widget.RecyclerView
import cz.eman.kaal.presentation.R
import cz.eman.kaal.presentation.adapter.binder.ItemBinder
import cz.eman.kaal.presentation.adapter.binder.VariableBinder

/**
* Handles basic (common) functionality for Binding adapters. Takes care of view creation, data binding, click listeners
* and other common functionality for adapters.
*
* This is an interface due to different parents for adapters. It requires access to multiple variables from the
* extending classes like [itemBinder], [itemClickListener], and others. Prevents code being repeated but it also makes
* the variables public.
*
* @property T type of the object handled by this interface
* @author eMan a.s.
* @author Radek Piekarz
* @see [GitHub project](https://github.com/radzio/android-data-binding-recyclerview)
* @see BaseBindingAdapter
* @since 0.9.0
*/
@Suppress("UNCHECKED_CAST")
interface BaseBindingAdapter<T : Any> : View.OnClickListener,
Del-S marked this conversation as resolved.
Show resolved Hide resolved
View.OnLongClickListener {

/**
* Gets item on a specific position in the adapter. Must be implemented by extending class.
*
* @param position of the item
* @return [T] item or null when not found
*/
fun getItemInternal(@IntRange(from = 0) position: Int): T?

/**
* Item binder specifies binding layout for specific item. For more information check [ItemBinder].
*/
val itemBinder: ItemBinder<T>

/**
* Specifies click action for the whole item layout ([ViewHolder]). Click on any sub-view of the holder must be
* handled separately. Ex: you can send ViewModel using [variableBinders] and bind click directly in layout xml.
*/
val itemClickListener: ((View, T) -> Unit)?

/**
* Same as the [itemClickListener] with the exception that it handles long click instead of click.
*/
val itemLongClickListener: ((View, T) -> Unit)?

/**
* Used to send custom variables to item layout [ViewHolder]. Any variable can be send but most common is ViewModel.
* For more information check [VariableBinder].
*/
val variableBinders: Array<VariableBinder<T>>?

/**
* Creates instance of [ViewHolder] with specific binding layout inflated using [layoutId].
*
* @param viewGroup for the layout inflation
* @param layoutId layout resource identifying which layout should be inflated
* @return inflated [ViewHolder]
* @see ViewHolder
* @see DataBindingUtil.inflate
*/
fun onCreateViewHolderInternal(viewGroup: ViewGroup, @LayoutRes layoutId: Int): ViewHolder {
return ViewHolder(DataBindingUtil.inflate(LayoutInflater.from(viewGroup.context), layoutId, viewGroup, false))
}

/**
* Binds item data to the [ViewHolder]. Gets the specific item (if possible) and sets it to the view holder binding.
* It also sets any other variables from [variableBinders] binds click listeners (if they are enabled).
*
* Since [ViewHolder] allows variable observing using [Lifecycle] it will set the binding [LifecycleOwner] as a
* [ViewHolder]. It makes sure all variable changes are propagated to the binding. After binding is configured it
* also triggers [ViewDataBinding.executePendingBindings] to make sure all displayed information correspond with the
* item information.
*
* @param viewHolder to have variables and listeners set
* @param position of the item in the list
* @see ViewDataBinding.setVariable
* @see ViewDataBinding.executePendingBindings
*/
fun onBindViewHolderInternal(viewHolder: ViewHolder, position: Int) {
val item = getItemInternal(position) ?: return
viewHolder.binding.apply {
lifecycleOwner = viewHolder

setVariable(itemBinder.getBindingVariable(item), item)
variableBinders?.forEach {
setVariable(
it.getVariableId(item),
it.getVariableValue(item)
)
}
}.executePendingBindings()

viewHolder.binding.root.apply {
setTag(R.id.recycler_view_adapter_item_model, item)
// Must be like this to keep the original value
isClickable = isClickable.also { setOnClickListener(this@BaseBindingAdapter) }
Del-S marked this conversation as resolved.
Show resolved Hide resolved
isLongClickable = isLongClickable.also { setOnLongClickListener(this@BaseBindingAdapter) }
}
}

/**
* When view is attached to the window the [ViewHolder] starts observing variables.
*
* @param viewHolder to start listening in
* @see ViewHolder.onStart
*/
fun onViewAttachedToWindowInternal(viewHolder: ViewHolder) {
viewHolder.onStart()
}

/**
* When view is detached from the window the [ViewHolder] stops observing variables.
*
* @param viewHolder to start listening in
* @see ViewHolder.onStop
*/
fun onViewDetachedFromWindowInternal(viewHolder: ViewHolder) {
viewHolder.onStop()
}

/**
* Gets item view type by it's [position]. Gets the item using [getItemInternal] and then it gets layout resource
* for it from the [itemBinder]. If it is not found then 0 is returned.
*
* @param position of the item in the list
* @return [Int] with layout resource
* @see getItemInternal
* @see ItemBinder.getLayoutRes
*/
@LayoutRes
fun getItemViewTypeInternal(position: Int) =
getItemInternal(position)?.let { itemBinder.getLayoutRes(it) } ?: 0

/**
* Handles click on the item view by getting the item from view tag and invoking [itemClickListener] if it exists.
*
* @param view used to get the item and also being send to the click listener
*/
override fun onClick(view: View) {
itemClickListener?.let {
smidf marked this conversation as resolved.
Show resolved Hide resolved
val item = view.getTag(R.id.recycler_view_adapter_item_model) as T
it.invoke(view, item)
}
}

/**
* Handles long click on the item view by getting the item from view tag and invoking [itemLongClickListener] if it
* exists.
*
* @param view used to get the item and also being send to the click listener
* @return true if handled by the [itemLongClickListener] else false
*/
override fun onLongClick(view: View): Boolean {
return itemLongClickListener?.let {
val item = view.getTag(R.id.recycler_view_adapter_item_model) as T
it.invoke(view, item)
true
} ?: false
}

/**
* View holder for the [RecyclerView.Adapter] holding [ViewDataBinding] for the item and also serving as a
* [LifecycleOwner] to listen for any changes in lifecycle events.
*
* @property binding view binding for the holder
* @see RecyclerView.ViewHolder
*/
class ViewHolder(val binding: ViewDataBinding) : RecyclerView.ViewHolder(binding.root),
LifecycleOwner {

/**
* @see LifecycleRegistry
*/
private val registry: LifecycleRegistry = LifecycleRegistry(this)

/**
* @see LifecycleOwner.getLifecycle
*/
override fun getLifecycle(): Lifecycle = registry

/**
* Sets [Lifecycle.Event.ON_START] event to the [registry] to start observing variable changes.
*/
@MainThread
fun onStart() {
registry.handleLifecycleEvent(Lifecycle.Event.ON_START)
}

/**
* Sets [Lifecycle.Event.ON_STOP] event to the [registry] to stop observing variable changes.
*/
@MainThread
fun onStop() {
registry.handleLifecycleEvent(Lifecycle.Event.ON_STOP)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package cz.eman.kaal.presentation.adapter

import android.view.View
import androidx.recyclerview.widget.DiffUtil
import cz.eman.kaal.presentation.adapter.binder.ItemBinder
import cz.eman.kaal.presentation.adapter.binder.VariableBinder

/**
* Wrapper data class for Binding adapter configuration. It is used when binding adapter is being created. Holds only
* common information and all extending information must be handled separately.
*
* @property itemBinder defines layout for specific item types
* @property variableBinders allows sending custom variables to the layout
* @property itemClickListener enables to handle item click (not sub-view click)
* @property itemLongClickListener enables to handle item long click (not sub-view click)
* @property limit of how many items can be displayed
* @property differ used to compare items
* @author: eMan a.s.
* @since 0.9.0
*/
class BindingAdapterConfig<T : Any>(
val itemBinder: ItemBinder<T>,
val variableBinders: Array<VariableBinder<T>>?,
val itemClickListener: ((View, T) -> Unit)?,
val itemLongClickListener: ((View, T) -> Unit)?,
val limit: Int? = null,
val differ: DiffUtil.ItemCallback<T>? = null
)
Loading