ui-component-adapter
This is a dependency for UI (user interface) adapter components.
Configure Dependency
You can add this module to your project using the following method.
SweetDependency (Recommended)
Add dependency in your project's SweetDependency
configuration file.
libraries:
com.highcapable.betterandroid:
ui-component-adapter:
version: +
Configure dependency in your project's build.gradle.kts
.
implementation(com.highcapable.betterandroid.ui.component.adapter)
Version Catalog
Add dependency in your project's gradle/libs.versions.toml
.
[versions]
betterandroid-ui-component-adapter = "<version>"
[libraries]
betterandroid-ui-component-adapter = { module = "com.highcapable.betterandroid:ui-component-adapter", version.ref = "betterandroid-ui-component-adapter" }
Configure dependency in your project's build.gradle.kts
.
implementation(libs.betterandroid.ui.component.adapter)
Please change <version>
to the version displayed at the top of this document.
Traditional Method
Configure dependency in your project's build.gradle.kts
.
implementation("com.highcapable.betterandroid:ui-component-adapter:<version>")
Please change <version>
to the version displayed at the top of this document.
Function Introduction
You can view the KDoc click here.
Contents of This Section
Can be used to build a BaseAdapter
.
Can be used to build a PagerAdapter
.
Can be used to build a RecyclerView.Adapter
.
Pager mediator for ViewPager
.
Cosmetic of LayoutManager
and ItemDecoration
of RecyclerView
.
LinearHorizontalItemDecoration
Linear horizontal list decoration for RecyclerView
.
Linear vertical list decoration for RecyclerView
.
Grid vertical list decoration for RecyclerView
.
Enhanced linear layout manager for RecyclerView
.
Enhanced grid layout manager for RecyclerView
.
Enhanced layout manager base class for RecyclerView
.
Custom adapter wrapper class for RecyclerView
.
Extension methods for RecyclerView
and its adapter builds.
Extension methods for the adapter build above.
Custom ViewHolder
delegate class.
Adapter position entity.
From the beginning of ListView
to the emergence of RecyclerView
, adapters in Android have always been one of the most troublesome problems for developers.
To address this problem, BetterAndroid
encapsulates the adapters of the following components:
ListView
, AutoCompleteTextView
, ListPopupWindow
, RecyclerView
, ViewPager
, ViewPager2
In Kotlin you can create a data adapter more easily.
Now, all you need is a data array and a custom adapter layout to create an adapter very quickly and bind to these components.
Base Adapter
Create a BaseAdapter
for ListView
, AutoCompleteTextView
, ListPopupWindow
.
The following example
// Assume that's your entity class.
data class MyEntity(
var iconRes: Int,
var name: String
)
// Assume that's the dataset you need to bind.
val listData = ArrayList<MyEntity>()
// Create and bind to a custom BaseAdapter.
val adapter = listView.bindAdapter<MyEntity> {
// Bind the dataset.
onBindData { listData }
// Bind the custom adapter layout adapter_my_layout.xml
onBindItemView<AdapterMyLayoutBinding> { binding, entity, position ->
binding.iconView.setImageResource(entity.iconRes)
binding.textView.text = entity.name
}
// Bind the click event for each item.
onItemViewClick { itemView, entity, position ->
// Your code here.
}
}
If you want to manually create an adapter and bind to the above components, please refer to the following example.
The following example
// Assume that's your current Context.
val context: Context
// Manually create a BaseAdapter.
val adapter = BaseAdapter<MyEntity>(context) {
// The content is the same as above.
}
// Then bind to listView.
listView.adapter = adapter
Create a PagerAdapter
for ViewPager
.
The following example
// Assume that's your entity class.
data class MyEntity(
var iconRes: Int,
var name: String
)
// Assume that's the dataset you need to bind.
val listData = ArrayList<MyEntity>()
// Create and bind to a custom PagerAdapter.
val adapter = viewPager.bindAdapter<MyEntity> {
// Bind the dataset.
onBindData { listData }
// Bind the custom adapter layout adapter_my_layout.xml
onBindPageView<AdapterMyLayoutBinding> { binding, entity, position ->
binding.iconView.setImageResource(entity.iconRes)
binding.textView.text = entity.name
}
}
You can also use dataSetCount
directly to not specify a dataset and only create multiple pages repeatedly.
The following example
// Create and bind to a custom PagerAdapter.
val adapter = viewPager.bindAdapter {
// Manually create two identical pages.
dataSetCount = 2
// Bind the custom adapter layout adapter_my_layout.xml
onBindPageView<AdapterMyLayoutBinding> { binding, _, position ->
// You can determine the position of the current page through position.
}
}
You can also reuse the onBindPageView
method to create multiple different pages, and the page order is determined by the creation order.
The following example
// Create and bind to a custom PagerAdapter.
val adapter = viewPager.bindAdapter {
// Bind the custom adapter layout adapter_my_layout_1.xml
onBindPageView<AdapterMyLayout1Binding> { binding, _, position ->
// You can determine the position of the current page through position.
}
// Bind the custom adapter layout adapter_my_layout_2.xml
onBindPageView<AdapterMyLayout2Binding> { binding, _, position ->
// You can determine the position of the current page through position.
}
}
The number of pages created is the number of times the onBindPageView
method is reused.
Pay Attention
If you reuse the onBindPageView
method to create multiple different pages, you cannot specify dataSetCount
or bind a dataset.
If you need to handle getPageTitle
and getPageWidth
in PagerAdapter
, you can use PagerMediator
to accomplish this.
The following example
// Create and bind to a custom PagerAdapter.
val adapter = viewPager.bindAdapter {
// Bind PagerMediator for each item.
onBindMediators {
// Handle page titles.
title = when (position) {
0 -> "Home"
else -> "Additional Page"
}
// Handle page width (ratio).
width = when (position) {
0 -> 1f
else -> 0.5f
}
}
// ...
}
If you want to manually create a PagerAdapter
and bind it to ViewPager
, please refer to the following example.
The following example
// Assume that's your current Context.
val context: Context
// Manually create a PagerAdapter.
val adapter = PagerAdapter<MyEntity>(context) {
// The content is the same as above.
}
// Then bind to viewPager.
viewPager.adapter = adapter
RecyclerView Adapter
Android Jetpack brings developers a more modern adapter component with richer functionality - RecyclerView.Adapter
.
Create a regular RecyclerView.Adapter
for RecyclerView
and ViewPager2
.
The following example
// Assume that's your entity class.
data class MyEntity(
var iconRes: Int,
var name: String
)
// Assume that's the dataset you need to bind.
val listData = ArrayList<MyEntity>()
// Create and bind to a custom RecyclerView.Adapter.
val adapter = recyclerView.bindAdapter<MyEntity> {
// Bind the dataset.
onBindData { listData }
// Bind the custom adapter layout adapter_my_layout.xml
onBindItemView<AdapterMyLayoutBinding> { binding, entity, position ->
binding.iconView.setImageResource(entity.iconRes)
binding.textView.text = entity.name
}
// Set click event for each item.
onItemViewClick { itemView, viewType, entity, position ->
// Your code here.
}
}
Create a multi-View
type RecyclerView.Adapter
for RecyclerView
and ViewPager2
.
The following example
// Assume that's your entity class.
data class MyEntity(
var iconRes: Int,
var name: String,
var title: String,
var dataType: Int
)
// Assume that's the dataset you need to bind.
val listData = ArrayList<MyEntity>()
// Create and bind to a custom RecyclerView.Adapter.
val adapter = recyclerView.bindAdapter<MyEntity> {
// Bind the dataset.
onBindData { listData }
// Bind View types.
onBindViewType { entity, position -> entity.dataType }
// Bind the custom adapter layout adapter_my_layout_1.xml
onBindItemView<AdapterMyLayout1Binding>(viewType = 1) { binding, entity, position ->
binding.iconView.setImageResource(entity.iconRes)
binding.textView.text = entity.name
}
// Bind the custom adapter layout adapter_my_layout_2.xml
onBindItemView<AdapterMyLayout2Binding>(viewType = 2) { binding, entity, position ->
binding.iconView.setImageResource(entity.iconRes)
binding.titleView.text = entity.title
}
// Set click event for each item.
onItemViewClick { itemView, viewType, entity, position ->
// Your code here.
}
}
Tips
In RecyclerView.Adapter
, the position
type in onBindItemView
is AdapterPosition
instead of Int
as in Base Adapter.
Since RecyclerView.Adapter
can be updated partially, after dynamically adding or removing items, the onBindItemView
of existing items will not be called back again. At this time, you need a dynamic index instance like AdapterPosition
to get the correct index of the current item through position.value
.
AdapterPosition
incorporates the getLayoutPosition
, getBindingAdapterPosition
, getAbsoluteAdapterPosition
methods from RecyclerView.ViewHolder
, which correspond to position.layout
, position.value
and position.absolute
.
Create header View
and footer View
for RecyclerView
.
You can use the onBindHeaderView
and onBindFooterView
methods to add a header View
and footer View
. These are two special item layouts that are not counted in the bound data, and the index position
called back through methods like onBindItemView
is not affected.
Notice
You can only add one header View
and one footer View
at the same time, and these added layouts do not support dynamic removal.
The following example
// Assume that's your entity class.
data class MyEntity(
var iconRes: Int,
var name: String
)
// Assume that's the dataset you need to bind.
val listData = ArrayList<MyEntity>()
// Create and bind to a custom RecyclerView.Adapter.
val adapter = recyclerView.bindAdapter<MyEntity> {
// Bind the dataset.
onBindData { listData }
// Bind header View.
onBindHeaderView<AdapterHeaderBinding> { binding ->
binding.someText.text = "Header"
}
// Bind footer View.
onBindFooterView<AdapterFooterBinding> { binding ->
binding.someText.text = "Footer"
}
// Bind the custom adapter layout adapter_my_layout.xml
onBindItemView<AdapterMyLayoutBinding> { binding, entity, position ->
binding.iconView.setImageResource(entity.iconRes)
binding.textView.text = entity.name
}
}
In addition to using ViewBinding
as shown in the above example, you can also use traditional layout resource IDs to bind them to adapter layouts.
The following example
// Bind the custom adapter layout adapter_my_layout.xml
onBindItemView(R.layout.adapter_my_layout) { itemView, entity, position ->
itemView.findViewById<ImageView>(R.id.icon_view).setImageResource(entity.iconRes)
itemView.findViewById<TextView>(R.id.text_view).text = entity.name
}
If all layout loading methods do not meet your needs, you can also create a custom ViewHolder
delegate class based on ViewHolderDelegate
.
The following example
// Create a delegate class to implement your own layout loading scheme.
// Here we assume that MyLayoutBinder is your layout loader.
class MyViewHolderDelegate(@LayoutRes private val resId: Int) : ViewHolderDelegate<MyLayoutBinder>() {
override fun create(context: Context, parent: ViewGroup?): MyLayoutBinder {
// Assume this is how your custom layout loader works.
// Remember to pass in and implement the parent parameter,
// because we need the parent's LayoutParams.
// Note: Don't bind to parent now! The adapter does not allow
// child layouts to hold parent layouts in advance.
val binder = MyLayoutBinder.inflate(context, resId, parent, attachToParent = false)
return binder
}
override fun getView(instance: MyLayoutBinder): View {
// Get the required View from your layout loader.
return instance.root
}
}
Then, use your custom ViewHolderDelegate
.
The following example
// Bind your custom ViewHolderDelegate.
onBindItemView(MyViewHolderDelegate(R.layout.adapter_my_layout)) { delegate, entity, position ->
// Here delegate is the MyLayoutBinder object,
// assuming the following methods are all implemented by yourself.
delegate.get<ImageView>(R.id.icon_view).setImageResource(entity.iconRes)
delegate.get<TextView>(R.id.text_view).text = entity.name
}
Notice
When you set header or footer View
, when using RecyclerView.Adapter
's notifyItemInserted
, notifyItemRemoved
, notifyItemChanged
, notifyItemMoved
and other methods, there will be issues with index positions, because by default the position
calculated by onBindItemView
will not include header and footer layouts, and methods like RecyclerView.scrollToPosition
, RecyclerView.smoothScrollToPosition
will also be affected.
Since these methods are all final
in RecyclerView.Adapter
and cannot be overridden, in this case, BetterAndroid
provides you with a solution. When using RecyclerView.Adapter
, you can call the wrapper
method to get a wrapper instance, which will automatically handle these issues for you.
The following example
// Assume you have bound the adapter created using RecyclerAdapterBuilder to RecyclerView.
val recyclerView: RecyclerView
// Get the wrapper instance. If the target adapter is not
// created by RecyclerAdapterBuilder, it will return null.
val wrapper = recyclerView.adapter?.wrapper
// Normally use RecyclerView.Adapter's notification update methods.
wrapper?.notifyItemInserted(0)
wrapper?.notifyItemRemoved(0)
// Header or footer layouts need to be updated separately using the following methods.
wrapper?.notifyHeaderItemChanged()
wrapper?.notifyFooterItemChanged()
// Furthermore, you can manually use the following methods to
// determine whether header and footer layouts exist.
val hasHeaderView = wrapper?.hasHeaderView == true
val hasFooterView = wrapper?.hasFooterView == true
Going back to the issue we mentioned earlier, methods like RecyclerView.scrollToPosition
, RecyclerView.smoothScrollToPosition
will also be affected. In this case, you can use the LinearLayoutManager
, GridLayoutManager
and RecyclerLayoutManager
provided under the com.highcapable.betterandroid.ui.component.adapter.recycler.layoutmanager
package to solve this.
These encapsulated enhanced layout managers will be automatically integrated through the default RecyclerCosmetic
(refer to Recycler Cosmetic below). You don't need any manual operations. When you need to manually create RecyclerView.LayoutManager
, we recommend that you inherit from the instances provided in this package.
When you use the RecyclerView.LayoutManager
provided by BetterAndroid
, since header or footer layouts will automatically handle position
, when using RecyclerView.scrollToPosition
, RecyclerView.smoothScrollToPosition
to scroll to the top and bottom, you need to use scrollToPosition(-1)
(top) or scrollToPosition(lastIndex + 1)
(bottom).
Therefore, we always recommend that when you have the need to scroll to the top and bottom, use the scrollToFirstPosition
, scrollToLastPosition
, smoothScrollToFirstPosition
, smoothScrollToLastPosition
methods instead. They will automatically handle such issues (regardless of whether you use the RecyclerView.LayoutManager
provided by BetterAndroid
).
Fragment Adapter
Create a FragmentPagerAdapter
for ViewPager
.
Notice
This usage has been deprecated by the official team. If possible, please start using ViewPager2
.
The following example
// Assume that's your current FragmentActivity.
val activity: FragmentActivity
// Create and bind to a custom FragmentPagerAdapter.
val adapter = viewPager.bindFragments(activity) {
// Set the number of Fragments to display.
pageCount = 5
// Bind each Fragment.
onBindFragments { position ->
when (position) {
0 -> FirstFragment()
1 -> SecondFragment()
2 -> ThirdFragment()
3 -> FourthFragment()
else -> FifthFragment()
}
}
}
Same as the constructor method usage of FragmentPagerAdapter
, you can also customize the behavior
parameter.
If you are using it in a Fragment
, you can fill in the current Fragment
instance in the first parameter of bindFragments
, and it will automatically bind to getChildFragmentManager()
.
If you want to manually create a FragmentPagerAdapter
and bind it to ViewPager
, please refer to the following example.
The following example
// Assume that's your current FragmentActivity.
val activity: FragmentActivity
// Manually create a FragmentPagerAdapter.
val adapter = FragmentPagerAdapter(activity) {
// The content is the same as above.
}
// Then bind to viewPager.
viewPager.adapter = adapter
Create a FragmentStateAdapter
for ViewPager2
.
The following example
// Assume that's your current FragmentActivity.
val activity: FragmentActivity
// Create and bind to a custom FragmentPagerAdapter.
val adapter = viewPager2.bindFragments(activity) {
// Set the number of Fragments to display.
pageCount = 5
// Bind each Fragment.
onBindFragments { position ->
when (position) {
0 -> FirstFragment()
1 -> SecondFragment()
2 -> ThirdFragment()
3 -> FourthFragment()
else -> FifthFragment()
}
}
}
If you are using it in a Fragment
, you can fill in the current Fragment
instance in the first parameter of bindFragments
, and it will automatically bind to getChildFragmentManager()
.
If you want to manually create a FragmentPagerAdapter
and bind it to ViewPager2
, please refer to the following example.
The following example
// Assume that's your current FragmentActivity.
val activity: FragmentActivity
// Manually create a FragmentStateAdapter.
val adapter = FragmentStateAdapter(activity) {
// The content is the same as above.
}
// Then bind to viewPager2.
viewPager2.adapter = adapter
Recycler Cosmetic
If you want to manually create a RecyclerView.Adapter
and bind it to RecyclerView
and ViewPager2
, please refer to the following example.
The following example
// Assume that's your current Context.
val context: Context
// Manually create a RecyclerView.Adapter.
val adapter = RecyclerAdapter<CustomBean>(context) {
// The content is the same as above.
}
// Manually create a decorator.
val cosmetic = RecyclerCosmetic.fromLinearVertical(context)
// Then bind to recyclerView.
recyclerView.layoutManager = cosmetic.layoutManager
recyclerView.addItemDecoration(cosmetic.itemDecoration)
recyclerView.adapter = adapter
// When binding to viewPager2, you don't need to set layoutManager.
viewPager2.addItemDecoration(cosmetic.itemDecoration)
viewPager2.adapter = adapter
BetterAndroid
provides developers with several common adapter layout types for RecyclerView
for your use.
You can specify a RecyclerCosmetic
in the method parameters, which defaults to a linear vertical list cosmetic.
The following example
// Create a linear vertical list with row spacing of 10dp.
val lvCosmetic = RecyclerCosmetic.fromLinearVertical(context, 10.toPx(context))
// Create a grid vertical list with column spacing of 10dp and row spacing of 10dp.
val gvCosmetic = RecyclerCosmetic.fromGridVertical(context, 10.toPx(context), 10.toPx(context))
// Taking lvCosmetic as an example.
// Use bindAdapter to bind to recyclerView.
recyclerView.bindAdapter<MyEntity>(lvCosmetic) {
// ...
}
// Or, manually bind.
val adapter = RecyclerAdapter<MyEntity>(context) {
// ...
}
recyclerView.layoutManager = lvCosmetic.layoutManager
recyclerView.addItemDecoration(lvCosmetic.itemDecoration)
recyclerView.adapter = adapter
Tips
If you only need an ItemDecoration
, you can create one through the preset LinearHorizontalItemDecoration
, LinearVerticalItemDecoration
, GridVerticalItemDecoration
.
Here's a simple example.
The following example
// Create a linear vertical ItemDecoration with row spacing of 10dp.
val itemDecoration = LinearVerticalItemDecoration(rowSpacing = 10.toPx(context))
// Set to recyclerView.
recyclerView.addItemDecoration(itemDecoration)
// If you need to update ItemDecoration parameters, you can use the update method.
itemDecoration.update(rowSpacing = 15.toPx(context))
// Then notify recyclerView to update.
recyclerView.invalidateItemDecorations()
Adapter Extensions
Contents of This Section
RecyclerAdapter → notifyAllItemsInserted
RecyclerAdapter → notifyAllItemsChanged
RecyclerAdapter → clearAndNotify
RecyclerAdapter → notifyDataSetChangedIgnore
Extensions for RecyclerView
adapter.
The adapter extensions introduced in this section are mostly extensions of RecyclerView.Adapter
.
Usually, we need to use methods like notifyItemInserted
, notifyItemChanged
, etc. to notify the adapter that the data has changed.
When we add data to the dataset all at once, we usually need to use notifyItemRangeInserted
to notify the adapter that the data has changed.
The following example
// Assume this is your RecyclerView.Adapter.
val adapter: RecyclerView.Adapter<*>
// Assume this is your dataset, initially empty.
val dataSet: MutableList<MyEntity>
// Add some data to the dataset.
dataSet.addAll(...)
// Notify the adapter that the data has changed.
adapter.notifyItemRangeInserted(0, dataSet.size)
When the data is confirmed to be added from 0, BetterAndroid
provides you with a simpler way to complete this. Now you can use the following method to notify the adapter that the data has changed.
The following example
// Assume this is your RecyclerView.Adapter.
val adapter: RecyclerView.Adapter<*>
// Assume this is your dataset, initially empty.
val dataSet: MutableList<MyEntity>
// Add some data to the dataset.
dataSet.addAll(...)
// Notify the adapter that the data has changed.
adapter.notifyAllItemsInserted()
The above method will use adapter.itemCount
by default to get the size of the dataset, no need to manually specify the range to be updated.
At this time, please ensure that your adapter returns the correct itemCount
, otherwise, please manually pass in dataSet
.
The following example
// Notify the adapter that the data has changed.
adapter.notifyAllItemsInserted(dataSet)
Similarly, when the data is confirmed to have all changed (for example, in a multi-select state list, updating the selected and unselected checkbox states), you can use the following method to notify the adapter that the data has changed.
The following example
// Assume this is your RecyclerView.Adapter.
val adapter: RecyclerView.Adapter<*>
// Assume this is your dataset.
val dataSet: MutableList<MyEntity>
// Simulate operating the dataset (e.g., select all action).
dataSet.forEach { it.isSelected = true }
// Notify the adapter that the data has changed.
adapter.notifyAllItemsChanged()
Similarly, please ensure that your adapter returns the correct itemCount
, otherwise, please manually pass in dataSet
.
The following example
// Notify the adapter that the data has changed.
adapter.notifyAllItemsChanged(dataSet)
When we need to clear the dataset and notify the adapter that the data has changed, we usually need to use notifyItemRangeRemoved
to notify the adapter that the data has changed.
The following example
// Assume this is your RecyclerView.Adapter.
val adapter: RecyclerView.Adapter<*>
// Assume this is your dataset.
val dataSet: MutableList<MyEntity>
// Save the current data size.
val count = dataSet.size
// Clear the dataset.
dataSet.clear()
// Notify the adapter that the data has changed.
adapter.notifyItemRangeRemoved(0, count)
This process is still cumbersome, BetterAndroid
provides a simpler way for this. Now you can use the following method to clear the dataset and notify the adapter that the data has changed. This method will automatically calculate the size of the dataset.
The following example
// Assume this is your RecyclerView.Adapter.
val adapter: RecyclerView.Adapter<*>
// Assume this is your dataset.
val dataSet: MutableList<MyEntity>
// Clear the dataset and notify the adapter that the data has changed.
adapter.clearAndNotify(dataSet)
Tips
There are also some other extensions that can be used. notifyDataSetChangedIgnore
will ignore the Lint warnings given during coding and directly provide you with the use of notifyDataSetChanged
.
However, this method is still not recommended because it will cause the entire list to refresh, which will cause performance issues in large datasets.