BindingAdapter(use in recyclerView)


Posted by Limon on 2023-01-12

在將舊的code更換成mvvm架構中,我發現如果沒辦法在xml使用自定義屬性時,在xml中除了livedata直接綁定各種原有屬性之外,基本上viewModel沒其他用處。這時候建立一個自定義的BindingAdapter就很重要了,而我通常會把所有自定義BindingAdapter放在同一個class裡,命名為CustomBindingAdapter。

以下舉例使用方式:

floatingactionbutton

要簡單使用LiveData去控制懸浮按鈕的show hide就必須先定義好BindingAdapter。

CustomBindingAdapter.class:

@BindingAdapter("showHideExtendedFloating")
    public static void setShowHideExtendedFloatingButton(ExtendedFloatingActionButton floatingButton, boolean show) {
        if (show) {
            floatingButton.show();
        } else {
            floatingButton.hide();
        }
    }

xml:

<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/extended_floating_button"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="end" android:backgroundTint="@color/white"
app:icon="@drawable/icon_fab_action"
app:showHideExtendedFloating="@{viewModel.showFloatingMenu}" />

這樣子就可以使用livedata控制floatingButton顯示與否了。

甚至可以在viewModel設置不同的mode去控制顯示機制:

<com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
android:id="@+id/extended_floating_button"
android:layout_width="40dp"
android:layout_height="40dp"
android:layout_gravity="end" android:backgroundTint="@color/white"
app:icon="@drawable/icon_fab_action"            
app:showHideExtendedFloating="@{viewModel.showFloatingMenu &amp;&amp; viewModel.mode != constants.MODE_DEFAULT &amp;&amp; viewModel.mode == constants.ID_LIST_FRAGMENT}"
 />

這些動作若都在fragment或activity會讓程式碼可讀性降低,放在xml裡操作清楚又好維護。

RecyclerView

以我目前的經驗RecyclerView絕對是最常用的BindingAdapter的UI,超級好用!不用再建立一個一個惱人的adpater,寫法稍微複雜一點,但只要上手就回不去了!

目標:需要使用泛型建立通用的adapter、viewModel,在adapter中我們可以自動檢查item是否一樣,item內容是否一樣。

Step1.建立RecyclerView通用adapter&viewModel

public abstract class GenericRecyclerViewAdapterWithViewModel<T extends RecyclerViewBase, V> extends RecyclerView.Adapter<GenericRecyclerViewAdapterWithViewModel.ViewHolder> {

    private List<T> list;
    private V viewModel;
    private String label;
    private int maxCount;
    private int count;

    public GenericRecyclerViewAdapterWithViewModel(V viewModel) {
        list = new ArrayList<>();
        this.viewModel = viewModel;
    }

    @NonNull
    @Override
    public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
        ViewDataBinding viewDataBinding = DataBindingUtil.inflate(LayoutInflater.from(parent.getContext()), viewType, parent, false);
        return new ViewHolder(viewDataBinding);
    }

    @Override
    public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
        holder.bind(list.get(position), viewModel);
    }

    @Override
    public int getItemCount() {
        if (getMaxCount() != 0) {
            return Math.min(list.size(), getMaxCount());
        } else if (getCount() != 0) {
            return getCount();
        }
        return list.size();
    }

    @Override
    public int getItemViewType(int position) {
        return list.get(position).getRecyclerItemType();
    }

    public void setItems(List<T> data) {
        if (list == null) {
            list = new ArrayList<>();
        }
        if (!list.isEmpty()) {
            list.clear();
        }
        list.addAll(data);
        notifyDataSetChanged();
    }

    public void setDiffItems(List<T> newList) {
        DiffUtil.DiffResult diffResult = DiffUtil.calculateDiff(new DiffUtilCallback(list, newList));
        diffResult.dispatchUpdatesTo(this);
        list.clear();
        list.addAll(newList);
    }

    public int getMaxCount() {
        return maxCount;
    }

    public void setMaxCount(int maxCount) {
        this.maxCount = maxCount;
    }

    public int getCount() {
        return count;
    }

    public void setCount(int count) {
        this.count = count;
    }

    public String getLabel() {
        return label;
    }

    public void setLabel(String label) {
        this.label = label;
    }

    protected abstract boolean areItemsSame(T oldItems, T newItems);

    protected abstract boolean areContentsSame(T oldItems, T newItems);

    public static class ViewHolder extends RecyclerView.ViewHolder {

        private ViewDataBinding binding;

        public ViewHolder(ViewDataBinding viewDataBinding) {
            super(viewDataBinding.getRoot());
            binding = viewDataBinding;
        }

        public void bind(Object item, Object viewModel) {
            binding.setVariable(BR.item, item);
            binding.setVariable(BR.viewModel, viewModel);
            binding.executePendingBindings();
        }
    }

    public class DiffUtilCallback extends DiffUtil.Callback {

        private List<T> oldList;
        private List<T> newList;

        public DiffUtilCallback(List<T> oldList, List<T> newList) {
            this.oldList = oldList;
            this.newList = newList;
        }

        @Override
        public int getOldListSize() {
            return oldList == null ? 0 : oldList.size();
        }

        @Override
        public int getNewListSize() {
            return newList == null ? 0 : newList.size();
        }

        @Override
        public boolean areItemsTheSame(int oldItemPosition, int newItemPosition) {
            return areItemsSame(oldList.get(oldItemPosition), newList.get(newItemPosition));
        }

        @Override
        public boolean areContentsTheSame(int oldItemPosition, int newItemPosition) {
            return areContentsSame(oldList.get(oldItemPosition), newList.get(newItemPosition));
        }
    }
}

如果需要增加特殊的bind可在onBindViewHolder中用if/else,條件用getItemViewType(position) == R.layout.???(your own layout),比對layout來源判斷。然後在ViewHolder新增bind function。
例如:
在onBindViewHolder

if (getItemViewType(position) == R.layout.recyclerview_item_label_with_time) {
            if (list.get(position) != null) {
                holder.bindTimeLabel(list.get(position).getDate());
            }
        }else {
            holder.bind(list.get(position), viewModel);
        }

在ViewHoder

public void bind(Object item, Object viewModel) {
            binding.setVariable(BR.item, item);
            binding.setVariable(BR.viewModel, viewModel);
            binding.executePendingBindings();
        }
public void bindTimeLabel(String label) {
            binding.setVariable(BR.label, label);
            binding.executePendingBindings();
        }

Step2.In CustomBindingAdapter.class

在這裡我們必須實作GenericRecyclerViewAdapterWithViewModel中兩個abstract function『areItemsSame&areContentsSame』,作用在於我們在recyclerView的viewmodel中data更新時refresh recyclerView。

@BindingAdapter(value = {"items", "viewModel", "maxCount", "orientation", "hasFixedSize"}, requireAll = false)
    public static <T extends RecyclerViewBase, V extends BaseViewModel> void setRecyclerViewAdapter(RecyclerView recyclerView, List<T> items, V viewModel, int count, int orientation, boolean hasFixedSize) {
        GenericRecyclerViewAdapterWithViewModel<T, V> adapter = null;
        if (recyclerView.getAdapter() == null && items != null) {
            adapter = new GenericRecyclerViewAdapterWithViewModel<T, V>(viewModel) {
                @Override
                protected boolean areItemsSame(T oldItems, T newItems) {
                    return viewModel.areSameItems(oldItems, newItems);
                }

                @Override
                protected boolean areContentsSame(T oldItems, T newItems) {
                    return viewModel.areSameContents(oldItems, newItems);
                }
            };

            recyclerView.setAdapter(adapter);
            recyclerView.setHasFixedSize(hasFixedSize);
            if (orientation != -1) {
                recyclerView.setLayoutManager(new WrapContentLinearLayoutManager(recyclerView.getContext(), orientation, false));
            } else {
                FlexboxLayoutManager manager = new FlexboxLayoutManager(recyclerView.getContext());
                manager.setFlexDirection(FlexDirection.ROW);
                manager.setFlexWrap(FlexWrap.WRAP);
                recyclerView.setLayoutManager(manager);
            }
        } else if (recyclerView.getAdapter() instanceof GenericRecyclerViewAdapterWithViewModel) {
            adapter = (GenericRecyclerViewAdapterWithViewModel) recyclerView.getAdapter();
        }

        if (adapter != null && items != null) {
            if (count > 0)
                adapter.setMaxCount(Math.min(items.size(), count));
            adapter.setDiffItems(items);
        }
    }

Step3.在xml中使用

建立好GenericRecyclerViewAdapterWithViewModel和BindingAdapter,就可以在XML中使用啦

<androidx.recyclerview.widget.RecyclerView
            android:id="@+id/recyclerview_list"
            android:layout_width="match_parent"
            android:layout_height="match_parent"
            android:layout_below="@+id/imageView_banner"
            android:background="@color/house_reservation_default_text"
            android:scrollbars="none"
            android:visibility="@{viewModel.listValue.size() &lt;= 0 ? View.GONE : View.VISIBLE}"
            app:hasFixedSize="@{true}"
            app:items="@{viewModel.listValue}"
            app:orientation="@{LinearLayoutManager.VERTICAL}"
            app:viewModel="@{viewModel}" />

我們可以在xml中綁定data『app:items』、RecyclerView方向(直或橫)和viewModel

Step4.建立interface

這是用在BaseViewModel的interface,配合前面GenericRecyclerViewAdapterWithViewModel比對在這個recyclerView的item

public interface RecyclerViewItemHandler {

    boolean areSameItems(Object oldObject, Object newObject);

    boolean areSameContents(Object oldObject, Object newObject);

}

your BaseViewModel


public abstract class BaseViewModel extends AndroidViewModel implements RecyclerViewItemHandler {
   public BaseViewModel(@NonNull Application application) {
        super(application);
    }
    @Override
    public boolean areSameItems(Object oldObject, Object newObject) {
        return sameItems(oldObject, newObject);
    }

    @Override
    public boolean areSameContents(Object oldObject, Object newObject) {
        return sameContents(oldObject, newObject);
    }

    protected abstract boolean sameItems(Object oldObject, Object newObject);

    protected abstract boolean sameContents(Object oldObject, Object newObject);
}

在需要用到recyclerView該頁面中的viewModel都需要extends BaseViewModel

Step5.viewModel

在Fragment or Activity的viewModel中,需要extends BaseViewModel,實作sameItems&sameContents

In your fragment or activity viewModel:

@Override
    protected boolean sameItems(Object oldObject, Object newObject) {
        CommonObject oldSearch = (CommonObject) oldObject;
        CommonObject newSearch = (CommonObject) newObject;
        return oldSearch.getData().getId() == newSearch.getData().getId();
    }

    @Override
    protected boolean sameContents(Object oldObject, Object newObject) {
        CommonObject oldSearch = (CommonObject) oldObject;
        CommonObject newSearch = (CommonObject) newObject;
        return oldSearch.getData().getId() == newSearch.getData().getId() && oldSearch.isVisited() == newSearch.isVisited();//對比item中各種資料
    }

從api fetch到的data,可以直接set進listValue,這裡可以給大家看fetch api function例子。

In your fragment or activity viewModel:

//repository是自定義用於呼叫api的class
repository.fetchData(填入你呼叫api時需要的參數).observeForever(dataServerErrorPair -> { //若使用observeForever記得要在destroy時刪除
            if (dataServerErrorPair != null) {
                if (dataServerErrorPair.first != null) {//我這裡的pair第一個是api的結果,第二個是當api error會設置(不為空)
                    for (DataObject object : dataServerErrorPair.first.getResults()) {
                        CommonObject object = new CommonObject();
                        object.setRecyclerItemType(R.layout.你的recyclerViewItem layout名稱);
                        object.setData(object);
                        list.add(object);
                    }
                    listValue.setValue(list);
                } else if (dataServerErrorPair.second != null) {
                    //TODO 如果api error時需做的動作
                }
            }
        });

之後所有recyclerView都可以這樣做了!超級方便!直接binding不需要一大堆adapter!推薦給所有朋友~


#BindingAdapter #recyclerView







Related Posts

VUE3 課前章節-JS 必備觀念-箭頭函式

VUE3 課前章節-JS 必備觀念-箭頭函式

Fetch 與 Promise (二):錯誤處理

Fetch 與 Promise (二):錯誤處理

React-[核心篇]- React渲染功能在後台是怎麼運作的?

React-[核心篇]- React渲染功能在後台是怎麼運作的?


Comments