Within a hundred lines-ultra-lightweight multi-type list view framework

Within a hundred lines-ultra-lightweight multi-type list view framework

The name is a bit bluffing, but it is actually a combination of several encapsulated classes that can be easily implemented RecyclerViewwith multiple views. After all, the word "frame" in my opinion still refers to a code system with a certain scale and key technology, but it only solves specific problems. As far as it goes, it might as well be named this name. At the same time, it is really "ultra-lightweight" with only 4 categories, no more than 130 lines of code

View abstraction

We already have a universal ViewHolder (ItemViewHolder) that does not require type force conversion . A ViewHolder object can find all view instances. And it is completely independent, without introducing any custom classes or any third-party dependencies; even without this "framework", it can be taken apart and used in other places.

Control adapter

Adapter is associated with the control and is an abstraction of the control's subview list. What is abstract? Determined by the specific definition. For example, the adapter of the list control (whether it is before ListView, now RecyclerView, or others ViewPager) generally abstracts three properties:

  1. Quantity getItemCount()
  2. Operation onBindView (ViewHolder holder, int position), onCreateView
  3. Type getViewType(int position)

Control adaptation is related to the SDK, and the framework is ItemAdapteralso based onRecyclerView.Adapter

Element abstraction

Adapter is the overall abstraction of the container control to the child control. There is no restriction on the element at the corresponding position. The positioncorresponding element can be a specific data returned by the interface or application data obtained locally. One of the work of the framework is to abstract the element data types, but the data types are so different that it is impossible to perform unified operations on the attributes of the data element itself. The result is to become like a MultiTypelibrary, abstract all data elements with a paradigm, and then register The data type ( .class) ItemViewBinder.classis mapped to the data binder type ( ), and the binder instance is obtained by reflection, in which a large number of object types are forced to be converted.

The framework does not abstract the data elements, but abstracts the operations, that is, the adapter abstracts positionthe operations of each element; uses a simple Listdata structure to hold the abstract instance; because there are also binding operations, it is also called a binder for the time being ItemBinder. ViewHolder uses our previous general ViewHolder( ItemViewHolder), combined with the previous mentioned adapter has three important attributes, so there are:

public interface ItemBinder {
    void onBindViewHolder(ItemViewHolder holder, int position);
    int getViewType();
}

public class ItemAdapter extends RecyclerView.Adapter<ItemViewHolder> {
    private final List<ItemBinder> mBinders = new ArrayList<>(10);

    @Override
    public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) {
        ItemBinder binder = mBinders.get(position);
        binder.onBindViewHolder(holder, position);
    }

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

    @Override
    public int getItemViewType(int position) {
        return mBinders.get(position).getViewType();
    }

    public void setBinders(List<ItemBinder> binders) {
        mBinders.clear();
        mBinders.addAll(binders);
    }
}
 

For the Adapter, the element is just that ItemBinder, it doesn't care ItemBinderwhat data type is used, and how to fill the data into the ViewHolder.

View type

RecyclerViewThe value returned by RecyclerView.Adapterthe getItemViewTypeinterface to identify a view type. The ListViewdifference is that the viewType may not be continuous, and RecyclerViewyou can perceive how many types are set viewType(in fact, it is used internally SparseArray). By viewTypethe identification, RecyclerView.Adapterof onCreateViewHoldercreating a corresponding view type. Usually we have to establish the mapping relationship of viewTypeand RecyclerView.ViewHolderby ourselves , and there is no big problem except a little cumbersome.

Note : We have reached a key point of the framework, which is to establish viewTypea relationship with the creation of a view instance.

I couldn't find which library it was in. When I saw that the view resource id (layoutId) was viewTypereturned directly , I was overwhelmed by this genius idea. The first is the resource id itself can create views; followed by full use of viewTypepossible discontinuous nature; the id is different for different natural resource corresponding to the type of view again, that is, in itself is a multi-view type; the final end It is this kind of implementation that provides great flexibility, including code reuse and resource reuse, which I will talk about later. So there are:

public interface ItemBinder {
    void onBindViewHolder(ItemViewHolder holder, int position);

    @LayoutRes
    int getLayoutId();
}

public class ItemAdapter extends RecyclerView.Adapter<ItemViewHolder> {
    private final List<ItemBinder> mBinders = new ArrayList<>(10);

    @NonNull
    @Override
    public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup container, int viewType) {
        return new ItemViewHolder(LayoutInflater.from(container.getContext()).inflate(
                viewType, container, false));
    }

    @Override
    public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) {
        ItemBinder binder = mBinders.get(position);
        binder.onBindViewHolder(holder, position);
    }

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

    @Override
    public int getItemViewType(int position) {
        return mBinders.get(position).getLayoutId();
    }

    public void setBinders(List<ItemBinder> binders) {
        mBinders.clear();
        mBinders.addAll(binders);
    }
}
 

We were getItemViewTypemisled by the default value of 0 before . The inertia of thinking makes us think that they viewTypecan ViewHolderbe separated from each other, but in fact they can be unified!

The rest of the work is simple and clear, implement specific ItemBindertypes and fill specific data into the view, such as:

public HomeBannerBinder implements ItemBinder {
    private final HomeBanner mItem;
    HomeBannerBinder(HomeBanner banner) {
        mItem = banner;
    }

    void onBinderViewHolder(ItemViewHolder holder, int position) {
        ImageView bg = holder.findViewById(R.id.background);
        if (bg != null) {
            ImageManager.load(bg, mItem.bg_url);
        }
    }
}
 

Flexible reuse

The reuse here is not the reuserecyclerView of the view memory object, but the reuse of the code, including the xml code that declares the resource.

viewTypeWhat kind of flexible reuse does the layoutId bring?

Let s take an example of a common WeChat Moments list: Obviously, many Moments content is different. There are videos, pictures and text, or a combination of them. The layout of processing 2 pictures and the layout of processing 9 pictures are also different. ; But every Moments layout has many similarities: it has a user avatar and user name at the top, and a like and comment layout at the bottom. So the question is: how to declare different view types without having to write these same places repeatedly?

This is of course not difficult. For example, the layout of a video circle of friends can be written like thiscircle_item_video.xml

<RelativeLayout>
     <include layout="@layout/circle_item_top"/>
     <include layout="@layout/circle_item_layer_video"/>
     <include layout="@layout/circle_item_bottom"/>
</RelativeLayout>
 

The layout of the audio Moments circle_item_audio.xmlwill be @layout/circle_item_layer_videoreplaced with @layout/circle_item_layer_audio, and so on.

This is completely achievable. As the types increase, the layout files can be increased accordingly; however, what happens if there is a change? As long as it involves the same layout, it must be changed again! (For example, turn it RelativeLayoutinto android.support.constraint.ConstraintLayout) And the actual situation may not be so simple. It may be because of various reasons that the level of the view is relatively deep, and there is no way to put it in the include. Once there are more view objects, the level of the view becomes deeper. This kind of redundancy It's unbearable. For a pursuing code animal, he definitely hopes to change only one place.

View reuse

How to realize the reuse just now if layoutId is used as viewType? Obviously they must be different viewType(what happens if they are the same?), so of course they are different layoutId, but different layoutId cannot avoid the above problem. At this time, android anonymous resources (anonymous) are used . A resource declares a reference, and the reference itself as a resource, that is <item name="def" type="drawable">@drawable/abc</item>, combined with the above example is circle_item.xml:

<RelativeLayout>
     <include layout="@layout/circle_item_top"/>
     <ViewStub/>
     <include layout="@layout/circle_item_bottom"/>
</RelativeLayout>
 

The middle part can be set to different Views by lazy loading, and even all the different parts can be ViewStubembedded in the layout in the form of. refs.xml:

<resources>
    <item name="circle_item_video" type="layout">@layout/circle_item</item>
    <item name="circle_item_audio" type="layout">@layout/circle_item</item>
    <item name="circle_item_pic_1" type="layout">@layout/circle_item</item>
    <item name="circle_item_pic_9" type="layout">@layout/circle_item</item>
</resources>
 

In other words, they all quote the same layout resource! But they layoutIdcan be recyclerViewregarded as different because they are different viewType!

Code reuse

According to the previous thinking, I also want to change the like and comment functions only in one place. So there is a base class:

public class CircleItemBinder implements ItemBinder {
    @Override
    public getLayoutId() {
        return R.layout.circle_item;
    }

    @Override
    void onBindViewHolder(ItemViewHolder holder, int position) {
        bindComment(holder);
        bindLike(holder);
    }

    private void bindComment(ItemViewHolder holder) {
    }

    private void bindLike(ItemViewHolder holder) {
    }
}
 

Each type of binder is similar:

public class CircleVideoBinder extends CircleItemBinder {
    private final YourVideoData mItem;

    public CircleVideoBinder(YourVideoData data) {
        mItem = data;
    }

    @Override
    public getLayoutId() {
        return R.layout.circle_item_video;
    }

    @Override
    void onBindViewHolder(ItemViewHolder holder, int position) {
        super.onBindViewHolder(holder, position);
        TextView title = holder.findViewById(R.id.video_title);
        if (title != null) {
            title.setText(mItem.title);
        }
        ...
    }
}

public class CircleAudioBinder extends CircleItemBinder {
    private final YourAudioData mItem;

    public CircleAudioBinder(YourAudioData data) {
        mItem = data;
    }

    @Override
    public getLayoutId() {
        return R.layout.circle_item_audio;
    }

    @Override
    void onBindViewHolder(ItemViewHolder holder, int position) {
        super.onBindViewHolder(holder, position);
        ImageView album = holder.findViewById(R.id.audio_album);
        if (album != null) {
            ImageLoader.load(album, mItem.album_background);
        }
        ...
    }
}
 

The code for the like and comment functions can be fully reused! All this is just using layoutId as the viewType! So far, the whole picture of the framework has been presented:

public interface ItemBinder {
    @LayoutRes
    int getLayoutId();

    void onBindViewHolder(ItemViewHolder holder, int position);
}

public class ItemAdapter extends RecyclerView.Adapter<ItemViewHolder> {
    private final List<ItemBinder> mBinders = new ArrayList<>(10);

    @NonNull
    @Override
    public ItemViewHolder onCreateViewHolder(@NonNull ViewGroup container, int viewType) {
        return new ItemViewHolder(LayoutInflater.from(container.getContext()).inflate(
                viewType, container, false));
    }

    @Override
    public void onBindViewHolder(@NonNull ItemViewHolder holder, int position) {
        ItemBinder binder = mBinders.get(position);
        binder.onBindViewHolder(holder, position);
    }

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

    @Override
    public int getItemViewType(int position) {
        return mBinders.get(position).getLayoutId();
    }

    public void setBinders(List<ItemBinder> binders) {
        mBinders.clear();
        appendBinders(binders);
    }
}
 

Our previous general ViewHolder is also listed here:

public class ItemViewHolder extends RecyclerView.ViewHolder {
    private final SparseArrayCompat<View> mCached = new SparseArrayCompat<>(10);

    public ItemViewHolder(View itemView) {
        super(itemView);
    }

    public <T extends View> T findViewById(@IdRes int resId) {
        int pos = mCached.indexOfKey(resId);
        View v;
        if (pos < 0) {
            v = itemView.findViewById(resId);
            mCached.put(resId, v);
        } else {
            v = mCached.valueAt(pos);
        }
        @SuppressWarnings("unchecked")
        T t = (T) v;
        return t;
    }
}
 

Generally, it is necessary to define a base class ItemBaseBinder. All derived classes may share an operation. This base class receives resource id as a constructor parameter:


public class ItemBaseBinder implements ItemBinder {
    private final int mLayoutId;

    public ItemBaseBinder(@layoutRes int layoutId) {
        mLayoutId = layoutId;
    }

    @Override
    public void onBindViewHolder(ItemViewHolder holder, int position) {
    }

    @Override
    public int getLayoutId() {
        return mLayoutId;
    }
}
 

The rest of the work is just to derive specific business classes, just like the previous example! All this is only 130 lines of code!

And MutiTypethe difference

Example one-to-one

MutiTypeThe library also has a binder, ItemViewBinderbut note that its binding is only one instance, and ours ItemAdapteruses the binder as an element object. One data corresponds to one binder, so it has multiple instances. In fact, this binder is Abstraction of data.

No type conversion and no reflection operation

Really, MutiTypeit's too complicated to make all of this! Sadly, there are still many people using...

Concluding remarks

With this framework, flexibility is not only lost at all, but also more concise,MutiType The type of forced rotation and reflection operations can be entered into the museum.

A large article is a bit cumbersome to talk about, and you can understand it directly by directly reading the code. The key is the thinking process and the idea of solving problems. What problems are solved by all the frameworks? This is the most important thing to understand and learn, otherwise the framework will be endless. Once we have ideas and goals, it is not difficult to achieve a framework. This small framework has been in practice for a long time, and it can cover most situations, and the effect is surprisingly good, MutiType"I don't know where it is higher than that."

There are 2 points to note

  1. onBindViewHolderThe method only does data filling and should not do data processing. In fact, it has nothing to do with the framework. There are still many people onBindViewHolderdoing data processing in Adapter.
  2. Dynamically changing the view type. Because the method getLayoutIdis an interface, it means that a different layoutId can be returned at runtime to dynamically change the view type, but it needs to be used in notifyItemChangedconjunction with Adatper
  3. External update notification ItemAdapter.setBindersmethod to achieve body does not call after the update examples notifyDataSetChanged, this operation should be determined by the external, although here is necessary, but it is likely to cause redundant updates.

Expand

The framework is also very easy to expand according to specific needs and scenarios.

Nested

In the case of a nested list of lists, how to abstract, in fact, it only needs to correspond to the view. The outermost list (first-level list) has a special ItemBindertype, and this type itself can also hold multiple ItemBinderlists provided to the inner-level list (second-level list):

public class ItemContainerBinder extends ItemBaseBinder {
    private final ItemAdapter mAdapter = new ItemAdapter();

    @Override
    public void onBinderViewHolder(ItemViewHolder holder, int position) {
        RecyclerView secondary = holder.findViewById(R.id.secondary);
        if (secondary != null) {
            if (secondary.getAdapter() != mAdapter) {
                secondary.setAdapter(mAdapter);
            }
            if (secondary.getLayoutManager() == null) {
                secondary.setLayoutManager(new LinearLayoutManager(secondary.getContext());
            }
        }
    }

    public void setBinders(List<ItemBinder> binders) {
        mAdapter.setBinders(binders);
    }
...
}
 

Here you can also use the previously mentioned reuse LayoutManager !

Partial update

It is very common that only one item of the list needs to be updated during operation. In many cases, you can not directly update the view by calling the method of the view object, but also call it Adapter.notifyItemChanged(like the dynamic update list view type mentioned above). That is, the Adapter holds ItemBinder, and you ItemBinderneed to call the Adapter method again, if you letItemBinder go references Adapter, this strong coupling is not necessarily a good design.

For the implementation of this framework, at this time, the ItemBinderinternal changes need to be notified first , but the timing of notification should be determined by the ItemBinderimplementation body, and the external response should be passive. This is of course the simplest observer pattern, so there are:

public interface ItemBinder {
...
    void setOnChangeListener(OnChangeListener listener);

    interface OnChangeListener {
        void onItemChanged(ItemBinder item, int payload);
    }
}

public class ItemBaseBinder implements ItemBinder {
...
    private OnChangeListener mChangeListener;

    @Override
    publi final void setChangeListener(OnChangeListener listener) {
        mChangeListener = listener;
    }

    public final void notifyItemChange(int payload) {
        if (mChangeListener != null) {
            mChangeListener.onItemChanged(this, payload);
        }
    }
}
 

The payloadreference here is RecyclerView.Adapterjust that the type has Objectchanged from int, which represents the information that needs to be carried in the partial update. In the ItemBinderimplementation body, because a certain data change needs to be notified to the outside, you only need to call the notifyItemChangemethod, pass the change out, and the outside will make a specific response:

List<ItemBinder> binders = new ArrayList<>();
...
ItemBinder special = new XXXYYYBinder(...);
specail.setChangeListener(new ItemBinder.OnChangeListener() {
    @Override
    public void onItemChanged(ItemBinder item, int payload) {
        int pos = mAdapter.indexOf(item);
        if (pos >= 0) {
            mAdapter.notifyItemChanged(pos,...);
        }
    }
});
binders.add(special);
...
mAdapter.setBinders(binders);