Android Data Binding : Dynamic RecyclerView Adapter
One of the most common list design patterns that I encounter as Android developer is a list with a variation of the following:
And all of these can be implemented in a generic manner with Android Data Binding library. You can write one adapter for all the RecycleViews in your app and use it to render all the common design patterns. Check out the source for the example in this tutorial here.
Final result:
Lets begin by defining our adapter:
The RecyclerViewBindingAdapter simply inflates each view type that is a layoutId. Next it binds all the variables to the appropriate models.
The AdapterDataItem model contains layout id field and a list of integer - object pairs. The reason for this is because any layout with data binding can have multiple variables and for dynamic variable bindings we need to know those variable ids for each layout in order to pass the data models from the code.
The BindingViewHolder class simply holds reference of the binding and provides the bind() method to bind the layout variables to models.
The ObservableListCallback class is a wrapper to automatically provide notification of change in our data to the adapter.
Next lets create an Presenter interface to help us keep the event handling separate from ui logic.
Next up lets create some custom attributes for RecyclerView:
Custom attributes for RecyclerView allow us to set the adapter, layout manager and item animator in the xml binding definition but are not necessary as you can just set these properties normally from code.
Lets take a look at a couple of our layouts:
activity_main.xml
Here we define the bindings for the RecyclerView's custom attributes.
layout_item.xml
Here we set the data bindings for the row's text content and on click handling. We also have couple more layouts such as header layout, sub item layout, loading layout, and load more layout, that our list will use but they are omitted as you are encouraged to create your own to see the flexibility of our dynamic adapter.
Finally we can create our Activity:
The MainActivity class initializes the list with a header row and a loading (progress bar) row. Then to simulate network load delay after 1.5 seconds we add some content items below the header and replace loading row to a "Load More" row. All the changes to the ObservableList listItems list are automatically updated in the adapter as it is registered to observe for changes in our collection.
And the ListItemPresenter interface is implemented by MainActivity like so:
The callbacks of the ListItemPresenter implementation allows us to handle row deletion,expanding rows, and appending more rows simply by inserting or deleting items in our ObservableList listItems and again any list changes are automatically reflected in the adapter.
Results:
- Multiple row types.
- Expandable/Collapsible rows or sections.
- Load more/Scroll to load more.
- Delete rows/Sections.
And all of these can be implemented in a generic manner with Android Data Binding library. You can write one adapter for all the RecycleViews in your app and use it to render all the common design patterns. Check out the source for the example in this tutorial here.
Final result:
Lets begin by defining our adapter:
public class RecyclerViewBindingAdapter extends RecyclerView.Adapter<RecyclerViewBindingAdapter.BindingViewHolder> { private ObservableList<AdapterDataItem> data; public RecyclerViewBindingAdapter(ObservableList<AdapterDataItem> data) { this.data = data; data.addOnListChangedCallback(new ObservableListCallback()); } @Override public BindingViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { return new BindingViewHolder(DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()),viewType,parent,false)); } @Override public void onBindViewHolder(BindingViewHolder holder, int position) { AdapterDataItem dataItem = data.get(position); for(Pair<Integer, Object> idObjectPair: dataItem.idModelPairs){ holder.bind(idObjectPair.first, idObjectPair.second); } holder.binding.executePendingBindings(); } @Override public int getItemCount() { return data.size(); } @Override public int getItemViewType(int position) { return data.get(position).layoutId; } private class ObservableListCallback extends ObservableList.OnListChangedCallback<ObservableList<RecyclerViewBindingAdapter.AdapterDataItem>>{...} public class BindingViewHolder extends RecyclerView.ViewHolder{...} public static class AdapterDataItem {...} }
The RecyclerViewBindingAdapter simply inflates each view type that is a layoutId. Next it binds all the variables to the appropriate models.
public class AdapterDataItem { public int layoutId; List<Pair<Integer, Object>> idModelPairs; ... }
The AdapterDataItem model contains layout id field and a list of integer - object pairs. The reason for this is because any layout with data binding can have multiple variables and for dynamic variable bindings we need to know those variable ids for each layout in order to pass the data models from the code.
public class BindingViewHolder extends RecyclerView.ViewHolder{ private ViewDataBinding binding; public BindingViewHolder(ViewDataBinding binding) { super(binding.getRoot()); this.binding=binding; } public void bind(int varId, Object obj){ this.binding.setVariable(varId, obj); } }
The BindingViewHolder class simply holds reference of the binding and provides the bind() method to bind the layout variables to models.
private class ObservableListCallback extends ObservableList.OnListChangedCallback<ObservableList<RecyclerViewBindingAdapter.AdapterDataItem>>{ @Override public void onChanged(ObservableList<AdapterDataItem> sender) { notifyDataSetChanged(); } @Override public void onItemRangeChanged(ObservableList<AdapterDataItem> sender, int positionStart, int itemCount) { notifyItemRangeChanged(positionStart, itemCount); } @Override public void onItemRangeInserted(ObservableList<AdapterDataItem> sender, int positionStart, int itemCount) { notifyItemRangeInserted(positionStart, itemCount); } @Override public void onItemRangeMoved(ObservableList<AdapterDataItem> sender, int fromPosition, int toPosition, int itemCount) { notifyDataSetChanged(); // not sure how to notify adapter of this event } @Override public void onItemRangeRemoved(ObservableList<AdapterDataItem> sender, int positionStart, int itemCount) { notifyItemRangeRemoved(positionStart, itemCount); } }
The ObservableListCallback class is a wrapper to automatically provide notification of change in our data to the adapter.
Next lets create an Presenter interface to help us keep the event handling separate from ui logic.
public interface ListItemsPresenter { void onClick(ItemModel itemModel); void onClick(SubItemModel subItemModel); void onDeleteClick(ItemModel itemModel); void onExpandClick(ItemModel itemModel); void onLoadMoreClick(); }
Next up lets create some custom attributes for RecyclerView:
public class AttributeBindingsAddapter { @BindingAdapter({"bind:list","bind:layoutManager","bind:itemAnimator"}) public static void setList(RecyclerView rv, ObservableList dataItems, RecyclerView.LayoutManager layoutManager, RecyclerView.ItemAnimator itemAnimator){ if(rv.getLayoutManager()==null) rv.setLayoutManager(layoutManager); if(rv.getAdapter() ==null) rv.setAdapter(new RecyclerViewBindingAdapter(dataItems)); if(rv.getItemAnimator() == null) rv.setItemAnimator(itemAnimator); } }
Custom attributes for RecyclerView allow us to set the adapter, layout manager and item animator in the xml binding definition but are not necessary as you can just set these properties normally from code.
Lets take a look at a couple of our layouts:
activity_main.xml
<?xml version="1.0" encoding="utf-8"?> <layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto"> <data> <variable name="modelList" type="android.databinding.ObservableList"/> <variable name="listLayoutManager" type="android.support.v7.widget.RecyclerView.LayoutManager"/> <variable name="itemAnimator" type="android.support.v7.widget.RecyclerView.ItemAnimator"/> </data> <android.support.v7.widget.RecyclerView ... app:list="@{modelList}" app:layoutManager="@{listLayoutManager}" app:itemAnimator="@{itemAnimator}"/> </layout>
Here we define the bindings for the RecyclerView's custom attributes.
layout_item.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:tools="http://schemas.android.com/tools"> <data> <variable name="itemModel" type="com.codeprinciples.advancedadapterexample.models.ItemModel"/> <variable name="itemPresenter" type="com.codeprinciples.advancedadapterexample.presenters.ListItemsPresenter"/> </data> <LinearLayout ... android:onClick="@{(v) -> itemPresenter.onClick(itemModel)}"> <TextView ... android:text="@{itemModel.item}"/> <ImageView ... android:onClick="@{(v) -> itemPresenter.onDeleteClick(itemModel)}"/> <ImageView android:onClick="@{(v) -> itemPresenter.onExpandClick(itemModel)}"/> </LinearLayout> </layout>
Here we set the data bindings for the row's text content and on click handling. We also have couple more layouts such as header layout, sub item layout, loading layout, and load more layout, that our list will use but they are omitted as you are encouraged to create your own to see the flexibility of our dynamic adapter.
Finally we can create our Activity:
public class MainActivity extends AppCompatActivity implements ListItemsPresenter{ private ActivityMainBinding mBinding ; private ObservableList<RecyclerViewBindingAdapter.AdapterDataItem> listItems; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); mBinding= DataBindingUtil.setContentView(this, R.layout.activity_main); mBinding.setListLayoutManager(new LinearLayoutManager(this,LinearLayoutManager.VERTICAL,false)); mBinding.setModelList(initList()); mBinding.setItemAnimator(new DefaultItemAnimator()); loadDataWithDelay(1500); } private ObservableList initList() { listItems = new ObservableArrayList<>(); listItems.add(new RecyclerViewBindingAdapter.AdapterDataItem(R.layout.layout_heading, new Pair<Integer, Object>(BR.headingModel, new HeadingModel("Content Heading")))); listItems.add(new RecyclerViewBindingAdapter.AdapterDataItem(R.layout.layout_loading)); return listItems; } private void loadDataWithDelay(int delayMilli) { new Handler().postDelayed(new Runnable() { @Override public void run() { listItems.addAll(listItems.size()-1,getItems());//insert before loading cell listItems.remove(listItems.size()-1);//remove loading cell listItems.add(new RecyclerViewBindingAdapter.AdapterDataItem(R.layout.layout_load_more, new Pair<Integer, Object>(BR.presenter,MainActivity.this))); } },delayMilli); } ... }
The MainActivity class initializes the list with a header row and a loading (progress bar) row. Then to simulate network load delay after 1.5 seconds we add some content items below the header and replace loading row to a "Load More" row. All the changes to the ObservableList listItems list are automatically updated in the adapter as it is registered to observe for changes in our collection.
And the ListItemPresenter interface is implemented by MainActivity like so:
public class MainActivity extends AppCompatActivity implements ListItemsPresenter{ ... @Override public void onClick(ItemModel itemModel) { Toast.makeText(this, "itemModel clicked", Toast.LENGTH_SHORT).show(); } @Override public void onClick(SubItemModel subItemModel) { Toast.makeText(this, "subItemModel clicked", Toast.LENGTH_SHORT).show(); } @Override public void onDeleteClick(ItemModel itemModel) { if(itemModel.expanded) onExpandClick(itemModel); listItems.remove(Utils.convert(itemModel, this)); } @Override public void onExpandClick( ItemModel itemModel) { itemModel.expanded = !itemModel.expanded; RecyclerViewBindingAdapter.AdapterDataItem item = Utils.convert(itemModel, this); if(itemModel.expanded) { listItems.addAll(listItems.indexOf(item)+1, getSubItems(itemModel)); }else { int index = listItems.indexOf(item)+1; listItems.subList(index, index + itemModel.subItemModels.size()).clear(); } } @Override public void onLoadMoreClick() { listItems.remove(listItems.size()-1);//remove load more cell listItems.add(new RecyclerViewBindingAdapter.AdapterDataItem(R.layout.layout_loading)); //add loading cell loadDataWithDelay(1500); } }
The callbacks of the ListItemPresenter implementation allows us to handle row deletion,expanding rows, and appending more rows simply by inserting or deleting items in our ObservableList listItems and again any list changes are automatically reflected in the adapter.
Results:
Sources and further reading:
- GitHub project for code in this tutorial
- Official Android Data Binding docs
- Simple Dynamic List Adapter and other cool Android Data Binding tricks
- Getting Started with Android Data Binding in existing project
I'll right away take hold of your rss as I can't find your e-mail subscription hyperlink or newsletter service. Do you have any? Please allow me know in order that I may subscribe. Thanks. americanexpress.com login
ReplyDeleteWow, amazing blog format! How lengthy have you been blogging for? you make blogging look easy. The whole look of your site is magnificent, as smartly as the content material!
ReplyDeletesign in hotmail
This is an excellent blog. Thanks for taking the time to share this information. Waiting for more updates.
ReplyDeleteSpoken English Classes in Velachery
Spoken English in Velachery
Spoken English Classes in Tambaram
Spoken English Class in Chrompet
Spoken English Classes in OMR Chennai
Spoken English Classes in Navalur
Spoken English Class in Ambattur
Spoken English Class in Avadi
Thank you for providing the creativity post with comprehensive content. This is very helpful for gain my skill knowledge, Kindly updating...
ReplyDeleteLinux Training in Chennai
best linux training institute in chennai
Social Media Marketing Courses in Chennai
Pega Training in Chennai
Oracle Training in Chennai
Oracle DBA Training in Chennai
Tableau Training in Chennai
Unix Training in Chennai
Excel Training in Chennai
Linux Training Fees in Chennai
thanks for sharing this information
ReplyDeletews training in Bangalore
Amazon web services training in Bangalore
best AWS Training institute in Bangalore
Aws training institutes in Bangalore
Aws certification course in Bangalore
DevOps training in Bangalore
Devops training institutes in Bangalore
develops certification course in Bangalore
Sensitivity analysis is used to check changes in factors which are beyond the users control, such as being a change in interest rates. mortgage payment calculator canada She shopped around continuously and assured us she would get us an excellent rate. mortgage payment calculator canada
ReplyDeleteThis comment has been removed by the author.
ReplyDelete