Minimalist Android Recycler View Adapter (using Data Binding)

Keywords: Android xml github encoding

Reading this article requires readers to have some knowledge of Android Data Binding and Recycler View.

brief introduction

We know that the core idea of Data Binding is data-driven. The goal of data-driven is View. With DataBinding, View automatically changes by adding, modifying, and deleting data sources.

Android Recycler View's Adadapter serves to connect data to View.

A simplest Recycler View Adapter might look like this:

public class UserAdapter extends RecyclerView.Adapter
{
    @Override
    public int getItemCount()
    {
        return 0;
    }
    
    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
    {
        return null;
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
    {

    }
}

Through getItemsCount(), RecyclerView knows the number of all subitems.

Through onCreateViewHolder(), RecyclerView knows what each subitem looks like.

With onBindViewHolder(), each subitem can display the correct data.

As you can see, the role of Adapter is very similar to that of DataBinding. Using DataBinding can make the writing of Adapter simpler.

Data Binding is simple to use

Let's look at a simple example. This example creates a simple list with the following effects:

Let's see how to implement it using DataBinding.

Model class:

public class User
{
    private String name;
    private int age;

    public User(String name, int age)
    {
        this.name = name;
        this.age = age;
    }

    public String getName()
    {
        return name;
    }

    public void setName(String name)
    {
        this.name = name;
    }

    public int getAge()
    {
        return age;
    }

    public void setAge(int age)
    {
        this.age = age;
    }
}

View xml

<?xml version="1.0" encoding="utf-8"?>
<layout>
    <data>
        <import type="cn.zmy.databindingadapter.model.User"/>
        <variable name="model"
                  type="User"/>
    </data>
    <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
                  android:layout_width="match_parent"
                  android:layout_height="60dp"
                  android:layout_marginBottom="10dp"
                  android:background="@android:color/darker_gray"
                  android:gravity="center_vertical"
                  android:orientation="vertical">
        <TextView android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:text="@{model.name}"/>
        <TextView android:layout_width="wrap_content"
                  android:layout_height="wrap_content"
                  android:text="@{String.valueOf(model.age)}"/>
    </LinearLayout>
</layout>

Adapter

public class UserAdapter extends RecyclerView.Adapter
{
    private Context context;
    private List<User> items;

    public UserAdapter(Context context)
    {
        this.context = context;
        this.items = new ArrayList<User>()
        {{
            add(new User("Zhang San", 18));
            add(new User("Li Si", 28));
            add(new User("Wang Wu", 38));
        }};
    }

    @Override
    public int getItemCount()
    {
        return this.items.size();
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
    {
        ItemUserBinding binding = DataBindingUtil.inflate(LayoutInflater.from(this.context), R.layout.item_user, parent, false);
        return new UserViewHolder(binding.getRoot());
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
    {
        ItemUserBinding binding = DataBindingUtil.getBinding(holder.itemView);
        binding.setModel(this.items.get(position));
        binding.executePendingBindings();
    }

    static class UserViewHolder extends RecyclerView.ViewHolder
    {
        public UserViewHolder(View itemView)
        {
            super(itemView);
        }
    }
}

Activity

public class MainActivity extends AppCompatActivity
{

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));
        recyclerView.setAdapter(new UserAdapter(this));
    }
}

As you can see, after using Data Binding, we don't need to write some code similar to holder.view.setXXX() in onBindViewHolder, because these are already done in Xml.

optimization

Can the Adadapter above be simpler?

Optimizing ViewHolder

We found that the UserViewHolder in the Adapter did almost nothing. In fact, we declare that it is entirely because the onCreateViewHolder of Adapter requires such a return value.

We can propose ViewHolder so that all adapters can be used without having to declare a ViewHolder in each Adapter.

The name is BaseBindingViewHolder.

public class BaseBindingViewHolder extends RecyclerView.ViewHolder
{
    public BaseBindingViewHolder(View itemView)
    {
        super(itemView);
    }
}

Optimizing getItemCount

getItemCount returns the number of subitems.

Since almost every Adapter has a List to hold all the subitems'data, we can create an Adapter base class and implement getItemCount in the base class.

Optimizing onCreateViewHolder & onBindViewHolder

The onCreateViewHolder code is as follows:

@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
{
    ItemUserBinding binding = DataBindingUtil.inflate(LayoutInflater.from(this.context), R.layout.item_user, parent, false);
    return new BaseBindingViewHolder(binding.getRoot());
}

As you can see, the only "variable" in this method is the layout of "R.layout.item_user".

The onBindViewHolder code is as follows:

@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
{
    ItemUserBinding binding = DataBindingUtil.getBinding(holder.itemView);
    binding.setModel(this.items.get(position));
    binding.executePendingBindings();
}

As you can see, this method first gets the Binding of View, and then assigns the Data of Binding. Where does Binding come from? They are all obtained through DataBindingUtil.getBinding(holder.itemView).

In line with the principle that no duplicate code can be written and encapsulated, we will create the Adapter base class. The code is as follows:

public abstract class BaseBindingAdapter<M, B extends ViewDataBinding> extends RecyclerView.Adapter
{
    protected Context context;
    protected List<M> items;

    public BaseBindingAdapter(Context context)
    {
        this.context = context;
        this.items = new ArrayList<>();
    }

    @Override
    public int getItemCount()
    {
        return this.items.size();
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
    {
        B binding = DataBindingUtil.inflate(LayoutInflater.from(this.context), this.getLayoutResId(viewType), parent, false);
        return new BaseBindingViewHolder(binding.getRoot());
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
    {
        B binding = DataBindingUtil.getBinding(holder.itemView);
        this.onBindItem(binding, this.items.get(position));
    }

    protected abstract @LayoutRes int getLayoutResId(int viewType);

    protected abstract void onBindItem(B binding, M item);
}

Then the User Adapter is inherited from the BaseBindingAdapter encapsulated above. The code is as follows:

public class UserAdapter extends BaseBindingAdapter<User, ItemUserBinding>
{
    public UserAdapter(Context context)
    {
        super(context);
        items.add(new User("Zhang San", 18));
        items.add(new User("Li Si", 28));
        items.add(new User("Wang Wu", 38));
    }

    @Override
    protected int getLayoutResId(int viewType)
    {
        return R.layout.item_user;
    }

    @Override
    protected void onBindItem(ItemUserBinding binding, User user)
    {
        binding.setModel(user);
        binding.executePendingBindings();
    }
}

As you can see, the optimized Adapter removes the part of the code that initializes the User data source, and in fact the core code is very few lines.

Through getLayoutResId, we told RecyclerView what the subitem looked like.

With onBindItem, we bind the appropriate data for each specific subitem.

As for the specific binding process, it is placed in the layout xml file.

Optimizing data sources

Our data source is added in the constructor as follows:

items.add(new User("Zhang San", 18));
items.add(new User("Li Si", 28));
items.add(new User("Wang Wu", 38));

In the actual development process, we rarely do so. Because we usually don't get any valid data when we construct the Adapter. Data sources may come from servers through network requests or from local database tables. After we construct the Adapter, it may take a long time to get the valid data source, which requires that the external caller can modify the data source after the Adapter construction is completed.

We can do this:

adapter.items.add(XXX);
adapter.notifyItemInserted();

So when we add new data sources, adapter also knows that we modify the data sources, and then View changes.

But with DataBinding, we can do this more skillfully.

ObservableArrayList

Observable ArrayList is a class in the Android Data Binding library.

public class ObservableArrayList<T> extends ArrayList<T> implements ObservableList<T>
{
    ...
}

ObservableArrayList implements the ObservableList interface. With ObservableList, we can add one or more Listener s to ObservableArrayList. When the data in the Observable Array List changes (when one or more elements are added and one or more elements are deleted), these listeners or receive notifications of changes in the data source.

In fact, the implementation of Observable ArrayList is not complicated, just rewrite add, addAll, remove, and so on, which may cause changes in the collection to achieve the above effect.

Although the implementation is not complicated, Observable ArrayList can solve the problem of modifying the data source we encountered above.

We only need to call adapter.notifyXXX() and other methods when the collection changes, so that when the data source changes, the View can also automatically change, while the external need not call adapter.notifyXXX().

code implementation

Once again, we modify the code of BaseBindingAdapter to support automatically updating View when the data source changes.

public abstract class BaseBindingAdapter<M, B extends ViewDataBinding> extends RecyclerView.Adapter
{
    protected Context context;
    protected ObservableArrayList<M> items;
    protected ListChangedCallback itemsChangeCallback;

    public BaseBindingAdapter(Context context)
    {
        this.context = context;
        this.items = new ObservableArrayList<>();
        this.itemsChangeCallback = new ListChangedCallback();
    }

    public ObservableArrayList<M> getItems()
    {
        return items;
    }

    @Override
    public int getItemCount()
    {
        return this.items.size();
    }

    @Override
    public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType)
    {
        B binding = DataBindingUtil.inflate(LayoutInflater.from(this.context), this.getLayoutResId(viewType), parent, false);
        return new BaseBindingViewHolder(binding.getRoot());
    }

    @Override
    public void onBindViewHolder(RecyclerView.ViewHolder holder, int position)
    {
        B binding = DataBindingUtil.getBinding(holder.itemView);
        this.onBindItem(binding, this.items.get(position));
    }

    @Override
    public void onAttachedToRecyclerView(RecyclerView recyclerView)
    {
        super.onAttachedToRecyclerView(recyclerView);
        this.items.addOnListChangedCallback(itemsChangeCallback);
    }

    @Override
    public void onDetachedFromRecyclerView(RecyclerView recyclerView)
    {
        super.onDetachedFromRecyclerView(recyclerView);
        this.items.removeOnListChangedCallback(itemsChangeCallback);
    }

    //region handles data set changes
    protected void onChanged(ObservableArrayList<M> newItems)
    {
        resetItems(newItems);
        notifyDataSetChanged();
    }

    protected void onItemRangeChanged(ObservableArrayList<M> newItems, int positionStart, int itemCount)
    {
        resetItems(newItems);
        notifyItemRangeChanged(positionStart,itemCount);
    }

    protected void onItemRangeInserted(ObservableArrayList<M> newItems, int positionStart, int itemCount)
    {
        resetItems(newItems);
        notifyItemRangeInserted(positionStart,itemCount);
    }

    protected void onItemRangeMoved(ObservableArrayList<M> newItems)
    {
        resetItems(newItems);
        notifyDataSetChanged();
    }

    protected void onItemRangeRemoved(ObservableArrayList<M> newItems, int positionStart, int itemCount)
    {
        resetItems(newItems);
        notifyItemRangeRemoved(positionStart,itemCount);
    }

    protected void resetItems(ObservableArrayList<M> newItems)
    {
        this.items = newItems;
    }
    //endregion

    protected abstract @LayoutRes int getLayoutResId(int viewType);

    protected abstract void onBindItem(B binding, M item);

    class ListChangedCallback extends ObservableArrayList.OnListChangedCallback<ObservableArrayList<M>>
    {
        @Override
        public void onChanged(ObservableArrayList<M> newItems)
        {
            BaseBindingAdapter.this.onChanged(newItems);
        }

        @Override
        public void onItemRangeChanged(ObservableArrayList<M> newItems, int i, int i1)
        {
            BaseBindingAdapter.this.onItemRangeChanged(newItems,i,i1);
        }

        @Override
        public void onItemRangeInserted(ObservableArrayList<M> newItems, int i, int i1)
        {
            BaseBindingAdapter.this.onItemRangeInserted(newItems,i,i1);
        }

        @Override
        public void onItemRangeMoved(ObservableArrayList<M> newItems, int i, int i1, int i2)
        {
            BaseBindingAdapter.this.onItemRangeMoved(newItems);
        }

        @Override
        public void onItemRangeRemoved(ObservableArrayList<M> sender, int positionStart, int itemCount)
        {
            BaseBindingAdapter.this.onItemRangeRemoved(sender,positionStart,itemCount);
        }
    }
}

Then we modify Activity's code:

public class MainActivity extends AppCompatActivity
{

    @Override
    protected void onCreate(Bundle savedInstanceState)
    {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        RecyclerView recyclerView = (RecyclerView) findViewById(R.id.recyclerView);
        recyclerView.setLayoutManager(new LinearLayoutManager(this));

        UserAdapter adapter = new UserAdapter(this);
        recyclerView.setAdapter(adapter);

        adapter.getItems().add(new User("Zhang San", 18));
        adapter.getItems().add(new User("Li Si", 28));
        adapter.getItems().add(new User("Wang Wu", 38));
    }
}

As you can see, the external only adds data to the data source, without doing anything else. But our View is updated, and the effect is the same as above. This also conforms to the core principle of Data Binding: data-driven. With DataBinding, all we care about is the data source. As long as the data source changes, the View should change accordingly.

Demo

The code in this article has been sorted and uploaded to Github.
Links: https://github.com/a3349384/DataBindingAdapter
Blog: https://www.zhoumingyao.cn/

Posted by davidlenehan on Sun, 09 Jun 2019 17:32:23 -0700