Adapt components, which are called Item
s,
can be displayed without modification in those parent widgets:
RecyclerView
ViewPager2
*
AdapterView
ListView
Spinner
**GridView
StackView
AdapterViewFlipper
***AlertDialog
****
ViewGroup
LinearLayout
- there are no restrictions on actual
ViewGroup
, it can be anyViewGroup
* — Item
in ViewPager2
must have match_parent
as width and height
** — Spinner
supports only a single view type
*** — wait, wat?
**** — AlertDailog
accepts a ListAdapter
- Interchangeable items between
RecyclerView
,ListView
(all children ofandroid.widget.AdapterView
) and differentViewGroup
s (same item is used without modification) - No prior usage registration - any instance of an
Item
can be displayed right away - Render individual item as a regular Android widget view (via
AdaptView
) - Modular design is enforced, leading to re-usable view components
- Ability to preview item in Layout Preview (by using
AdaptViewGroup
), display available design components without launching on a device without leaving IDE
class PageIndicatorItem(
val title: String,
var selected: Boolean,
val onClick: (PageIndicatorItem) -> Unit
) : ItemLayout(hash(title), R.layout.item_page_indicator) {
override fun bind(holder: CachedHolder) {
// obtain required view (cached internally by the holder)
val titleView: TextView = holder.requireView(R.id.title)
// bind data
titleView.text = title
holder.itemView().also {
it.setOnClickListener { onClick(this) }
it.activate(selected)
}
}
}
Apart from XML layout this is the complete Item
that can be used with Adapt. Each item
must have an id
set (the first argument to constructor). Here Item.hash(vararg any: Any)
is used,
which is a convenience method call to Objects.hash
. ItemLayout
uses special Holder
that
caches views queried, so it is safe to requireView
with each bind
call.
val container: ViewGroup = view.findViewById(R.id.container)
val adapt = AdaptViewGroup.init(container)
// click handler
fun onClick(item: PageIndicatorItem) {/*...*/}
// create a list of Items
val items: List<Item<*>> = listOf(
PageIndicatorItem("Page 1", false, ::onClick),
PageIndicatorItem("Page 2", false, ::onClick),
PageIndicatorItem("Page 3", false, ::onClick)
)
adapt.setItems(items)
Adapt
accepts List<Item<*>>
so your list can contain any other Item
s - you don't need
to register them beforehand. By changing 2 lines we can display the same list in a RecyclerView
:
val recyclerView: RecyclerView = obtainRecyclerView()
val adapt = AdaptRecyclerView.init(recyclerView)
or a ListView
:
val listView: ListView = view.findViewById(R.id.list_view)
val adapt = AdaptListView.init(listView)
val adapt = AdaptRecyclerView.init(recyclerView) {
// data set change handler that takes care of updating underlying list of items
// optional, by default NotifyDataSetChangedHandler
it.dataSetChangeHandler(DiffUtilDataSetChangedHandler.create())
// optional, by default true
it.hasStableIds(false)
}
Additionally there is also create
factory method that creates AdaptRecyclerView
instance
without actual RecyclerView
- for example to be used with ViewPager2
:
// additionally can also specify `dataSetChangedHandler`
// and `hasStableIds` if supplied configurator lambda
val adapt = AdaptRecyclerView.create()
val viewPager2: ViewPager2 = view.findViewById(R.id.view_pager2)
viewPager2.adapter = adapt.adapter()
There is also StickyItemDecoration
that allows creating sticky items (aka section items).
Refer to the sample application for sample usage.
val adapt = AdaptListView.init(listView) {
// value that is returned from `Adapter.hasStableIds`
// optional, by default true
it.hasStableIds(false)
// indicates if all items are enabled, in ListView's language if all items should be
// clickable (delivered by ListView.OnItemClickListener) and have a divider after them
// by default false, all disabled unless specified further
// by enabling individual items (see below)
it.areAllItemsEnabled(true);
// includes specified Item, `isEnabled = false`
it.include(Item::class.java)
// includes specified Item and isEnabled
it.include(Item::class.java, true)
}
Registration of Item
s (via include
calls) is optional in most of the cases. Still, if you are planning to use
only android.widget.Adapter
(or any of its siblings like ListAdapter
) without holding an AdapterView
instance,
for example in an AlertDialog
and there are multiple item views (multiple types of Item
s
will be displayed) then all displayed items must be included explicitly.
This registration can also be a good optimization, because internally AdaptListView
will
rebuild internal ListView
scrap cache when new item views (types of Item
s) are encountered.
val adapt = AdaptListView.create(view.context) {
// all items must be explicitly registered
// if there is only one item, then it is not required
it.include(CardBigItem::class.java)
it.include(CardItem::class.java)
it.include(ControlItem::class.java)
it.include(PlainItem::class.java)
}
// from sample application
adapt.setItems(/*...*/)
AlertDialog.Builder(view.context)
.setAdapter(adapt.adapter()) { _, position ->
// item at position is clicked
}
.show()
// LinearLayout is the most obvious ViewGroup for this
// but it can be any other suitable ViewGroup as well
val container: LinearLayout = view.findViewById(R.id.container)
val adapt = AdaptViewGroup.init(container) {
// optional
// in this case all changes are going to be animated by default Transition
it.changeHandler(TransitionChangeHandler.create())
// optional, used to create Item views
it.layoutInflater(LayoutInflater.from(context))
// optional, in case a custom diffing algorithm is required
it.adaptViewGroupDiff(AdaptViewGroupDiff.create())
}
NB! A list of Item
s supplied to AdaptViewGroup
must have unique ids for the same
type (multiple items of the same type cannot have duplicate ids). Item.NO_ID
can be used, but this would
result in Item
's view being created each time anew. So, if possible, consider having unique ids even
for supplementary items.
The core Item
class (which ItemLayout
subclasses) is Item
:
class SectionItem(val text: String) :
Item<SectionItem.Holder>(hash(SectionItem::class, text)) {
// this holder does not cache views returned by `requireView` and `findView`
class Holder(view: View) : Item.Holder(view) {
val textView: TextView = requireView(R.id.text)
}
override fun createHolder(inflater: LayoutInflater, parent: ViewGroup): Holder {
return Holder(inflater.inflate(R.layout.item_section, parent, false))
}
override fun bind(holder: Holder) {
holder.textView.text = text
}
}
AdaptView
can be used to display a single Item
in your current layout.
val container: ViewGroup = findViewById(R.id.view_group)
val adaptView: AdaptView = AdaptView.init(container)
adaptView.setItem(SectionItem("Section 1"))
Sometimes an Item
needs a minor modification depending on layout, like displaying a divider
or having specific background. To achieve that in a composable way (prefer composition over modification)
an ItemWrapper
can be used:
class PaddingWrapper(val padding: Int, item: Item<*>) : ItemWrapper(item) {
override fun bind(holder: Holder) {
super.bind(holder)
holder.itemView()
.setPadding(padding, padding, padding, padding)
}
}
Important thing to note here - if ItemWrapper
creates a modification based on a variable
then it should apply its modification in bind(Holder)
method. For example if your list
contains PaddingWrapper(12), PaddingWrapper(4)
(actual padding
variable is different),
then bind(Holder)
must be used. If, on the other hand, PaddingWrapper
would always apply the same
value (some constant value), then createHolder(LayoutInflater,ViewGroup)
can be used instead.
ItemWrapper
allows wrapping other ItemWrapper
, for example:
class MarginWrapper(val margin: Int, item: Item<*>) : ItemWrapper(item) {
override fun bind(holder: Holder) {
super.bind(holder)
val lp = holder.itemView().layoutParams as ViewGroup.MarginLayoutParams
lp.setMargins(margin, margin, margin, margin)
holder.itemView().layoutParams = lp
}
}
val mp = MarginWrapper(12, PaddingWrapper(24, TextItem("Margin / Padding")))
val pm = PaddingWrapper(8, MarginWrapper(100, TextItem("Padding / Margin")))
assert(mp.viewType() != pm.viewType())
Each ItemWrapper
changes viewType
of resulting Item
. This functionality is
encapsulated by the Item.Key
class:
val key = Item.Key.builder(TextItem::class.java)
.wrapped(MarginWrapper::class.java)
.wrapped(PaddingWrapper::class.java)
.build()
Item.Key
should be used when an explicit item registration is required, for example
when used with the AlertDialog
(or item in ListView
should be enabled):
val adapt = AdaptListView.create(context) {
// simple TextItem
it.include(TextItem::class.java)
// TextItem wrapped in `PaddingWrapper`
val pt = Item.Key.builder(TextItem::class.java)
.wrapped(PaddingWrapper::class.java)
.build()
it.include(pt)
// TextItem wrapped in `MarginWrapper`
val mt = Item.Key.builder(TextItem::class.java)
.wrapped(MarginWrapper::class.java)
.build()
it.include(mt)
}
val items = listOf(
TextItem("Text"),
PaddingWrapper(12, TextItem("Padding / Text")),
MarginWrapper(96, TextItem("Margin / Text"))
)
adapt.setItems(items)
AlertDialog.Builder(context)
.setAdapter(adapt.adapter()) { _, _ ->
}
.show()
Please note that explicit registration is required in only some cases of ListView/AdapterView
.
// since 4.0.0 all items have special `wrap` method
val item = TextItem("This is text")
.wrap(BackgroundWrapper.init(0xFFff0000))
// instead of
val item = BackgroundWrapper(0xFFff0000, TextItem("This is text"))
// which can be turned into an extension function:
fun Item<*>.background(color: Int): Item<*> = wrap(BackgroundWrapper.init(color))
// an used:
val item = TextItem("this is text")
.background(0xFFff00ff)
There is a number of different wrappers distributed along with the library:
BackgroundWrapper
- modifies background of item viewEnabledWrapper
- setsisEnabled
for the item viewFrameWrapper
- wraps item view in a FrameLayout, accepts width, height and gravityIdWrapper
- changes id of itemMarginWrapper
- changes margins of item viewOnBindWrapper
- accepts a callback that could be triggered each timeonBind
method of original item is calledOnClickWrapper
- adds anOnClickListener
for item viewPaddingWrapper
- changes paddings of item view