Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up

RecyclerView-driven declarative UIs

License

NotificationsYou must be signed in to change notification settings

gotev/recycler-adapter

Repository files navigation

RecyclerView-driven declarative UIs.

Using stock Android View system and Recycler Adapter, you can already write UIs similar toJetPack Compose and use it in production.

render {+Items.leaveBehind("swipe to left to leave behind","option")    (0..random.nextInt(200)+50).map { number->if (randomNumber%2==0)+Items.Card.titleSubtitle("Item$number","subtitle$number")else+Items.Card.labelWithToggle("Toggle$number")    }}

Every time you callrender, RecyclerAdapter takes care of updating the RecyclerView and make the needed diffings to reflect the new status. Underneath it usesDiffUtil for maximum performance and efficiency.

Schermata 2021-05-08 alle 07 39 59

StandardRecyclerView.Adapter is tedious to work with, because you have to write repetitive boilerplate and spaghetti code and to concentrate all your items view logic and binding into the adapter itself, which is really bad. This library was born to be able to have the following for each element in a recycler view:

  • amodel, which is a simpledata class
  • a programmaticView or an XML layout file, in which to define the item's view hierarchy
  • aview model file (calledAdapterItem), in which to specify the binding between the model and the view and in which to handle user interactions with the item.

In this way every item of the recycler view has its own set of files, resulting in a cleaner and easier to maintain code base.

Examples

Before diving into some details, it's worth mentioning you can download and try those example apps which are using the library:

Index

Setup

In your gradle dependencies add:

def recyclerAdapterVersion="x.y.z"// change it with the version you want to useimplementation"net.gotev:recycleradapter:$recyclerAdapterVersion"implementation"net.gotev:recycleradapter-extensions:$recyclerAdapterVersion"

This is the latest version:Maven Central

Basic usage tutorial

1. Declare the RecyclerView

In your layout resource file or where you want theRecyclerView (e.g.activity_main.xml) add the following:

<androidx.recyclerview.widget.RecyclerViewandroid:id="@+id/recycler_view"android:scrollbars="vertical"android:layout_width="match_parent"android:layout_height="wrap_content" />

2. Create your item layout

Create your item layout (e.g.item_example.xml). For example:

<?xml version="1.0" encoding="utf-8"?><TextViewxmlns:android="http://schemas.android.com/apk/res/android"android:gravity="center_vertical"android:layout_width="wrap_content"android:layout_margin="8dp"android:layout_height="48dp"android:textSize="18sp" />

3. Create the item

openclassExampleItem(privatevalcontext:Context, privatevaltext:String):AdapterItem<ExampleItem.Holder>(text) {// Variant using XML inflationoverridefungetView(parent:ViewGroup):View= parent.inflating(R.layout.item_example)// Variant using code only/*    override fun getView(parent: ViewGroup): View = TextView(parent.context).apply {        layoutParams = ViewGroup.MarginLayoutParams(parent.layoutParams).apply {            width = WRAP_CONTENT            height = 48.dp(context)            gravity = CENTER_VERTICAL            val margin = 8.dp(context)            leftMargin = margin            rightMargin = margin            topMargin = margin            bottomMargin = margin        }        setTextSize(TypedValue.COMPLEX_UNIT_SP, 18f)    }*/overridefunbind(firstTime:Boolean,holder:ExampleItem.Holder) {// you can use firstTime to discriminate between bindings you// need only the first time the item is binded from the others        holder.titleField.text= text    }classHolder(itemView:View):RecyclerAdapterViewHolder(itemView),LayoutContainer {overrideval containerView:View?            get()= itemViewinternalval titleField:TextView by lazy { title }overridefunprepareForReuse() {// Here you can perform operations to clear data from the holder// and free used resources, like bitmaps or other heavy weight// things        }    }}

LayoutContainer is from Kotlin Android Extensions and is not mandatory, but it prevents memory leaks.Read the article linked here

4. Instantiate RecyclerView and add items

In your Activity (onCreate method) or Fragment (onCreateView method):

val recyclerAdapter=RecyclerAdapter()recycler_view.apply {// recycler_view is the id of your Recycler View in the layout    layoutManager=LinearLayoutManager(context,RecyclerView.VERTICAL,false)    adapter= recyclerAdapter}//add itemsrecyclerAdapter.add(ExampleItem("test"))

Diffing Strategy

Prior to 2.9.0, you had to implementdiffingId method yourself. Starting from 2.9.0, all you need to do is to pass your model, which can be a primitive type or a complex data class.

This model, combined with your item's class name, is used to retrieve a diffingId to identify every single instance of your items uniquely.

// Exampledata classYourModel(valtext1:String,valtext2:String)openclassYourItem(privatevalcontext:Context, privatevalyourModel:YourModel):AdapterItem<ExampleItem.Holder>(yourModel) {// In this case YourItem diffing id will be `YourItem.javaClass.name + yourModel.hashCode()`...}

This means that in most cases this is what you are looking for and what you need in your project.

N.B: If you are migrating from previous version of the library, you have to do a little refactor by following this simple steps:

  1. Provide a model instance to your items constructors
  2. Remove diffingId() overrides if it's a combination of solely javaClass.name and model properties values (even if its a subset of model properties)
  3. Remove hasToBeReplacedBy() implementations if it consist of elementary diffing of old model properties values with the new ones.

Stable IDs

Starting from 2.4.2,RecyclerAdapter has stable IDs out of the box. If you want to know more about what they are:

Adding different kind of items

You can have more than one kind of item in yourRecyclerView. Just implement a differentAdapterItem for every type you want to support, and then just add it into the adapter:

recyclerAdapter.add(ExampleItem("example item"))recyclerAdapter.add(TextWithButtonItem("text with button"))

Checkout the example app provided to get a real example in action.

Carousels and nested RecyclerViews

When more complex layouts are needed in a recycler view, you have two choices:

  • use a combination of existing layout managers and nest recycler views
  • create a custom layout manager

Since the second strategy is really hard to implement and maintain, also due to lack of documentation and concrete working examples without huge memory leaks or crashes, in my experience resorting to the first strategy has always paid off both in terms of simplicity and maintainability.

One of the most common type of nested RecyclerViews are Carousels, like those you can find in Google Play Store. How to achieve that? First of all, includerecycleradapter-extensions in your gradle:

implementation"net.gotev:recycleradapter-extensions:$recyclerAdapterVersion"

The concept is really simple. You want to have a whole recycler view inside a singleAdapterItem. To make things modular and to not reinvent the wheel, you want to be able to use aRecyclerAdapter in this nestedRecyclerView. Please welcomeNestedRecyclerAdapterItem which eases things for you. Override it to implement your custom nested recycler views. You can find a complete example inCarousels Activity together with a customCarouselItem

Since having nested recycler views consumes a lot of memory and you may experience lags in your app, it's recommended to share a singleRecycledViewPool across all your root and nestedRecyclerViews. In that way all theRecyclerViews will use a single recycled pool like there's only oneRecyclerView. You can see the performance difference by running the demo app on a low end device and trying Carousels both with pool and without pool.

Paged Lists

Starting from2.6.0 onwards, RecyclerAdapter integrates with Android JetPack'sPaging Library which allows you to have maximum performance when dealing with very long lists loaded from network, database or both.

Add this to your dependencies:

implementation"net.gotev:recycleradapter-paging:$recyclerAdapterVersion"

It's strongly advised to study Google's Paging Library first so you can better understand how everything works and the motivation behind it.Check this codelab which is great to learn. When you are ready, check the demo provided inPagingActivity.

The paging module aims to provide an essential and thin layer on top of Google'sPaging Library, to allow you to benefit the RecyclerAdapter abstractions and reuse all your existing Adapter items.PagingAdapter does not have all the features of the standardRecyclerAdapter on purpose, becausePagingAdapter doesn't have the entire list in memory and it's intended to be used for different use cases.

Empty item in Paged Lists

If you want to add an Empty Item when RecyclerView is empty also when you're using thePagingAdapter extension, you have to implement a newItem (like described before) then, in yourDataSourceLoadInitial method, use thewithEmptyItem enxtension for your callback instead ofonResult:

callback.withEmptyItem(emptyItem,response.results)

Filter items

If you need to search items in your recycler view, you have to overrideonFilter method in each one of your items implementation. Let's say ourAdapterItem has atext field and we want to check if the search term matches it:

/** * Gets called for every item when the [RecyclerAdapter.filter] method gets called. * @param searchTerm term to search for * @return true if the items matches the search term, false otherwise*/openfunonFilter(searchTerm:String):Boolean {return text.contains(searchTerm)}

To filter the recycler view, call:

recyclerAdapter.filter("search item")

and only the items which matches the search term will be shown. To reset the search filter, passnull or an empty string.

Sort items

To sort items, you have the following possible approaches. Be sure to have includedrecycleradapter-extensions in your project.

1. ImplementcompareTo and callsort on theRecyclerAdapter

This is the recommended approach if you have to sort all your items by a single criteria and you have a list with only one type ofItem. CheckcompareTo JavaDoc reference for further information. In yourAdapterItem implement:

overridefuncompareTo(other:AdapterItem<*>):Int {if (other.javaClass!= javaClass)return-1val item= otherasSyncItemif (id== item.id)return0returnif (id> item.id)1else-1}

Then call:

// ascending orderrecyclerAdapter.modifyItemsAndRender { it.sorted() }// descending orderrecyclerAdapter.modifyItemsAndRender { it.sortedDescending() }

You can see an example in action by looking at the code in theSyncActivity andSyncItem of the demo app.

2. Provide a custom comparator implementation

Your items doesn't necessarily have to implementcompareTo for sorting purposes, as you can provide also the sorting implementation outside of them, like this:

recyclerAdapter.modifyItemsAndRender { items->    items.sortedWith { itemA, itemB->// compare itemA and itemB and return -1, 0 or 1 (standard Java and Kotlin Comparator)    }}

This is the recommended approach if you want to be able to sort your items by many different criteria, as you can simply pass theComparator implementation of the sort type you want.

3. Combining the two techniques

You can also combine the two techniques described above. This is the recommended approach if you have a list with different kind of items, and you want to perform different kind of grouping between items of different kind, maintaining the same sorting strategy for elements of the same type. You can implementcompareTo in everyone of your items, to sort the items of the same kind, and a customComparable which will handle comparison between diffent kinds of items, like this:

recyclerAdapter.modifyItemsAndRender { items->    items.sortedWith { itemA, itemB->// handle ordering of items of the same type with their// internal compareTo implementationif (itemA.javaClass==RobotItem::class.java&& itemB.javaClass==RobotItem::class.java) {val first= itemAasRobotItemval second= itemBasRobotItemreturn first.compareTo(second)        }if (itemA.javaClass==PersonItem::class.java&& itemB.javaClass==PersonItem::class.java) {val first= itemAasPersonItemval second= itemBasPersonItemreturn first.compareTo(second)        }// in this case, we want to put all the PersonItems// before the RobotItems in our listreturnif (itemA.javaClass==PersonItem::class.java&& itemB.javaClass==RobotItem::class.java) {-1        }else0    }}

Using ButterKnife

You can safely useButterKnife in your ViewHolders, however Kotlin Android Extensions are more widely used and recommended.

Using Kotlin Android Extensions

WARNING! JetBrains officially deprecated Kotlin Android Extensions starting from Kotlin 1.4.20:https://youtrack.jetbrains.com/issue/KT-42121

If you use Kotlin in your project, you can also use Kotlin Android Extensions to bind your views in ViewHolder, but be careful to not fall in a common pitfall, explained very well here:https://proandroiddev.com/kotlin-android-extensions-using-view-binding-the-right-way-707cd0c9e648

Reorder items with drag & drop

To be able to change the items order with drag & drop, be sure to have importedrecycleradapter-extensions in your projectand just add this line:

recyclerAdapter.enableDragDrop(recyclerView)

Java users have to write:RecyclerViewExtensionsKt.enableDragDrop(recyclerAdapter, recyclerView);

Handle clicks

One of the things which you may need is to set one or more click listeners to every item. How do you do that? Let's see an example.

item_example.xml:

<?xml version="1.0" encoding="utf-8"?><LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_margin="8dp">    <TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:id="@+id/title" />    <TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:textColor="@android:color/secondary_text_dark"android:id="@+id/subtitle" /></LinearLayout>

ExampleItem.kt:

openclassExampleItem(privatevalcontext:Context, privatevaltext:String):AdapterItem<ExampleItem.Holder>(text) {overridefungetView(parent:ViewGroup):View= parent.inflating(R.layout.item_example)overridefunonFilter(searchTerm:String)= text.contains(searchTerm)overridefunbind(firstTime:Boolean,holder:Holder) {        holder.titleField.text= text        holder.subtitleField.text="subtitle"    }privatefunonTitleClicked(position:Int) {        showToast("clicked TITLE at position$position")    }privatefunonSubTitleClicked(position:Int) {        showToast("clicked SUBTITLE at position$position")    }privatefunshowToast(message:String) {Toast.makeText(context, message,Toast.LENGTH_SHORT).show()    }classHolder(itemView:View):RecyclerAdapterViewHolder(itemView),LayoutContainer {overrideval containerView:View?            get()= itemViewinternalval titleField:TextView by lazy { title }internalval subtitleField:TextView by lazy { subtitle }init {            titleField.setOnClickListener {                withAdapterItem<ExampleItem> {                    onTitleClicked(adapterPosition)                }            }            subtitleField.setOnClickListener {                withAdapterItem<ExampleItem> {                    onSubTitleClicked(adapterPosition)                }            }        }    }}

As you can see, to handle click events on a view, you have to create a click listener in the ViewHolder and propagate an event to theAdapterItem:

titleField.setOnClickListener {    withAdapterItem<ExampleItem> {        onTitleClicked(adapterPosition)    }}

You can call any method defined in yourAdapterItem and pass whatever parameters you want. It's important that you honor nullability, as each ViewHolder has a weak reference to itsAdapterItem, so to prevent crashes at runtime always use the form:

withAdapterItem<ExampleItem> {// methods to call on the adapter item}

In this case, the following method has been implemented to handle title clicks:

privatefunonTitleClicked(position:Int) {    showToast("clicked TITLE at position$position")}

Look at theevent lifecycle to have a complete understaning.

Handle item status and save changes into the model

It's possible to also change the status of the model associated to an item directly from the ViewHolder. Imagine we need to persist a toggle button status when the user presses on it. How do we do that? Let's see an example.

item_text_with_button.xml:

<?xml version="1.0" encoding="utf-8"?><LinearLayoutxmlns:android="http://schemas.android.com/apk/res/android"android:orientation="vertical"android:layout_width="match_parent"android:layout_height="wrap_content"android:layout_margin="8dp">    <TextViewandroid:layout_width="match_parent"android:layout_height="wrap_content"android:id="@+id/textView" />    <ToggleButtonandroid:layout_width="match_parent"android:layout_height="wrap_content"android:id="@+id/toggleButton" /></LinearLayout>

TextWithButtonItem.kt:

classTextWithButtonItem(privatevaltext:String) : AdapterItem<TextWithButtonItem.Holder>(text) {privatevar pressed=falseoverridefunonFilter(searchTerm:String)= text.contains(searchTerm)overridefungetView(parent:ViewGroup):View= parent.inflating(R.layout.item_text_with_button)overridefunbind(firstTime:Boolean,holder:Holder) {        holder.textViewField.text= text        holder.buttonField.isChecked= pressed    }classHolder(itemView:View):RecyclerAdapterViewHolder(itemView),LayoutContainer {overrideval containerView:View?            get()= itemViewinternalval textViewField:TextView by lazy { textView }internalval buttonField:ToggleButton by lazy { toggleButton }init {            buttonField.setOnClickListener {                withAdapterItem<TextWithButtonItem> {                    pressed= buttonField.isChecked                    notifyItemChanged()                }            }        }    }}

In theHolder we have added a click listener to theToggleButton. When the user presses the toggle button, theAdapterItempressed status gets changed and then theRecyclerAdapter gets notified that the model has been changed by invokingnotifyItemChanged(). This triggers the rebinding of the ViewHolder to reflect the new model.

So, to recap, theevent lifecycle is:

// gets ViewHolder's AdapterItemwithAdapterItem<YourAdapterItem> {// methods to call on the adapter item// (optional)// if you want the current ViewHolder to be rebinded, call    notifyItemChanged()}

As a rule of thumb, if an event does not directly change the UI, you should not callnotifyItemChanged().

Single and Multiple selection of items

Often RecyclerViews are used to implement settings toggles, bottom sheets and other UI to perform selections. What is needed in the vast majority of the cases is:

  • a way to select a single item from a list of items
  • a way to select many items from a list of items

To complicate things, many times a single RecyclerView has to contain various groups of selectable items, for example let's imagine an online order form in which the user has to select:

  • a payment method from a list of supported ones (only one selection)
  • additions like extra support, gift packaging, accessories, ... (many selections)
  • shipping address (only one selection)
  • billing address (only one selection)

So it gets pretty complicated, huh 😨? Don't worry,RecyclerAdapter to the rescue! 🙌🏼

Check the example app implementations in GroupsSelectionActivity and SubordinateGroupsSelectionActivity to see what you can achieve!

Subordinate Groups Selections

Leave Behind pattern example implementation

In the demo app provided with the library, you can also see how to implement theleave behind material design pattern. All the changes involved into the implementation can be seen inthis commit. This implementation has not been included into the base library deliberately, to avoid depending on external libraries just for a single kind of item implementation. You can easily import the needed code in your project from the demo app sources if you want to have leave behind implementation.

Lock scrolling while inserting

When dynamically loading many data at once in the RecyclerView, specially when we are inserting new items at the first position, the default behavior of the RecyclerView, which scrolls down automatically may not be what we want. To lock the scrolling while inserting new items, be sure to have includedrecycleradapter-extensions in your project, then simply call:

recyclerAdapter.lockScrollingWhileInserting(layoutManager)

To get a better comprehension of this behavior, try commentinglockScrollingWhileInserting inSyncActivity and run the demo app again pressing theshuffle button to see the difference.

Contributors and Credits

Thanks to:


[8]ページ先頭

©2009-2025 Movatter.jp