Never code any boilerplate RecyclerAdapter again!
This library will make it easy and painless to map your data item with a target ViewHolder.
- Smart OnClick / OnLongClickListener SimpleItemOnClickOnLongClickActivity
- State holding with OnItemSelectedListener MultipleViewTypesResolverActivity
- Custom View Events CustomViewEventActivity
- Drag & drop DragAndDropItemActivity
- Drag & drop with handle DragAndDropHandleItemActivity
- Swipe to remove item SwipeRemoveItemActivity
- Drag & drop, Swipe, View Events MultipleEventsAndExtensionsActivity
- Grid + Drag & drop GridActivity
- Multiple ViewHolder types resolver MultipleViewTypesResolverActivity
- Multiple items select MultiSelectItemsActivity
- Single RadioButton select SingleSelectRadioButtonItemActivity
- Multiple CheckBox select MultiSelectCheckBoxItemsActivity
- Multiple Switch select MultiSelectSwitchItemsActivity
- Multiple Expandable items MultipleExpandableItemActivity
- Single Expandable item SingleExpandableItemActivity
- Nested SmartRecyclerAdapter NestedSmartRecyclerAdaptersActivity
- Endless scroll EndlessScrollActivity
- Endless scroll with load more button EndlessScrollLoadMoreButtonActivity
- Diff Util extension DiffUtilActivity
- Kotlin + AndroidX (jcenter, jitpack) v4.0.0
- Java + AndroidX (jcenter, jitpack) v3.0.0
- Java + AppCompat (jitpack) v2.2.0
Add jcenter()
or maven { url "https://dl.bintray.com/manneohlund/maven" }
to your build.gradle
under repositories
dependencies {
implementation 'io.github.manneohlund:smart-recycler-adapter:4.1.0'
}
SmartRecyclerAdapter
.items(items)
.map(MoviePosterModel::class, PosterViewHolder::class)
.map(MovieBannerModel::class, BannerViewHolder::class)
.map(MovieModel::class, MovieViewHolder::class)
.map(TopNewsModel::class, TopNewsViewHolder::class)
.into<SmartRecyclerAdapter>(recyclerView)
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(ViewGroup parentView)
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>(
LayoutInflater.from(parentView.context).inflate(R.layout.poster_item, parentView, false)
) {
override fun bind(movie: MovieModel) {
Glide.with(imageView)
.load(model.posterUrl)
.into(imageView)
}
override fun unbind() {
Glide.with(imageView).clear(imageView)
}
}
You can easily assign events to views and add an OnViewEventListener
to the SmartRecyclerAdapter for easy event handling.
SmartRecyclerAdapter
.items(items)
.map(MovieModel::class, MovieViewHolder::class)
// Adds a basic `OnViewEventListener` to any `SmartViewHolder` extension that implements `ViewEventListenerHolder`
.addViewEventListener(onViewEventListener { view, viewEventId, position -> handleItemEvent()})
.into<SmartRecyclerAdapter>(recyclerView)
In your view holder, add eg OnClickListener
to a view and call onViewEvent
on the OnViewEventListener
.
Your ViewHolder
must implements ViewEventListenerHolder
to receive the OnViewEventListener
.
open class SimpleItemViewHolder(parentView: ViewGroup) : SmartViewHolder<Int>(
LayoutInflater.from(parentView.context).inflate(R.layout.simple_item, parentView, false)
), ViewEventListenerHolder {
override lateinit var viewEventListener: OnViewEventListener
init {
itemView.setOnClickListener { view ->
viewEventListener.onViewEvent(view, R.id.action_play_movie, adapterPosition)
}
}
override fun bind(item: Int) {
// Handle binding
}
}
Kotlin has no SAM constructors so instead of writing interface instantiation you can utilize lambda helper methods for all predefined library event listeners ex:
.addViewEventListener(onViewEventListener { view, viewEventId, position ->
// Handle event
})
is same as
.addViewEventListener(object : OnViewEventListener {
override fun onViewEvent(view: View, viewEventId: ViewEventId, position: Position) {
// Handle event
}
})
If you are lazy and want to auto assign a predefined onClickListener
and onLongClickListener
with viewEventIds R.id.event_on_click
and R.id.event_on_long_click
,
Default implemented view event id for OnItemClickListener
is R.id.event_on_click
.
Default implemented view id for OnItemClickListener
is R.id.undefined
.
R.id.undefined
targets root view of the ViewHolder (ViewHolder.itemView).
interface OnMovieItemClickListener : OnItemClickListener {
override val viewHolderType: SmartViewHolderType
get() = MovieViewHolder::class
}
SmartRecyclerAdapter will automatically bind an View.OnClickListener
to a view with id R.id.movie_info_button
.
interface OnMovieInfoButtonClickListener : OnItemClickListener {
override val viewHolderType: SmartViewHolderType
get() = MovieViewHolder::class
override val viewId: ViewId
get() = R.id.movie_info_button
}
And add event listener to SmartRecyclerAdapter
builder.
SmartRecyclerAdapter
.items(items)
.map(HeaderModel::class, HeaderViewHolder::class)
.map(MovieModel::class, MovieViewHolder::class)
.map(MovieTrailerModel::class, MovieTrailerViewHolder::class)
// Adds `OnItemClickListener` and auto binds `View.OnClickListener` on all ViewHolders.
.addViewEventListener(onItemClickListener { view, viewEventId, position -> handleEvent(viewEventId) })
// Adds event listener for MovieViewHolder only and overrides any generic `OnItemClickListener`
.addViewEventListener(object : OnMovieItemClickListener {
override fun onViewEvent(view: View, viewEventId: ViewEventId, position: Position) {
playMovie()
}
})
// Adds event listener for MovieViewHolder only and auto binds `View.OnClickListener` on view with id `R.id.movie_info_button`
.addViewEventListener(object : OnMovieInfoButtonClickListener {
override fun onViewEvent(view: View, viewEventId: ViewEventId, position: Position) {
showMovieInfo(position)
}
})
.into<SmartRecyclerAdapter>(recyclerView)
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<SmartRecyclerAdapter>(recyclerView)
New in SmartRecyclerAdapter
v2.0.0 is support for nested recycler adapter.
Now you can easily build complex nested adapters without hustle and have full control of the adapter in your view controlling Fragment
or Activity
.
Use the new create()
method instead of the into(recyclerView)
to create just the SmartRecyclerAdapter
then set the adapter to the recycler view in your ViewHolder
.
Just implement the SmartAdapterHolder
interface in your ViewHolder
and SmartRecyclerAdapter
will handle the mapping.
val myWatchListSmartMovieAdapter: SmartRecyclerAdapter = SmartRecyclerAdapter
.items(myWatchListItems)
.map(MovieModel::class, ThumbViewHolder::class)
.addViewEventListener(onItemClickListener { view, viewEventId, position -> playMovie() })
.create()
SmartRecyclerAdapter
.items(items)
.map(MoviePosterModel::class, PosterViewHolder::class)
.map(MyWatchListModel::class, MyWatchListViewHolder::class)
.map(MyWatchListViewHolder::class, myWatchListSmartMovieAdapter)
.into<SmartRecyclerAdapter>(recyclerView)
class MyWatchListViewHolder : SmartViewHolder<MyWatchListModel>, SmartAdapterHolder {
override var smartRecyclerAdapter: SmartRecyclerAdapter? = null
set(value) {
field = value
recyclerView.layoutManager = LinearLayoutManager(recyclerView.context, HORIZONTAL, false)
recyclerView.adapter = value
}
override fun bind(myWatchListModel: MyWatchListModel) {
// bind model data to views
}
override fun unbind() {
// optional unbinding of view data model
}
}
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.
val endlessScrollAdapter: SmartEndlessScrollRecyclerAdapter = SmartEndlessScrollRecyclerAdapter
.items(items)
.map(MovieModel::class, MovieViewHolder::class)
.into(recyclerView)
Called when scrolled to the last item and loading view is visible.
endlessScrollAdapter.setOnLoadMoreListener{
endlessScrollAdapter.addItems(moreItems)
}
Enable/Disable endless scrolling and thus removing the loading view.
endlessScrollAdapter.isEndlessScrollEnabled = false
You can also set your custom loading/loadmore view.
endlessScrollAdapter.setCustomLoadMoreLayoutResource(R.layout.your_custom_loadmore_view);
For more samples test out the sample app and see the source code.
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
}
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 guides coming to the Wiki Page
Variable parameter overloading with many different addViewEventListener
calls.
.addViewEventListener(
MovieViewHolder.class,
R.id.event_on_click,
(view, viewEventId, position) -> playMovie())
class MovieViewHolder
extends SmartAutoEventViewHolder<MyWatchListModel>
implements SmartAdapterHolder {}
Create an OnItemClickListener
for MovieViewHolder.
SmartAutoEvent
implementations has been removed so no need for ex MovieViewHolder
to extend SmartAutoEventViewHolder
.
interface OnMovieItemClickListener extends OnItemClickListener {
@NonNull
@Override
default Class<? extends SmartViewHolder> getViewHolderType() {
return MovieViewHolder.class;
}
}
Add listener to the SmartAdapterBuilder.
.addViewEventListener((OnMovieItemClickListener) (view, viewEventId, position) -> playMovie())
In Kotlin the interface properties are overridden instead of default methods as in java.
SmartViewHolderType
is a typealias of KClass<out SmartViewHolder<*>>
.
interface OnMovieItemClickListener : OnItemClickListener {
override val viewHolderType: SmartViewHolderType
get() = MovieViewHolder::class
}
.addViewEventListener(object : OnMovieItemClickListener {
override fun onViewEvent(view: View, viewEventId: ViewEventId, position: Position) {
playMovie()
}
})
Kotlin has no SAM constructor for interface as java so lambda resolving is not working yet.
However you can do lambda call to eg OnItemClickListener
extensions like this:
inline fun onMovieItemClickListener(crossinline viewEvent: (
view: View,
viewEventId: ViewEventId,
position: Position) -> Unit) = object : OnItemClickListener {
override fun onViewEvent(view: View, viewEventId: ViewEventId, position: Position) {
viewEvent(view, viewEventId, position)
}
}
.addViewEventListener(onMovieItemClickListener { view, viewEventId, position -> playMovie() })
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()