在將舊的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 && viewModel.mode != constants.MODE_DEFAULT && 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() <= 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!推薦給所有朋友~