Skip to content

Latest commit

 

History

History
353 lines (272 loc) · 11.5 KB

README.md

File metadata and controls

353 lines (272 loc) · 11.5 KB

Adapt

Adapt components, which are called Items, 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 any ViewGroup

*Item in ViewPager2 must have match_parent as width and height
**Spinner supports only a single view type
*** — wait, wat?
****AlertDailog accepts a ListAdapter

gif XML layout-preview

Pros

  • Interchangeable items between RecyclerView, ListView (all children of android.widget.AdapterView) and different ViewGroups (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

Usage

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 Items - 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)

RecyclerView

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.

ListView

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 Items (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 Items 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 Items) 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()

ViewGroup

// 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 Items 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.

Item

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
    }
}

View

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"))

Wrapper

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 view
  • EnabledWrapper - sets isEnabled for the item view
  • FrameWrapper - wraps item view in a FrameLayout, accepts width, height and gravity
  • IdWrapper - changes id of item
  • MarginWrapper - changes margins of item view
  • OnBindWrapper - accepts a callback that could be triggered each time onBind method of original item is called
  • OnClickWrapper - adds an OnClickListener for item view
  • PaddingWrapper - changes paddings of item view