This commit is contained in:
coco
2026-07-03 16:23:31 +08:00
commit 7a4fb0e6ae
1979 changed files with 101570 additions and 0 deletions
@@ -0,0 +1 @@
/build
@@ -0,0 +1,30 @@
plugins {
id("com.android.library")
id("org.jetbrains.kotlin.android")
}
android {
compileSdk = Version.compileSdk
defaultConfig {
minSdk = Version.minSdk
targetSdk = Version.targetSdk
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_11.toString()
}
compileOptions {
targetCompatibility(JavaVersion.VERSION_11)
sourceCompatibility(JavaVersion.VERSION_11)
}
}
dependencies {
implementation(Deps.recyclerview)
implementation(Deps.paging)
implementation(Deps.pagingKtx)
testImplementation(Deps.testJunit)
androidTestImplementation(Deps.androidTestJunit)
androidTestImplementation(Deps.androidTestEspresso)
}
+21
View File
@@ -0,0 +1,21 @@
# Add project specific ProGuard rules here.
# You can control the set of applied configuration files using the
# proguardFiles setting in build.gradle.
#
# For more details, see
# http://developer.android.com/guide/developing/tools/proguard.html
# If your project uses WebView with JS, uncomment the following
# and specify the fully qualified class name to the JavaScript interface
# class:
#-keepclassmembers class fqcn.of.javascript.interface.for.webview {
# public *;
#}
# Uncomment this to preserve the line number information for
# debugging stack traces.
#-keepattributes SourceFile,LineNumberTable
# If you keep the line number information, uncomment this to
# hide the original source file name.
#-renamesourcefileattribute SourceFile
@@ -0,0 +1,24 @@
package com.lowe.multitype
import androidx.test.platform.app.InstrumentationRegistry
import androidx.test.ext.junit.runners.AndroidJUnit4
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.Assert.*
/**
* Instrumented test, which will execute on an Android device.
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
@RunWith(AndroidJUnit4::class)
class ExampleInstrumentedTest {
@Test
fun useAppContext() {
// Context of the app under test.
val appContext = InstrumentationRegistry.getInstrumentation().targetContext
assertEquals("com.lowe.mylibrary.test", appContext.packageName)
}
}
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.lowe.multitype">
</manifest>
@@ -0,0 +1,260 @@
package com.lowe.multitype
import android.util.Log
import android.view.ViewGroup
import androidx.annotation.CheckResult
import androidx.recyclerview.widget.RecyclerView
import kotlin.reflect.KClass
/**
* [MultiTypeAdapter] or [PagingMultiTypeAdapter] base implements, used for abstract main functions.
*
* @author Lowae
*/
abstract class BaseMultiTypeAdapter @JvmOverloads constructor(
open val initialCapacity: Int = 0,
open var types: Types = MutableTypes(initialCapacity),
) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {
/**
* [PagingMultiTypeAdapter] need this
*/
abstract fun getItem(position: Int): Any
/**
* Registers a type class and its item view delegate. If you have registered the class,
* it will override the original delegate(s). Note that the method is non-thread-safe
* so that you should not use it in concurrent operation.
*
* Note that the method should not be called after
* [RecyclerView.setAdapter], or you have to call the setAdapter
* again.
*
* @param clazz the class of a item
* @param delegate the item view delegate
* @param T the item data type
* */
fun <T> register(clazz: Class<T>, delegate: ItemViewBaseDelegate<T, *>) {
unregisterAllTypesIfNeeded(clazz)
register(Type(clazz, delegate, DefaultLinker()))
}
inline fun <reified T : Any> register(delegate: ItemViewBaseDelegate<T, *>) {
register(T::class.java, delegate)
}
inline fun <reified T : Any> register(
// Keep this parameter to provide the explicit relationship
@Suppress("UNUSED_PARAMETER") clazz: KClass<T>,
delegate: ItemViewBaseDelegate<T, *>,
) {
// Always use the reified type to avoid javaPrimitiveType problem
// See https://github.com/drakeet/MultiType/issues/302
register(T::class.java, delegate)
}
fun <T> register(clazz: Class<T>, binder: ItemViewBinder<T, *>) {
register(clazz, binder as ItemViewBaseDelegate<T, *>)
}
fun <T> register(clazz: Class<T>, binder: PagingItemViewBinder<T, *>) {
register(clazz, binder as ItemViewBaseDelegate<T, *>)
}
inline fun <reified T : Any> register(binder: ItemViewBinder<T, *>) {
register(binder as ItemViewBaseDelegate<T, *>)
}
inline fun <reified T : Any> register(binder: PagingItemViewBinder<T, *>) {
register(binder as ItemViewBaseDelegate<T, *>)
}
inline fun <reified T : Any> register(clazz: KClass<T>, binder: ItemViewBinder<T, *>) {
register(clazz, binder as ItemViewBaseDelegate<T, *>)
}
inline fun <reified T : Any> register(clazz: KClass<T>, binder: PagingItemViewBinder<T, *>) {
register(clazz, binder as ItemViewBaseDelegate<T, *>)
}
internal fun <T> register(type: Type<T>) {
types.register(type)
when (type.delegate) {
is ItemViewDelegate -> {
check(this is MultiTypeAdapter) { IllegalArgumentException("Are you register ${type.delegate} to MultiTypeAdapter correctly?") }
type.delegate._adapter = this
}
is PagingItemViewDelegate -> {
check(this is PagingMultiTypeAdapter<*>) { IllegalArgumentException("Are you register ${type.delegate} to PagingMultiTypeAdapter correctly?") }
type.delegate._adapter = this
}
}
}
/**
* Registers a type class to multiple item view delegates. If you have registered the
* class, it will override the original delegate(s). Note that the method is non-thread-safe
* so that you should not use it in concurrent operation.
*
* Note that the method should not be called after
* [RecyclerView.setAdapter], or you have to call the setAdapter again.
*
* @param clazz the class of a item
* @param <T> the item data type
* @return [OneToManyFlow] for setting the delegates
* @see [register]
*/
@CheckResult
fun <T> register(clazz: Class<T>): OneToManyFlow<T> {
unregisterAllTypesIfNeeded(clazz)
return OneToManyBuilder(this, clazz)
}
@CheckResult
fun <T : Any> register(clazz: KClass<T>): OneToManyFlow<T> {
return register(clazz.java)
}
/**
* Registers all of the contents in the specified [Types]. If you have registered a
* class, it will override the original delegate(s). Note that the method is non-thread-safe
* so that you should not use it in concurrent operation.
*
* Note that the method should not be called after
* [RecyclerView.setAdapter], or you have to call the setAdapter
* again.
*
* @param types a [Types] containing contents to be added to this adapter inner [Types]
* @see [register]
* @see [register]
*/
fun registerAll(types: Types) {
val size = types.size
for (i in 0 until size) {
val type = types.getType<Any>(i)
unregisterAllTypesIfNeeded(type.clazz)
register(type)
}
}
override fun getItemViewType(position: Int): Int {
return indexInTypesOf(position, getItem(position))
}
override fun onCreateViewHolder(
parent: ViewGroup,
indexViewType: Int
): RecyclerView.ViewHolder {
return types.getType<Any>(indexViewType).delegate.onCreateViewHolder(parent.context, parent)
}
override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
onBindViewHolder(holder, position, emptyList())
}
override fun onBindViewHolder(
holder: RecyclerView.ViewHolder,
position: Int,
payloads: List<Any>
) {
getOutDelegateByViewHolder(holder).onBindViewHolder(holder, getItem(position), payloads)
}
/**
* Called to return the stable ID for the item, and passes the event to its associated delegate.
*
* @param position Adapter position to query
* @return the stable ID of the item at position
* @see ItemViewBaseDelegate.getItemId
* @see RecyclerView.Adapter.setHasStableIds
* @since v3.2.0
*/
override fun getItemId(position: Int): Long {
val itemViewType = getItemViewType(position)
return types.getType<Any>(itemViewType).delegate.getItemId(getItem(position))
}
/**
* Called when a view created by this adapter has been recycled, and passes the event to its
* associated delegate.
*
* @param holder The ViewHolder for the view being recycled
* @see RecyclerView.Adapter.onViewRecycled
* @see ItemViewBaseDelegate.onViewRecycled
*/
override fun onViewRecycled(holder: RecyclerView.ViewHolder) {
getOutDelegateByViewHolder(holder).onViewRecycled(holder)
}
/**
* Called by the RecyclerView if a ViewHolder created by this Adapter cannot be recycled
* due to its transient state, and passes the event to its associated item view delegate.
*
* @param holder The ViewHolder containing the View that could not be recycled due to its
* transient state.
* @return True if the View should be recycled, false otherwise. Note that if this method
* returns `true`, RecyclerView *will ignore* the transient state of
* the View and recycle it regardless. If this method returns `false`,
* RecyclerView will check the View's transient state again before giving a final decision.
* Default implementation returns false.
* @see RecyclerView.Adapter.onFailedToRecycleView
* @see ItemViewBaseDelegate.onFailedToRecycleView
*/
override fun onFailedToRecycleView(holder: RecyclerView.ViewHolder): Boolean {
return getOutDelegateByViewHolder(holder).onFailedToRecycleView(holder)
}
/**
* Called when a view created by this adapter has been attached to a window, and passes the
* event to its associated item view delegate.
*
* @param holder Holder of the view being attached
* @see RecyclerView.Adapter.onViewAttachedToWindow
* @see ItemViewBaseDelegate.onViewAttachedToWindow
*/
override fun onViewAttachedToWindow(holder: RecyclerView.ViewHolder) {
getOutDelegateByViewHolder(holder).onViewAttachedToWindow(holder)
}
/**
* Called when a view created by this adapter has been detached from its window, and passes
* the event to its associated item view delegate.
*
* @param holder Holder of the view being detached
* @see RecyclerView.Adapter.onViewDetachedFromWindow
* @see ItemViewBaseDelegate.onViewDetachedFromWindow
*/
override fun onViewDetachedFromWindow(holder: RecyclerView.ViewHolder) {
getOutDelegateByViewHolder(holder).onViewDetachedFromWindow(holder)
}
private fun getOutDelegateByViewHolder(holder: RecyclerView.ViewHolder): ItemViewBaseDelegate<Any, RecyclerView.ViewHolder> {
@Suppress("UNCHECKED_CAST")
return types.getType<Any>(holder.itemViewType).delegate as ItemViewBaseDelegate<Any, RecyclerView.ViewHolder>
}
private fun getOutDelegateByViewHolder(position: Int): ItemViewBaseDelegate<Any, RecyclerView.ViewHolder> {
@Suppress("UNCHECKED_CAST")
return types.getType<Any>(getItemViewType(position)).delegate as ItemViewBaseDelegate<Any, RecyclerView.ViewHolder>
}
@Throws(DelegateNotFoundException::class)
internal fun indexInTypesOf(position: Int, item: Any): Int {
val index = types.firstIndexOf(item.javaClass)
if (index != -1) {
val linker = types.getType<Any>(index).linker
return index + linker.index(position, item)
}
throw DelegateNotFoundException(item.javaClass)
}
private fun unregisterAllTypesIfNeeded(clazz: Class<*>) {
if (types.unregister(clazz)) {
Log.w(TAG, "The type ${clazz.simpleName} you originally registered is now overwritten.")
}
}
companion object {
private const val TAG = "MultiTypeAdapter"
}
}
@@ -0,0 +1,44 @@
/*
* Copyright (c) 2016-present. Drakeet Xu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lowe.multitype
/**
* @author Drakeet Xu
*/
internal class ClassLinkerBridge<T> private constructor(
private val javaClassLinker: JavaClassLinker<T>,
private val delegates: Array<ItemViewDelegate<T, *>>
) : Linker<T> {
override fun index(position: Int, item: T): Int {
val indexedClass = javaClassLinker.index(position, item)
val index = delegates.indexOfFirst { it.javaClass == indexedClass }
if (index != -1) return index
throw IndexOutOfBoundsException(
"The delegates'(${delegates.contentToString()}) you registered do not contain this ${indexedClass.name}."
)
}
companion object {
fun <T> toLinker(
javaClassLinker: JavaClassLinker<T>,
delegates: Array<ItemViewDelegate<T, *>>
): Linker<T> {
return ClassLinkerBridge(javaClassLinker, delegates)
}
}
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2016-present. Drakeet Xu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lowe.multitype
/**
* @author Drakeet Xu
*/
internal class DefaultLinker<T> : Linker<T> {
override fun index(position: Int, item: T): Int = 0
}
@@ -0,0 +1,24 @@
/*
* Copyright (c) 2016-present. Drakeet Xu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lowe.multitype
/**
* @author Drakeet Xu
*/
internal class DelegateNotFoundException(clazz: Class<*>) : RuntimeException(
"Have you registered the ${clazz.name} type and its delegate or binder?"
)
@@ -0,0 +1,11 @@
package com.lowe.multitype
import androidx.recyclerview.widget.RecyclerView
/**
* Footer ItemBinder
*
* @author Lowae
*/
abstract class FooterStateItemBinder<VH : RecyclerView.ViewHolder> :
ItemViewBaseDelegate<androidx.paging.LoadState, VH>()
@@ -0,0 +1,11 @@
package com.lowe.multitype
import androidx.recyclerview.widget.RecyclerView
/**
* Header ItemBinder
*
* @author Lowae
*/
abstract class HeaderStateItemBinder<VH : RecyclerView.ViewHolder> :
ItemViewBaseDelegate<androidx.paging.LoadState, VH>()
@@ -0,0 +1,154 @@
package com.lowe.multitype
import android.content.Context
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.RecyclerView.LayoutManager
import androidx.recyclerview.widget.RecyclerView.ViewHolder
abstract class ItemViewBaseDelegate<T, VH : ViewHolder> {
abstract fun onCreateViewHolder(context: Context, parent: ViewGroup): VH
/**
* Called by MultiTypeAdapter to display the data with its view holder. This method should
* update the contents of the [ViewHolder.itemView] to reflect the given item.
*
* If you need the position of an item later on (e.g. in a click listener), use
* `ViewHolder#getAdapterPosition()` which will have the updated adapter position.
*
* Override `onBindViewHolder(ViewHolder, Object, List)` instead if your ItemViewDelegate
* can handle efficient partial bind.
*
* @param holder The ViewHolder which should be updated to represent the contents of the
* given item in the items data set.
* @param item The item within the MultiTypeAdapter's items data set.
*/
abstract fun onBindViewHolder(holder: VH, item: T)
/**
* Called by MultiTypeAdapter to display the data with its view holder. This method should
* update the contents of the [ViewHolder.itemView] to reflect the given item.
*
* If you need the position of an item later on (e.g. in a click listener), use
* [ViewHolder.getAdapterPosition] which will have the updated adapter position.
*
* Partial bind vs full bind:
*
* The payloads parameter is a merge list from [MultiTypeAdapter.notifyItemChanged] [MultiTypeAdapter.notifyItemRangeChanged].
* If the payloads list is not empty, the ViewHolder is currently bound to old data and
* ItemViewDelegate may run an efficient partial update using the payload info.
* If the payload is empty, ItemViewDelegate must run a full bind.
* ItemViewDelegate should not assume that the payload passed in notify methods will be
* received by onBindViewHolder(). For example when the view is not attached to the screen,
* the payload in notifyItemChange() will be simply dropped.
*
* This implementation calls the `onBindViewHolder(ViewHolder, Object)` by default.
*
* @param holder The ViewHolder which should be updated to represent the contents of the
* given item in the items data set.
* @param item The item within the MultiTypeAdapter's items data set.
* @param payloads A non-null list of merged payloads. Can be empty list if requires full
* update.
* @since v2.5.0
*/
open fun onBindViewHolder(holder: VH, item: T, payloads: List<Any>) {
onBindViewHolder(holder, item)
}
/**
* Return the stable ID for the `item`. If [RecyclerView.Adapter.hasStableIds]
* would return false this method should return [RecyclerView.NO_ID]. The default
* implementation of this method returns [RecyclerView.NO_ID].
*
* @param item The item within the MultiTypeAdapter's items data set to query
* @return the stable ID of the item
* @see RecyclerView.Adapter.setHasStableIds
* @since v3.2.0
*/
@Suppress("UNUSED_PARAMETER")
open fun getItemId(item: T): Long = RecyclerView.NO_ID
/**
* Called when a view created by this [ItemViewDelegate] has been recycled.
*
* A view is recycled when a [LayoutManager] decides that it no longer
* needs to be attached to its parent [RecyclerView]. This can be because it has
* fallen out of visibility or a set of cached views represented by views still
* attached to the parent RecyclerView. If an item view has large or expensive data
* bound to it such as large bitmaps, this may be a good place to release those
* resources.
*
* RecyclerView calls this method right before clearing ViewHolder's internal data and
* sending it to RecycledViewPool.
*
* @param holder The ViewHolder for the view being recycled
* @since v3.1.0
*/
open fun onViewRecycled(holder: VH) {}
/**
* Called by the RecyclerView if a ViewHolder created by this Adapter cannot be recycled
* due to its transient state. Upon receiving this callback, Adapter can clear the
* animation(s) that effect the View's transient state and return `true` so that
* the View can be recycled. Keep in mind that the View in question is already removed from
* the RecyclerView.
*
* In some cases, it is acceptable to recycle a View although it has transient state. Most
* of the time, this is a case where the transient state will be cleared in
* [.onBindViewHolder] call when View is rebound to a new item.
* For this reason, RecyclerView leaves the decision to the Adapter and uses the return
* value of this method to decide whether the View should be recycled or not.
*
* Note that when all animations are created by [RecyclerView.ItemAnimator], you
* should never receive this callback because RecyclerView keeps those Views as children
* until their animations are complete. This callback is useful when children of the item
* views create animations which may not be easy to implement using an [ ].
*
* You should *never* fix this issue by calling
* `holder.itemView.setHasTransientState(false);` unless you've previously called
* `holder.itemView.setHasTransientState(true);`. Each
* `View.setHasTransientState(true)` call must be matched by a
* `View.setHasTransientState(false)` call, otherwise, the state of the View
* may become inconsistent. You should always prefer to end or cancel animations that are
* triggering the transient state instead of handling it manually.
*
* @param holder The ViewHolder containing the View that could not be recycled due to its
* transient state.
* @return True if the View should be recycled, false otherwise. Note that if this method
* returns `true`, RecyclerView *will ignore* the transient state of
* the View and recycle it regardless. If this method returns `false`,
* RecyclerView will check the View's transient state again before giving a final decision.
* Default implementation returns false.
* @since v3.1.0
*/
open fun onFailedToRecycleView(holder: VH): Boolean {
return false
}
/**
* Called when a view created by this [ItemViewDelegate] has been attached to a window.
*
* This can be used as a reasonable signal that the view is about to be seen
* by the user. If the [ItemViewDelegate] previously freed any resources in
* [onViewDetachedFromWindow][.onViewDetachedFromWindow]
* those resources should be restored here.
*
* @param holder Holder of the view being attached
* @since v3.1.0
*/
open fun onViewAttachedToWindow(holder: VH) {}
/**
* Called when a view created by this [ItemViewDelegate] has been detached from its
* window.
*
* Becoming detached from the window is not necessarily a permanent condition;
* the consumer of an Adapter's views may choose to cache views offscreen while they
* are not visible, attaching and detaching them as appropriate.
*
* @param holder Holder of the view being detached
* @since v3.1.0
*/
open fun onViewDetachedFromWindow(holder: VH) {}
}
@@ -0,0 +1,20 @@
package com.lowe.multitype
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
/**
* This is a compatible version of [ItemViewDelegate].
* @see ItemViewDelegate
* @author Drakeet Xu
*/
abstract class ItemViewBinder<T, VH : RecyclerView.ViewHolder> : ItemViewDelegate<T, VH>() {
final override fun onCreateViewHolder(context: Context, parent: ViewGroup): VH {
return onCreateViewHolder(LayoutInflater.from(context), parent)
}
abstract fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): VH
}
@@ -0,0 +1,55 @@
/*
* Copyright (c) 2016-present. Drakeet Xu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lowe.multitype
import androidx.recyclerview.widget.RecyclerView.ViewHolder
/***
* @author Drakeet Xu
* @since v4.1.0
*/
abstract class ItemViewDelegate<T, VH : ViewHolder> : ItemViewBaseDelegate<T, VH>(){
@Suppress("PropertyName")
internal var _adapter: MultiTypeAdapter? = null
/**
* Gets the associated [MultiTypeAdapter].
* @since v2.3.4
*/
val adapter: MultiTypeAdapter
get() {
if (_adapter == null) {
throw IllegalStateException(
"This $this has not been attached to MultiTypeAdapter yet. " +
"You should not call the method before registering the delegate."
)
}
return _adapter!!
}
/**
* Gets or sets the items of the associated [MultiTypeAdapter].
* @see MultiTypeAdapter.items
* @since v4.0.0
*/
var adapterItems: List<Any>
get() = adapter.items
set(value) {
adapter.items = value
}
}
@@ -0,0 +1,35 @@
/*
* Copyright (c) 2016-present. Drakeet Xu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lowe.multitype
/**
* An interface to link the items and delegates by the classes of delegates.
*
* @author Drakeet Xu
*/
interface JavaClassLinker<T> {
/**
* Returns the class of your registered delegates for your item.
*
* @param position The position in items
* @param item The item
* @return The index of your registered delegates
* @see OneToManyEndpoint.withJavaClassLinker
*/
fun index(position: Int, item: T): Class<out ItemViewDelegate<T, *>>
}
@@ -0,0 +1,37 @@
/*
* Copyright (c) 2016-present. Drakeet Xu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lowe.multitype
import kotlin.reflect.KClass
/**
* An interface to link the items and delegates by the classes of delegates.
*
* @author Drakeet Xu
*/
interface KotlinClassLinker<T> {
/**
* Returns the class of your registered delegates for your item.
*
* @param position The position in items
* @param item The item
* @return The index of your registered delegates
* @see OneToManyEndpoint.withJavaClassLinker
*/
fun index(position: Int, item: T): KClass<out ItemViewDelegate<T, *>>
}
@@ -0,0 +1,43 @@
/*
* Copyright (c) 2016-present. Drakeet Xu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lowe.multitype
import androidx.annotation.IntRange
/**
* An interface to link the items and delegates by the array index.
*
* @author Drakeet Xu
*/
interface Linker<T> {
/**
* Returns the index of your registered delegates for your item. The result should be in range of
* `[0, one-to-multiple-delegates.length)`.
*
* Note: The argument of [OneToManyFlow.to] is the
* one-to-multiple-delegates.
*
* @param position The position in items
* @param item The data item
* @return The index of your registered delegates
* @see OneToManyFlow.to
* @see OneToManyEndpoint.withLinker
*/
@IntRange(from = 0)
fun index(position: Int, item: T): Int
}
@@ -0,0 +1,41 @@
/*
* Copyright (c) 2016-present. Drakeet Xu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lowe.multitype
import androidx.recyclerview.widget.RecyclerView
import java.util.concurrent.CopyOnWriteArrayList
/**
* @author Drakeet Xu
*/
open class MultiTypeAdapter @JvmOverloads constructor(
/**
* Sets and updates the items atomically and safely. It is recommended to use this method
* to update the items with a new wrapper list or consider using [CopyOnWriteArrayList].
*
* Note: If you want to refresh the list views after setting items, you should
* call [RecyclerView.Adapter.notifyDataSetChanged] by yourself.
*
* @since v2.4.1
*/
open var items: List<Any> = emptyList(),
) : BaseMultiTypeAdapter() {
override fun getItem(position: Int) = items[position]
override fun getItemCount() = items.size
}
@@ -0,0 +1,49 @@
/*
* Copyright (c) 2016-present. Drakeet Xu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lowe.multitype
/**
* A TypePool implemented by an ArrayList.
*
* @author Drakeet Xu
*/
open class MutableTypes constructor(
open val initialCapacity: Int = 0,
open val types: MutableList<Type<*>> = ArrayList(initialCapacity)
) : Types {
override val size: Int get() = types.size
override fun <T> register(type: Type<T>) {
types.add(type)
}
override fun unregister(clazz: Class<*>): Boolean {
return types.removeAll { it.clazz == clazz }
}
@Suppress("UNCHECKED_CAST")
override fun <T> getType(index: Int): Type<T> = types[index] as Type<T>
override fun firstIndexOf(clazz: Class<*>): Int {
val index = types.indexOfFirst { it.clazz == clazz }
if (index != -1) {
return index
}
return types.indexOfFirst { it.clazz.isAssignableFrom(clazz) }
}
}
@@ -0,0 +1,58 @@
/*
* Copyright (c) 2016-present. Drakeet Xu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lowe.multitype
import androidx.annotation.CheckResult
/**
* @author Drakeet Xu
*/
internal class OneToManyBuilder<T>(
private val adapter: BaseMultiTypeAdapter,
private val clazz: Class<T>
) : OneToManyFlow<T>, OneToManyEndpoint<T> {
private var delegates: Array<ItemViewDelegate<T, *>>? = null
@SafeVarargs
@CheckResult(suggest = "#withLinker(Linker)")
override fun to(vararg delegates: ItemViewDelegate<T, *>) = apply {
@Suppress("UNCHECKED_CAST")
this.delegates = delegates as Array<ItemViewDelegate<T, *>>
}
@SafeVarargs
@CheckResult(suggest = "#withLinker(Linker)")
override fun to(vararg binders: ItemViewBinder<T, *>) = apply {
@Suppress("UNCHECKED_CAST")
this.delegates = binders as Array<ItemViewDelegate<T, *>>
}
override fun withLinker(linker: Linker<T>) {
doRegister(linker)
}
override fun withJavaClassLinker(javaClassLinker: JavaClassLinker<T>) {
withLinker(ClassLinkerBridge.toLinker(javaClassLinker, delegates!!))
}
private fun doRegister(linker: Linker<T>) {
for (delegate in delegates!!) {
adapter.register(Type(clazz, delegate, linker))
}
}
}
@@ -0,0 +1,67 @@
/*
* Copyright (c) 2016-present. Drakeet Xu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lowe.multitype
import kotlin.reflect.KClass
/**
* End-operators for one-to-many.
*
* @author Drakeet Xu
*/
interface OneToManyEndpoint<T> {
/**
* Sets a linker to link the items and delegates by array index.
*
* @param linker the row linker
* @see Linker
*/
fun withLinker(linker: Linker<T>)
fun withLinker(linker: (position: Int, item: T) -> Int) {
withLinker(object : Linker<T> {
override fun index(position: Int, item: T): Int {
return linker(position, item)
}
})
}
/**
* Sets a class linker to link the items and delegates by the class instance of delegates.
*
* @param javaClassLinker the class linker
* @see JavaClassLinker
*/
fun withJavaClassLinker(javaClassLinker: JavaClassLinker<T>)
private fun withJavaClassLinker(classLinker: (position: Int, item: T) -> Class<out ItemViewDelegate<T, *>>) {
withJavaClassLinker(object : JavaClassLinker<T> {
override fun index(position: Int, item: T): Class<out ItemViewDelegate<T, *>> {
return classLinker(position, item)
}
})
}
fun withKotlinClassLinker(classLinker: KotlinClassLinker<T>) {
withJavaClassLinker { position, item -> classLinker.index(position, item).java }
}
fun withKotlinClassLinker(classLinker: (position: Int, item: T) -> KClass<out ItemViewDelegate<T, *>>) {
withJavaClassLinker { position, item -> classLinker(position, item).java }
}
}
@@ -0,0 +1,39 @@
/*
* Copyright (c) 2016-present. Drakeet Xu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lowe.multitype
import androidx.annotation.CheckResult
/**
* Process and flow operators for one-to-many.
*
* @author Drakeet Xu
*/
interface OneToManyFlow<T> {
/**
* Sets some item view delegates to the item type.
*
* @param delegates the item view delegates
* @return end flow operator
*/
@CheckResult
fun to(vararg delegates: ItemViewDelegate<T, *>): OneToManyEndpoint<T>
@CheckResult
fun to(vararg delegates: ItemViewBinder<T, *>): OneToManyEndpoint<T>
}
@@ -0,0 +1,20 @@
package com.lowe.multitype
import android.content.Context
import android.view.LayoutInflater
import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView
/**
* This is a compatible version of [PagingItemViewDelegate].
* @see PagingItemViewDelegate
* @author Lowae
*/
abstract class PagingItemViewBinder<T, VH : RecyclerView.ViewHolder> : PagingItemViewDelegate<T, VH>() {
final override fun onCreateViewHolder(context: Context, parent: ViewGroup): VH {
return onCreateViewHolder(LayoutInflater.from(context), parent)
}
abstract fun onCreateViewHolder(inflater: LayoutInflater, parent: ViewGroup): VH
}
@@ -0,0 +1,46 @@
/*
* Copyright (c) 2016-present. Drakeet Xu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lowe.multitype
import androidx.recyclerview.widget.RecyclerView.ViewHolder
/***
* @author Lowae
*/
abstract class PagingItemViewDelegate<T, VH : ViewHolder>:ItemViewBaseDelegate<T, VH>() {
/**
* This is a [PagingMultiTypeAdapter] like PagingDataAdapter
*/
@Suppress("PropertyName")
internal var _adapter: PagingMultiTypeAdapter<*>? = null
/**
* Gets the associated [PagingMultiTypeAdapter].
* @since v2.3.4
*/
val adapter: PagingMultiTypeAdapter<*>
get() {
if (_adapter == null) {
throw IllegalStateException(
"This $this has not been attached to PagingMultiTypeAdapter yet. " +
"You should not call the method before registering the delegate."
)
}
return _adapter!!
}
}
@@ -0,0 +1,47 @@
package com.lowe.multitype
import androidx.paging.LoadState
import androidx.paging.LoadStateAdapter
import androidx.recyclerview.widget.ConcatAdapter
/**
* Adapter for displaying a RecyclerView item based on [LoadState], such as a loading spinner, or
* a retry error button.
* more @see [LoadStateAdapter]
*
* @param types must passed base [PagingMultiTypeAdapter.types] to share since set [ConcatAdapter.Config.isolateViewTypes] is FALSE
*
* @author Lowae
*/
open class PagingLoadStateAdapter(binder: FooterStateItemBinder<*>, override var types: Types) :
BaseMultiTypeAdapter() {
init {
register(binder)
}
var loadState: LoadState = LoadState.NotLoading(endOfPaginationReached = false)
set(loadState) {
if (field != loadState) {
val oldItem = displayLoadStateAsItem(field)
val newItem = displayLoadStateAsItem(loadState)
if (oldItem && !newItem) {
notifyItemRemoved(0)
} else if (newItem && !oldItem) {
notifyItemInserted(0)
} else if (oldItem && newItem) {
notifyItemChanged(0)
}
field = loadState
}
}
override fun getItem(position: Int) = loadState
override fun getItemCount(): Int = if (displayLoadStateAsItem(loadState)) 1 else 0
open fun displayLoadStateAsItem(loadState: LoadState): Boolean {
return loadState is LoadState.Loading || loadState is LoadState.Error || (loadState is LoadState.NotLoading && loadState.endOfPaginationReached)
}
}
@@ -0,0 +1,117 @@
package com.lowe.multitype
import androidx.annotation.IntRange
import androidx.lifecycle.Lifecycle
import androidx.paging.*
import androidx.recyclerview.widget.AdapterListUpdateCallback
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.DiffUtil
import androidx.recyclerview.widget.RecyclerView
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
/**
* [RecyclerView.Adapter] base class for presenting paged data from [PagingData]s in
* a [RecyclerView].
* more @see [PagingDataAdapter]
*
* @author Lowae
*/
class PagingMultiTypeAdapter<T : Any>(
diffCallback: DiffUtil.ItemCallback<T>,
mainDispatcher: CoroutineDispatcher = Dispatchers.Main,
workerDispatcher: CoroutineDispatcher = Dispatchers.Default,
override val initialCapacity: Int = 0,
override var types: Types = MutableTypes(initialCapacity)
) : BaseMultiTypeAdapter() {
private val differ = AsyncPagingDataDiffer(
diffCallback = diffCallback,
updateCallback = AdapterListUpdateCallback(this),
mainDispatcher = mainDispatcher,
workerDispatcher = workerDispatcher
)
override fun setHasStableIds(hasStableIds: Boolean) =
throw UnsupportedOperationException("Stable ids are unsupported on PagingDataAdapter.")
override fun getItem(@IntRange(from = 0) position: Int) = differ.getItem(position) as Any
override fun getItemCount() = differ.itemCount
@Suppress("UNCHECKED_CAST")
suspend fun submitData(pagingData: PagingData<*>) =
differ.submitData(pagingData as PagingData<T>)
@Suppress("UNCHECKED_CAST")
fun submitData(lifecycle: Lifecycle, pagingData: PagingData<*>) =
differ.submitData(lifecycle, pagingData as PagingData<T>)
fun retry() = differ.retry()
fun refresh() = differ.refresh()
fun peek(@IntRange(from = 0) index: Int) = differ.peek(index)
fun snapshot(): ItemSnapshotList<T> = differ.snapshot()
val loadStateFlow: Flow<CombinedLoadStates> = differ.loadStateFlow
val onPagesUpdatedFlow: Flow<Unit> = differ.onPagesUpdatedFlow
fun addLoadStateListener(listener: (CombinedLoadStates) -> Unit) =
differ.addLoadStateListener(listener)
fun removeLoadStateListener(listener: (CombinedLoadStates) -> Unit) =
differ.removeLoadStateListener(listener)
fun addOnPagesUpdatedListener(listener: () -> Unit) = differ.addOnPagesUpdatedListener(listener)
fun removeOnPagesUpdatedListener(listener: () -> Unit) =
differ.removeOnPagesUpdatedListener(listener)
fun withLoadStateHeader(
headerStateAdapter: PagingLoadStateAdapter,
header: HeaderStateItemBinder<*>
): ConcatAdapter {
headerStateAdapter.types = this.types
headerStateAdapter.register(header)
addLoadStateListener { loadStates ->
headerStateAdapter.loadState = loadStates.prepend
}
return ConcatAdapter(concatAdapterConfig(), headerStateAdapter, this)
}
fun withLoadStateFooter(
footerStateAdapter: PagingLoadStateAdapter,
): ConcatAdapter {
addLoadStateListener { loadStates ->
// Avoid of shown footer when adapter is empty
if (itemCount != 0) {
footerStateAdapter.loadState = loadStates.append
}
}
return ConcatAdapter(concatAdapterConfig(), this, footerStateAdapter)
}
fun withLoadStateHeaderAndFooter(
headerStateAdapter: PagingLoadStateAdapter,
header: HeaderStateItemBinder<*>,
footerStateAdapter: PagingLoadStateAdapter,
footer: FooterStateItemBinder<*>
): ConcatAdapter {
headerStateAdapter.types = this.types
headerStateAdapter.register(header)
footerStateAdapter.types = this.types
footerStateAdapter.register(footer)
addLoadStateListener { loadStates ->
headerStateAdapter.loadState = loadStates.prepend
footerStateAdapter.loadState = loadStates.append
}
return ConcatAdapter(concatAdapterConfig(), headerStateAdapter, this, footerStateAdapter)
}
private fun concatAdapterConfig() = ConcatAdapter.Config.Builder().setIsolateViewTypes(false)
.setStableIdMode(ConcatAdapter.Config.StableIdMode.NO_STABLE_IDS).build()
}
@@ -0,0 +1,26 @@
/*
* Copyright (c) 2016-present. Drakeet Xu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lowe.multitype
/**
* @author Drakeet Xu
*/
data class Type<T>(
val clazz: Class<out T>,
val delegate: ItemViewBaseDelegate<T, *>,
val linker: Linker<T>
)
@@ -0,0 +1,61 @@
/*
* Copyright (c) 2016-present. Drakeet Xu
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.lowe.multitype
/**
* An ordered collection to hold the types, delegates and linkers.
*
* @author Drakeet Xu
*/
interface Types {
/**
* Returns the size of the [Types].
*/
val size: Int
fun <T> register(type: Type<T>)
/**
* Unregister all types indexed by the specified class.
*
* @param clazz the main class of a [Type]
* @return true if any types are unregistered from this [Types]
*/
fun unregister(clazz: Class<*>): Boolean
/**
* Gets the type at the specified index.
*
* @param index the type index
* @return the [Type] at the specified index
* @throws IndexOutOfBoundsException if the index is out of range
*/
fun <T> getType(index: Int): Type<T>
/**
* For getting index of the type class. If the subclass is already registered,
* the registered mapping is used. If the subclass is not registered, then look
* for its parent class if is registered, if the parent class is registered,
* the subclass is regarded as the parent class.
*
* @param clazz the type class.
* @return The index of the first occurrence of the specified class in this [Types],
* or -1 if this [Types] does not contain the class.
*/
fun firstIndexOf(clazz: Class<*>): Int
}
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<item name="tagViewHolder" type="id" />
</resources>
@@ -0,0 +1,17 @@
package com.lowe.multitype
import org.junit.Test
import org.junit.Assert.*
/**
* Example local unit test, which will execute on the development machine (host).
*
* See [testing documentation](http://d.android.com/tools/testing).
*/
class ExampleUnitTest {
@Test
fun addition_isCorrect() {
assertEquals(4, 2 + 2)
}
}