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

What Type of Laser Engraving Machine Should be Used for Stainless Steel Engraving?

What Type of Laser Engraving Machine Should be Used for Stainless Steel Engraving?

Kotlin 練功場 - Android 組

Kotlin 練功場 - Android 組

除錯Debug

除錯Debug


Comments