Android Data Binding in RecyclerView

ac6364b41862460ab3b24dfe92e5e33e.png

На Google IO 2015 анонсировали новую библиотеку Data Binding Library. Основная ее задача — вынесения взаимодействия модели и View в xml-файлы. Она значительно упрощает написание кода и избавляет от необходимости использования методов findByViewId(), добавления ссылок на view-элементы внутри Activity/Fragment’ов. Также она позволяет использовать кастомные атрибуты, привязывая их к статическим методам. Поскольку статьей просто по Data Binding уже достаточно, но по его использованию в RecycleView всего ничего, восполним этот пробел.

Настройка
Для начала заходим в файл build.gradle, который лежит в корневом каталоге проекта. В блоке dependencies выставляем:

buildscript {
   repositories {
       jcenter()
   }
   dependencies {
       classpath "com.android.tools.build:gradle:1.3.0"
       classpath "com.android.databinding:dataBinder:1.0-rc1"
   }
}

allprojects {
   repositories {
       jcenter()
   }
}

Далее подключим Data Binding плагин к проекту. Для этого в build.gradle добавляем строчку с плагином. Также проверяем, чтобы compileSdkVersion была 23.

apply plugin: 'com.android.application'
apply plugin: 'com.android.databinding'

Биндинг
Перейдем к созданию xml-файла. Он, как обычно, создается в паке res/layoyt. В качестве корневого тега используем layout. Android Studio может подсвечивать его красным или предлагать выставить ширину и высоту, но мы ее игнорируем.

<layout xmlns:android="http://schemas.android.com/apk/res/android" 
   xmlns:app="http://schemas.android.com/apk/res-auto"> 

   <data> 
   </data> 

   <!-- Сюда добавляем свой layout --> 

</layout>

Чтобы создался биндер-класс, который и будет привязывать модель к view, нужно привязать xml к модели. Для этого внутри тега указываем имя и путь к нашей модели. В качестве примера будет отображатьcя список фильмов.

public class Movie {
   public boolean isWatched;
   public String image;
   public String description;
   public String title;

   public Movie(boolean isWatched, String image, String description, String title) {
       this.isWatched = isWatched;
       this.image = image;
       this.description = description;
       this.title = title;
   }
}

<layout xmlns:android="http://schemas.android.com/apk/res/android" 
   xmlns:app="http://schemas.android.com/apk/res-auto"> 

  <data>
   <variable
       name="movie"
       type="com.example.databinding.Movie" />
  </data>

 <!-- Сюда добавляем свой layout --> 

</layout>

Осталось добавить свой layout и привязать к нему модель. Пусть у каждого фильма будет картинка, заголовок и краткое описание. Чтобы указать, что поле будет считываться из модели используем “@{*какое поле из модели использовать*}”.

<?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">

   <data>
       <variable
           name="movie"
           type="com.example.databinding.Movie" />
   </data>

   <android.support.v7.widget.CardView
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:orientation="vertical"
       android:layout_margin="8dp">

       <RelativeLayout
           android:id="@+id/relativeLayout"
           android:layout_width="match_parent"
           android:layout_height="wrap_content">

           <ImageView
               android:id="@+id/imageView"
               ...              
               app:imageUrl="@{movie.image}"/>
           <TextView
               android:id="@+id/textView"
               ...
               android:text="@{movie.title}"
               android:textAppearance="?android:attr/textAppearanceLarge" />

           <TextView
               android:id="@+id/textView2"
               ...
               android:text="@{movie.description}"
               android:textAppearance="?android:attr/textAppearanceSmall" />

       </RelativeLayout>

   </android.support.v7.widget.CardView>

</layout>

С android:text="@{movie.title}" и android:text="@{movie.description}" все понятно — просто в качестве текста будет показано соответствующее поле, но что на счет app:imageUrl="@{movie.image}"? Тут начинается реальная магия Data Binding. Вы можете добавлять сколько угодно кастомных атрибутов и даже не прописывать их в atts.xml, а аннотация @BindingAdapter() поможет вам их обработать. Ниже будет показано, как обрабатывать такие аннотации.

Перейдем к адаптеру. Напишем простой RecyclerView.Adapter. Начнем с ViewHolder. Как он выглядел раньше:

public static class MovieItemViewHolder extends RecyclerView.ViewHolder {
   private TextView title, description;
   private ImageView image;

   public ViewHolder(View v) {
       super(v);
       title = (TextView) v.findViewById(R.id.textView);
       description = (TextView) v.findViewById(R.id.textView2);
       image = (ImageView) v.findViewById(R.id.imageView);
   }
}

Как он выглядел после Butter Knife:

public static class MovieItemViewHolder extends RecyclerView.ViewHolder {
   @Bind(R.id.textView) TextView title;
   @Bind(R.id.textView2) TextView description;
   @Bind(R.id.imageView) ImageView image;

   public ViewHolder(View v) {
       super(v);
       ButterKnife.bind(v);
   }
}

Как он выглядит после DataBinding:

public class MovieItemViewHolder extends RecyclerView.ViewHolder {

   MovieItemBinding binding;

   public MovieItemViewHolder(View v) {
       super(v);
       binding = DataBindingUtil.bind(v);
   }
}

Далее нас интересуют два основных метода адаптера: onCreateViewHolder и onBindViewHolder. Созданием и биндигом будет заниматься MovieItemBinding. Он генерируется по названию xml, который мы написали выше. В данном случае файл xml назывался movie_item.xml.

@Override
public MovieItemViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
   LayoutInflater inflater = LayoutInflater.from(parent.getContext());
   MovieItemBinding binding = MovieItemBinding.inflate(inflater, parent, false);
   return new MovieItemViewHolder(binding.getRoot());
}

Теперь перейдем к onBindViewHolder, как он выглядел раньше:

@Override
public void onBindViewHolder(MovieItemViewHolder holder, int position) {
   Movie movie = Movie.ITEMS[position];
   holder.title.setText(movie.title);
   holder.description.setText(movie.description);
   Picasso.with(holder.image.getContext()).load(movie.image).into(holder.image);
}

Как он выглядит теперь:

@Override
public void onBindViewHolder(MovieItemViewHolder holder, int position) {
   Movie movie = Movie.ITEMS[position];
   holder.binding.setMovie(movie);
}

Но это еще не всё, как на счет кастомного app:imageUrl="@{movie.image}"?.. Опять же все просто: внутри адаптера делаем статический метод с аннотацией @BindingAdapter. Внутрь аннотации передаем наш аттрибут. В итоге получаем

@BindingAdapter("bind:imageUrl")
public static void loadImage(ImageView imageView, String v) {
   Picasso.with(imageView.getContext()).load(v).into(imageView);
}

На вход поступит imageView и то, что передаст модель в качестве image. Теперь все заработает.

Остальные полезности
В модели Movie была переменная isWatched. Допустим, мы хотим, чтобы у просмотренных и новых фильмов были разные обработчики на клик. С DataBinding’ом теперь это проще простого. Напишем обработчик нажатия для фильма.

public interface MovieClickHandler{
   void onNewClick(View view);
   void onWatchedClick(View view);
}

Добавим его в xml-файл в тег data.

...

   <data>
       ...
       <variable name="click" type="com.example.databinding.MovieClickHandler" />

   </data>
...
           <ImageView
            ...
           android:onClick="@{movie.isWatched ? click.onWatchedClick : click.onNewClick}"/>
         
...

Теперь в методе адаптера onBindViewHolder можно засетить наш лисенер. Как и в случае с биндером, название метода генерируется соотвественному названию переменной в xml-файле.

public void onBindViewHolder(MovieItemViewHolder holder, int position) {
   Movie movie = Movie.ITEMS[position];
   holder.binding.setMovie(movie);
   holder.binding.setClick(new MovieClickHandler() {
       @Override
       public void onWatchedClick(View view) {

       }

       @Override
       public void onOldClick(View view) {

       }
   });
}

Пусть по загрузке картинка у просмотренных фильмов будет черно-белая. Для преобразование картинки добавим новый атрибут.

<ImageView
   ...
   app:filter='@{movie.isWatched ? "grey" : null}'
   .../>

В адаптере через @BindingAdapter реализуем обработку

@BindingAdapter("bind:filter")
public static void applyFilter(ImageView imageView, String v) {
   imageView.setColorFilter(null);
   if("grey".equals(v)){
       ColorMatrix matrix = new ColorMatrix();
       matrix.setSaturation(0);
       ColorMatrixColorFilter cf = new ColorMatrixColorFilter(matrix);
       imageView.setColorFilter(cf);
   }
}

Также очень удобно использовать стабовые значения, если одно из полей пустое.

<TextView
   ...
   android:text='@{movie.title ?? "unknown"}'
   ... />

Стоит также отметить, что внутри MovieItemBinding содержатся ссылки на все view, у которых есть ID в xml-файле.

9397f6809f7f490e96de7e76f6d5fd50.png

Итог
Библиотека очень упрощает работу с RecycleView, количество кода при написании теперь уменьшается в разы, при этом никаких if/else для колбеков и данных. С JavaRX можно еще больше упростить обновление данных, пока правда оно работает только в одну сторону: при изменении данных обновляется UI, но не наоборот.

Полезные ссылки:

Тестовый проект.
Официальная документация.
Быстрый старт Data Binding в Android.

© Habrahabr.ru