smart-recycler-adapter
Never code any boilerplate RecyclerAdapter again! This library will make it easy and painless to map your data item with a target ViewHolder.
Features
OnViewEventListener
- Smart OnClick / OnLongClickListener SimpleItemOnClickOnLongClickActivity
- State holding with OnItemSelectedListener MultipleViewTypesResolverActivity
- Custom View Events CustomViewEventActivity
ItemTouchHelper Swipe, Drag & Drop extensions
- Drag & drop DragAndDropItemActivity
- Drag & drop with handle DragAndDropHandleItemActivity
- Swipe to remove item SwipeRemoveItemActivity
- Drag & drop, Swipe, View Events MultipleEventsAndExtensionsActivity
- Grid + Drag & drop GridActivity
ViewTypeResolver
- Multiple ViewHolder types resolver MultipleViewTypesResolverActivity
SmartStateHolder
- Multiple items select MultiSelectItemsActivity
- Single RadioButton select SingleSelectRadioButtonItemActivity
- Multiple CheckBox select MultiSelectCheckBoxItemsActivity
- Multiple Switch select MultiSelectSwitchItemsActivity
- Multiple Expandable items MultipleExpandableItemActivity
- Single Expandable item SingleExpandableItemActivity
Sticky header
- Simple sticky header StickyHeaderActivity
- Multiple expandable items + sticky header MultipleExpandableItemHeaderActivity
Nested adapter
- Nested SmartRecyclerAdapter NestedSmartRecyclerAdaptersActivity
Pagination
- Endless scroll EndlessScrollActivity
- Endless scroll with load more button EndlessScrollLoadMoreButtonActivity
DiffUtil
- Diff Util extension DiffUtilActivity
Filter
- SimpleFilterActivity SimpleFilterActivity
- SimpleFilterDiffSwapActivity SimpleFilterDiffSwapActivity
- FilterGridActivity FilterGridActivity
Release overview
- Extension libraries (ViewEvent, DiffUtil, NestedAdapter, StickyHeader, Filter) v5.0.0-rc01
- Kotlin + AndroidX (jcenter, jitpack) v4.0.0
- Java + AndroidX (jcenter, jitpack) v3.0.0
- Java + AppCompat (jitpack) v2.2.0
Gradle
Add jcenter()
or maven { url "https://dl.bintray.com/manneohlund/maven" }
to your build.gradle
under repositories
Core
dependencies {
// Core SmartRecyclerAdapter
implementation 'io.github.manneohlund:smart-recycler-adapter:5.0.0-rc01'
}
Extensions
dependencies {
// ViewEvent click listeners, multi select, swipe dismiss and drag & drop
implementation 'io.github.manneohlund:smart-recycler-adapter-viewevent:1.0.0-beta03'
// DiffUtil extension library
implementation 'io.github.manneohlund:smart-recycler-adapter-diffutil:1.0.0-beta01'
// Nested adapter extension library
implementation 'io.github.manneohlund:smart-recycler-adapter-nestedadapter:1.0.0-beta01'
// Sticky header extension library
implementation 'io.github.manneohlund:smart-recycler-adapter-stickyheader:1.0.0-alpha02'
// Filter extension library
implementation 'io.github.manneohlund:smart-recycler-adapter-filter:1.0.0-alpha01'
}
Basic
Basic adapter creation
SmartRecyclerAdapter
.items(items)
.map(MoviePosterModel::class, PosterViewHolder::class)
.map(MovieBannerModel::class, BannerViewHolder::class)
.map(MovieModel::class, MovieViewHolder::class)
.map(TopNewsModel::class, TopNewsViewHolder::class)
.add(OnClickEventListener { event: ViewEvent.OnClick ->
// Handle event
})
.into<SmartRecyclerAdapter>(recyclerView)
SmartViewHolder
Just extend your ViewHolder class with SmartViewHolder
and pass in the target type ex SmartViewHolder<Mail>
.
Note that the constructor can both take View
or ViewGroup
as parameter, in this case PosterViewHolder(parentView: ViewGroup)
to avoid casting to ViewGroup while inflating.
The parentView
is the recyclerView.
The method unbind
has an default implementation and is optional.
class PosterViewHolder(parentView: ViewGroup) :
SmartViewHolder<MovieModel>(parentView, R.layout.poster_item) {
override fun bind(movie: MovieModel) {
Glide.with(imageView)
.load(model.posterUrl)
.into(imageView)
}
override fun unbind() {
Glide.with(imageView).clear(imageView)
}
}
Works with Android DataBinding! Just add the DataBinding LayoutInflater in super
call. 🚀
class PosterViewHolder(parentView: ViewGroup) :
SmartViewHolder<MovieModel>(
LayoutInflater.from(parentView.context)
.inflate(R.layout.poster_item, parentView, false)
)
Adapter creation with ViewTypeResolver
If you want to bind one data type with different view holders depending on some attribute you can set a ViewTypeResolver.
Note .map() call not needed in this case but you can combine if you want to.
SmartRecyclerAdapter
.items(items)
.setViewTypeResolver{ item, position -> {
when {
item is MovieTrailerModel -> MovieTrailerViewHolder::class
item is MovieModel && item.isRatedR() -> RMovieViewHolder::class
else -> MovieViewHolder::class // Add default view if needed, else SmartRecyclerAdapter will look at the base `.map` mapping
}
}}
.into(recyclerView)
SmartEndlessScrollRecyclerAdapter
A popular feature in apps is to have endless scrolling with pagination, in other words load more items when user has scrolled to bottom. With SmartEndlessScrollRecyclerAdapter you can achieve this.
setAutoLoadMoreEnabled
defines if falseload more
button should be visible before loading.setLoadMoreLayoutResource
can also set your custom loading/loadmore view.OnLoadMoreListener
is called when scrolled to the last item and loading view is visible.
Create SmartEndlessScrollRecyclerAdapter
val endlessScrollAdapter: SmartEndlessScrollRecyclerAdapter = SmartEndlessScrollRecyclerAdapter
.items(items)
.setAutoLoadMoreEnabled(true)
.setLoadMoreLayoutResource(R.layout.custom_loadmore_view)
.setOnLoadMoreListener { adapter, loadMoreViewHolder ->
// Handle load more items
}
.map(MovieModel::class, MovieViewHolder::class)
.into(recyclerView)
More SmartEndlessScrollRecyclerAdapter features
Enable/Disable endless scrolling and thus removing the loading view.
endlessScrollAdapter.isEndlessScrollEnabled = false
Extension libraries
smart-recycler-adapter-viewevent
As of smart-recycler-adapter:v5.0.0
all ViewEvent listeners have been removed from SmartRecyclerAdapter
and added in this extension library smart-recycler-adapter-viewevent
.
Essentially the SmartRecyclerAdapter
will now hold a list of SmartViewHolderBinder
that can implement any of these interfaces to listen to the adapter view holder stages:
OnSmartRecycleAdapterCreatedListener
Invoked fromSmartRecyclerAdapter
initOnCreateViewHolderListener
Invoked fromSmartRecyclerAdapter.onCreateViewHolder
OnBindViewHolderListener
Invoked fromSmartRecyclerAdapter.onBindViewHolder
OnViewAttachedToWindowListener
Invoked fromSmartRecyclerAdapter.onViewAttachedToWindow
OnViewDetachedFromWindowListener
Invoked fromSmartRecyclerAdapter.onViewDetachedFromWindow
This way all extension libraries has full control over the view holder lifecycle stages and can be hooked with various listeners and state holders.
You can create any type of SmartViewHolderBinder
extension and implement any number of the listed adapter listeners.
View Events
In io.github.manneohlund:smart-recycler-adapter-viewevent
comes with a range of ViewEvent listeners.
Default viewId
is R.id.undefined
that targets root view of the ViewHolder (ViewHolder.itemView).
SmartRecyclerAdapter
.items(items)
.map(MovieModel::class, MovieViewHolder::class)
// Your ViewHolder must implement CustomViewEventListenerHolder & SmartAdapterHolder
.add(OnCustomViewEventListener { event: ViewEvent -> })
// Adds click event listener to all SmartViewHolder root itemView
.add(OnClickEventListener { event: ViewEvent.OnClick -> })
// Adds long click event listener to all SmartViewHolder root itemView
.add(OnLongClickEventListener { event: ViewEvent.OnLongClick -> })
// Adds click event listener to PosterViewHolder root itemView
.add(OnClickEventListener(PosterViewHolder::class) { event: ViewEvent.OnClick -> })
// Adds click event listener to PosterViewHolder on view with id R.id.playButton
.add(OnClickEventListener(PosterViewHolder::class, R.id.playButton){ event: ViewEvent.OnClick -> })
// Adds touch event listener to PosterViewHolder
.add(OnTouchEventListener(PosterViewHolder::class) { event: ViewEvent.OnTouchEvent ->
when(it.event.action) {
MotionEvent.ACTION_UP -> // Handle touch event
}
})
.into(recyclerView)
SmartStateHolder & ViewEventViewModel
With OnMultiItemSelectListener
, OnMultiItemCheckListener
, OnSingleItemSelectListener
& OnSingleItemCheckListener
you can easily keep track on selection states.
In combination with ViewEventViewModel
you can keep selection states during screen rotation within the Activity lifecycle.
ViewEventViewModel
provides a live data for the selection events.
OnMultiItemSelectListener
OnMultiItemSelectListener holds multi select states for recycler adapter positions and takes 4 arguments:
- If
enableOnLongClick
is true multi select will be enabled after a long click, otherwise a regularViewEvent.OnClick
will be emitted when tapping. viewId
is by defaultR.id.undefined to target all SmartViewHolder.itemView
.viewHolderType
is by defaultSmartViewHolder
::class to target all view holders.eventListener
is by default noop in case ofOnMultiItemSelectListener
will be used withViewEventViewModel
along with live data observer.
// Define your ViewEventViewModel for OnMultiItemSelectListener to preserve state.
class MultiItemSelectViewModel :
ViewEventViewModel<ViewEvent, OnMultiItemSelectListener>(
OnMultiItemSelectListener(
enableOnLongClick = true,
)
)
// Get MultiItemSelectViewModel by androidx default viewModels provider.
private val multiItemSelectViewModel: MultiItemSelectViewModel by viewModels()
// Observe ViewEvent live data.
SmartRecyclerAdapter
.items(items)
.map(Integer::class, SimpleSelectableItemViewHolder::class)
.add(multiItemSelectViewModel.observe(this) { event: ViewEvent ->
// Either ViewEvent.OnClick or ViewEvent.OnItemSelected when enableOnLongClick = true
})
.into(recyclerView)
See sample app section: #SmartStateHolder
Drag & Drop
AutoDragAndDropBinder
will be activated on long press if longPressDragEnabled = true
and on release the AutoDragAndDropBinder
will automatically notify the SmartRecyclerAdapter
about the item move.
You can extend the BasicDragAndDropBinder
or DragAndDropEventBinder
and create your custom implementation.
SmartRecyclerAdapter
.items(items)
.map(Integer::class, SimpleItemViewHolder::class)
.add(AutoDragAndDropBinder(longPressDragEnabled = true) { event: ViewEvent.OnItemMoved ->
// Handle drag event
})
.into(recyclerView)
See sample app section: #SmartStateHolder
Swipe dismiss
AutoRemoveItemSwipeEventBinder
will automatically remove the item from the adapter on swipe.
You can extend the BasicSwipeEventBinder
or SwipeEventBinder.kt
and create your custom implementation.
SmartRecyclerAdapter
.items(items)
.map(Integer::class, SimpleItemViewHolder::class)
.add(AutoRemoveItemSwipeEventBinder(ItemTouchHelper.LEFT or ItemTouchHelper.RIGHT) { event: ViewEvent.OnItemSwiped ->
// Handle swipe event
})
.into(recyclerView)
See sample app section: #SmartStateHolder
smart-recycler-adapter-stickyheader
With io.github.manneohlund:smart-recycler-adapter-stickyheader
it’s super easy to add a sticky header recycler view item decoration.
Just set the target headerItemType
and the StickyHeaderItemDecorationExtension
will do the rest.
You can even add a sticky header item touch event listener.
SmartRecyclerAdapter
.items(items)
.map(String::class, SimpleHeaderViewHolder::class)
.map(Integer::class, SimpleItemViewHolder::class)
.add(StickyHeaderItemDecorationExtension(
headerItemType = HeaderViewHolder::class
) { motionEvent, itemPosition ->
if (motionEvent.action == MotionEvent.ACTION_UP) {
showToast("Header $itemPosition clicked")
}
})
.into(recyclerView)
See sample app section: #Sticky header
smart-recycler-adapter-diffutil
As of smart-recycler-adapter:v5.0.0
diff util have been removed from SmartRecyclerAdapter
and is added in this extension library smart-recycler-adapter-diffutil
.
Essentially the SmartRecyclerAdapter
will now hold a map of SmartRecyclerAdapterBinder
that is the basic interface for SmartRecyclerAdapter
binding extensions.
// If adapter items contains unspecified super type DiffPredicate bust be of type Any, DiffPredicate<Any>
private val predicate = object : DiffUtilExtension.DiffPredicate<Int> {
override fun areItemsTheSame(oldItem: Int, newItem: Int): Boolean {
return oldItem == newItem
}
override fun areContentsTheSame(oldItem: Int, newItem: Int): Boolean {
return oldItem == newItem
}
}
// Add SimpleDiffUtilExtension to the adapter
SmartRecyclerAdapter
.items((0..100).toMutableList())
.map(Integer::class, SimpleItemViewHolder::class)
.add(SimpleDiffUtilExtension(predicate))
.into(recyclerView)
// Add some new random items
smartRecyclerAdapter.diffSwapList((0..100).shuffled().toMutableList())
See sample app section: #DiffUtil
smart-recycler-adapter-nestedadapter
As of smart-recycler-adapter:v5.0.0
static nested adapter mapping have been removed from SmartRecyclerAdapter
and is added in this extension library smart-recycler-adapter-nestedadapter
.
Default binder in nestedadapter is SmartNestedAdapterBinder
implements SmartViewHolderBinder
for basic view holder mapping functionality.
SmartRecyclerAdapter
will hold the SmartNestedAdapterBinder
references and call the default implemented interfaces OnCreateViewHolderListener
, OnBindViewHolderListener
, OnViewRecycledListener
on ViewHolder lifecycle stages.
SmartViewHolder
subclasses must implement SmartNestedRecyclerViewHolder
in order for SmartNestedAdapterBinder
to get the target recyclerView.
How does it work? 👇
SmartViewHolder
Sample uses kotlin synthetic view property import!
class NestedRecyclerViewHolder(parentView: ViewGroup) :
SmartViewHolder<MovieCategory>(parentView, R.layout.nested_recycler_view),
SmartNestedRecyclerViewHolder {
override val recyclerView: RecyclerView = itemView.nestedRecyclerView
init {
// Set RecyclerView properties here or with RecyclerViewBinder
itemView.nestedRecyclerView.apply {
layoutManager = LinearLayoutManager(context, HORIZONTAL, false)
isNestedScrollingEnabled = false
setHasFixedSize(true)
}
}
override fun bind(item: MovieCategory) {
itemView.title.text = item.title
}
}
SmartRecyclerAdapter
SmartNestedAdapterBinder
will only target NestedRecyclerViewHolder
.
Supply a SmartAdapterBuilder
or SmartEndlessScrollAdapterBuilder
that will be build a new nested adapter for each NestedRecyclerViewHolder
.
With reuseParentAdapterRecycledViewPool
set to true
will reuse the parent SmartRecyclerAdapters
RecyclerView.RecycledViewPool
in all nested adapters.
SmartRecyclerAdapter
.items(items)
.add(
SmartNestedAdapterBinder(
viewHolderType = NestedRecyclerViewHolder::class,
reuseParentAdapterRecycledViewPool = true,
smartRecyclerAdapterBuilder = SmartRecyclerAdapter.empty()
.map(MovieModel::class, ThumbViewHolder::class)
.add(OnClickEventListener { event: ViewEvent.OnClick ->
// Handle nested adapter item click event
})
)
)
.add(OnClickEventListener(NestedRecyclerViewHolder::class, R.id.more) {
// Handle parent adapter click event
})
.into(recyclerView)
See sample app section: #Nested SmartRecyclerAdapters
smart-recycler-adapter-filter
With the FilterExtension
extension you can synchronously or asynchronously filter your items.
Create SmartRecyclerAdapter
SmartRecyclerAdapter.items(items)
.map(String::class, HeaderViewHolder::class)
.map(Int::class, FilterItemViewHolder::class)
.add(OnClickEventListener {
// Handle click event
})
.add(
FilterExtension(
filterPredicate = { item, constraint ->
when (item) {
is Int -> item.toString().contains(constraint)
else -> true
}
},
loadingStateListener = { isLoading ->
// Set loading progress visibility
}
)
)
.into(recyclerView)
Set search view filter
searchView.setOnQueryTextListener(object : android.widget.SearchView.OnQueryTextListener {
// Call some filter function ex: filter(newText)
})
Filter
fun filter(query: String?) {
val filterExtension: FilterExtension = smartAdapter.get()
filterExtension.filter(lifecycleScope, query, autoSetNewItems = true)
}
See sample app section: #Filter
Proguard
Only known rule is to keep constructor for all ViewHolders.
This rule is auto included in the consumer-rules.pro
for smart-recycler-adapter
library so no manual config is needed.
-keepclassmembers class **ViewHolder {
public <init>(**);
}
More
For more samples test out the sample app and see the source code.
RecyclableViewHolder
Sometimes a ViewHolder created by the Adapter cannot be recycled due to its transient state.
In order to fix this is to implement RecyclableViewHolder
in your SmartViewHolder
extension so that 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.
class MovieViewHolder : SmartViewHolder, RecyclableViewHolder {
override fun onFailedToRecycleView(): Boolean = true
}
OnViewAttachedToWindowListener and OnViewDetachedFromWindowListener
If you want to catch when the view is attached and detached from the window in your ViewHolder you can implement OnViewAttachedToWindowListener
and OnViewDetachedFromWindowListener
in your SmartViewHolder
extension.
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.
class MovieViewHolder : SmartViewHolder,
OnViewAttachedToWindowListener,
OnViewDetachedFromWindowListener {
override fun onViewAttachedToWindow(viewHolder: RecyclerView.ViewHolder) {
// Restore
}
override fun onViewDetachedFromWindow(viewHolder: RecyclerView.ViewHolder) {
// Cache
}
}
More SmartRecyclerAdapter features
val adapter: SmartRecyclerAdapter = SmartRecyclerAdapter
.items(items)
.map(MovieModel::class, MovieViewHolder::class)
.into(recyclerView)
// We can add more data
adapter.addItems(items)
// Add data at index with animation
adapter.addItem(0, item)
// Add data at index without animation
adapter.addItem(0, item, false)
// Remove item at index with animation
adapter.removeItem(0)
// Remove item at index without animation
adapter.removeItem(0, false)
// Replace item at index with animation
adapter.replaceItem(0, item)
// Replace item at index without animation
adapter.replaceItem(0, item, false)
// Get items by type
adapter.getItems(MovieModel::class)
// Delete all items in the list
adapter.clear()