昨天因為建立功能要做的事比較多,有刻意把新的東西都避開,所以其中的RecyclerView Adapter我們只用了一半的Data Binding,今天就把這邊從頭說明並做完。
在Adapter中使用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分別設置。
首先也是在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的意思是我們可以自創一個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);
}
那麼如果除了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