iT邦幫忙

2018 iT 邦幫忙鐵人賽
DAY 6
0
Software Development

Android Architecture系列 第 6

Data Binding with RecyclerView & Custom setter

昨天因為建立功能要做的事比較多,有刻意把新的東西都避開,所以其中的RecyclerView Adapter我們只用了一半的Data Binding,今天就把這邊從頭說明並做完。

在Adapter中使用Data Binding需修改三個部分:

  1. ViewHolder
  2. onCreateViewHolder
  3. onBindViewHolder

Before Data Binding

先看未使用Data Binding的原樣:

public class RepoAdapter extends RecyclerView.Adapter<RepoAdapter.RepoViewHolder> {

    ...

    class RepoViewHolder extends RecyclerView.ViewHolder{

        private TextView ...;

        RepoViewHolder(View itemView) {
            super(itemView);
            // ...findViewById
        }
    }

    @Override
    public RepoViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        View view = LayoutInflater.from(parent.getContext())
            .inflate(R.layout.repo_item, parent, false);
        return new RepoViewHolder(view);
    }

    @Override
    public void onBindViewHolder(RepoViewHolder holder, int position) {
        Repo repo = items.get(position);
        holder.name.setText(repo.fullName);
        ...
    }

    ...
}

就是很一般的Adapter寫法,在onCreateViewHolder中inflate layout,onBindViewHolder中將每個view分別設置。

Apply Data Binding

首先也是在repo_item.xml中加入<data>

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <variable
            name="repo"
            type="ivankuo.com.itbon2018.data.model.Repo" />

    </data>

    <android.support.v7.widget.CardView
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        app:cardUseCompatPadding="true">

        <android.support.constraint.ConstraintLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content">

            <ImageView
                android:id="@+id/ownerAvatar"
                android:layout_width="48dp"
                android:layout_height="48dp"
                ... />

            <TextView
                android:id="@+id/name"
                android:layout_width="224dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="@dimen/default_margin"
                android:layout_marginTop="16dp"
                android:text="@{repo.fullName}"
                ... />

            <TextView
                android:id="@+id/desc"
                android:layout_width="0dp"
                android:layout_height="wrap_content"
                android:layout_marginStart="@dimen/default_margin"
                android:layout_marginTop="8dp"
                android:text="@{repo.description}"
                ... />

            <TextView
                android:id="@+id/stars"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginEnd="@dimen/default_margin"
                android:layout_marginTop="@dimen/default_margin"
                android:drawableEnd="@android:drawable/btn_star"
                android:gravity="center"
                android:text="@{String.valueOf(repo.stars)}"
                ... />

        </android.support.constraint.ConstraintLayout>
    </android.support.v7.widget.CardView>
</layout>

可以注意的是stars的text我們用了String.valueOf(),因為repo.start是一個int,直接設置的話系統會以為是要找string resource,藉著Data Binding支援Expression Language讓我們可以直接用String.valueOf()解決,其他支援的expression請參考官方文件

Java檔修改如下:

public class RepoAdapter extends RecyclerView.Adapter<RepoAdapter.RepoViewHolder> {

    ...

    class RepoViewHolder extends RecyclerView.ViewHolder{

        private final RepoItemBinding binding;

        RepoViewHolder(RepoItemBinding binding) {
            super(binding.getRoot());
            this.binding = binding;
        }

        void bind(Repo repo) {
            binding.setRepo(repo);
            binding.executePendingBindings();
        }
    }

    @Override
    public RepoViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
        LayoutInflater layoutInflater = LayoutInflater.from(parent.getContext());
        RepoItemBinding binding = RepoItemBinding.inflate(layoutInflater, parent, false);
        return new RepoViewHolder(binding);
    }

    @Override
    public void onBindViewHolder(RepoViewHolder holder, int position) {
        Repo repo = items.get(position);
        holder.bind(repo);
        Glide.with(holder.binding.getRoot().getContext())
                .load(repo.owner.avatarUrl)
                .into(holder.binding.ownerAvatar);
    }

    ...
}

RepoViewHolder的參數我們改成RepoItemBinding,onCreateViewHolder的inflate對象由view改成binding,最後是onBindViewHolder中原本個別設置每個view改成使用RepoViewHolder中的bind(repo)來設置。

需特別注意的是bind(repo)當中的executePendingBindings(),這會讓binding立即更新畫面,所以每次onBindViewHolder後都會立刻更新,以免快速滑動等情況導致UI顯示錯誤。

然而,還有一個讀圖片的Glide要處理,現在這樣雖然也可以運作,但程式不好看。如果能用Data Binding在ImageView上直接設置它讀圖就更棒了,我們繼續往下做。

Custom setter

Custom setter的意思是我們可以自創一個binding方法,例如我們想要一個boolean值決定元件是否顯示:

@BindingAdapter("visibleGone")
public static void showHide(View view, boolean show) {
    view.setVisibility(show ? View.VISIBLE : View.GONE);
}

建立一個static method並標註為BindingAdapter,showHide(view, boolean)的第一個參數view即為使用這個method的view本身,第二個參數boolean就是我們待會可以自由設置的部分。

BindingAdapter括號內的visibleGone就是在xml中的方法名稱,所以在xml中我們就這樣用:

<ProgressBar
    android:id="@+id/progressBar"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    app:visibleGone="@{viewModel.isLoading}"
    ... />

這樣就完成讓progressBar在isLoading為true時顯示,否則隱藏。

OK,如法炮製我們就可以寫好用Glide讀取圖片的setter,目標是bind在ImageView上並給予url讓Glide讀取:

@BindingAdapter("imageUrl")
public static void bindImage(ImageView imageView, String url) {
    Context context = imageView.getContext();
    Glide.with(context)
            .load(url)
            .into(imageView);
}

接著於repo_item.xml中的ImageView使用

<?xml version="1.0" encoding="utf-8"?>
<ImageView
    android:id="@+id/ownerAvatar"
    android:layout_width="48dp"
    android:layout_height="48dp"
    android:layout_marginStart="8dp"
    android:scaleType="fitCenter"
    app:imageUrl="@{repo.owner.avatarUrl}"
    ... />

這樣RepoAdapter中的onBindViewHolder就可以把Glide那段拿掉了

@Override
public void onBindViewHolder(RepoViewHolder holder, int position) {
    Repo repo = items.get(position);
    holder.bind(repo);
}

Multiple parameters

那麼如果除了Url還有其他參數要設置,例如等待讀取時的drawable place holder,可以這樣寫:

@BindingAdapter({"imageUrl", "holder"})
public static void bindImage(ImageView imageView, String url, Drawable holder) {
    Context context = imageView.getContext();
    Glide.with(context)
            .placeHolder(holder)
            .load(url)
            .into(imageView);
}

BindingAdapter()中增加方法名稱holder,並在bindImage中也增加Drawable參數

<ImageView
    android:id="@+id/ownerAvatar"
    android:layout_width="48dp"
    android:layout_height="48dp"
    android:layout_marginStart="8dp"
    android:scaleType="fitCenter"
    app:imageUrl="@{repo.owner.avatarUrl}"
    app:holder="@{some drawable}"
    ... />

xml中直接使用新增的方法holder來指定drawable就可以了。

簡短的說,當有多個參數時就先在BindingAdapter寫好名稱,注意要用大括號包起來,接著在method中設置參數類型,就可以在xml中使用了。

multiple parameters我們目前還沒有實際需求所以在我的code裡沒有,有需要的話請再參考官方文件的說明。


GitHub source code:
https://github.com/IvanBean/ITBon2018/tree/day06-data-binding-with-recyclerview

Reference:
Data Binding Library
Android Data Binding: RecyclerView


上一篇
連線至GitHub API取得資料
下一篇
Transform LiveData
系列文
Android Architecture30
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言