Android Data Binding. Observable поля. Двосторонній біндинг.

Коли ми використовуємо біндинг для звичайного Java-об'єкта, то екран не буде автоматично змінюватися при зміні значень у цьому об'єкті.

Тобто ви передаєте об'єкт Employee у біндинг, а потім можете скільки завгодно змінювати в ньому поля і нічого на екрані змінюватися не буде. Вам треба буде знову руками передати змінений об'єкт у біндинг або викликати у біндингу метод invalidateAll, тоді екран відобразить актуальні дані.

У першому уроці з Data Binding я згадував, що є можливість зробити так, щоб біндинг сам моніторив значення полів і оновлював екран, щойно відбулися якісь зміни. Для цього треба використовувати механізми Observable.

Observable поля

Зробимо кілька Observable полів у класі Employee:

public class Employee {

   public Employee(long id, String name, String address, int salary) {
       this.id = id;
       this.name.set(name);
       this.address = address;
       this.salary.set(salary);
   }

   public long id;

   public ObservableField<String> name = new ObservableField<>();

   public String address;

   public ObservableInt salary = new ObservableInt();

}

Для Java примітивів є готові Observable поля: ObservableInt, ObservableFloat тощо. Для решти використовуємо ObservableField із зазначенням типу. Щоб присвоїти такому полю значення, використовуємо метод set.

Поля name і salary робимо Observable. Їх ми будемо використовувати в біндингу.

layout файл має звичайний вигляд, у ньому нічого змінювати не треба:

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

   <data>
       <variable
           name="employee"
           type="ru.startandroid.application.data.Employee"/>
   </data>

   <LinearLayout
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:orientation="vertical">

       <TextView
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{employee.name}" />

       <TextView
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{String.valueOf(employee.salary)}"/>
   </LinearLayout>

</layout>

Тепер, передавши в біндинг об'єкт Employee, ви зможете змінювати значення його полів:

employee.name.set("Mark");
employee.salary.set(20000);

А біндінг сам відстежить ці зміни і оновить екран.

Для колекцій є класи ObservableArrayMap і ObservableArrayList.

Біндинг працюватиме, навіть якщо передавати значення в Observable поля не в UI потоці.

Розглянемо ще один можливий сценарій використання ObservableField. Його можна використовувати не тільки з окремими полями об'єкта, а й з цілим об'єктом.

Є клас Employee:

public class Employee {

   public Employee(long id, String name, String address, int salary) {
       this.id = id;
       this.name = name;
       this.address = address;
       this.salary = salary;
   }

   public long id;

   public String name;

   public String address;

   public int salary;

}

Цілком може бути ситуація, коли нам у ViewModel (або в презентер) періодично «прилітає» з репозиторію новий об'єкт Employee і його треба відобразити в View.

У цьому разі в ViewModel створюємо поле ObservableField<Employee>:

public class ViewModel {

   public ObservableField<Employee> employee = new ObservableField<>();

   ...
}

У це поле методом employee.set() репозиторій поміщатиме новий Employee.

У layout як змінну використовуємо ViewModel.

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

   <data>
       <variable
           name="model"
           type="ru.startandroid.application.data.ViewModel"/>
   </data>

   <LinearLayout
       android:layout_width="match_parent"
       android:layout_height="match_parent"
       android:orientation="vertical">

       <TextView
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{model.employee.name}" />

       <TextView
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{model.employee.address}"/>
   </LinearLayout>

</layout>

Дістаємо з model об'єкт employee і використовуємо його поля в біндингу.

Тепер під час оновлення значення в ObservableField<Employee> будуть змінені й поля в View без будь-яких додаткових дій з нашого боку.

BaseObservable

Є ще один спосіб увімкнути автобіндинг для Java-об'єкта.

Робиться це наслідуванням BaseObservable:

public class Employee extends BaseObservable {

   private long id;
   private String name = "";
   private int salary = 0;

   public Employee(long id, String name, int salary) {
       this.id = id;
       setName(name);
       setSalary(salary);
   }

   @Bindable
   public String getName() {
       return name;
   }

   public void setName(String name) {
       this.name = name;
       notifyPropertyChanged(BR.name);
   }

   @Bindable
   public int getSalary() {
       return salary;
   }

   public void setSalary(int salary) {
       this.salary = salary;
       notifyPropertyChanged(BR.salary);
   }
}

Поле id я залишив звичайним. А поля name і salary будуть відстежуватися біндингом. Для цього треба позначити get-методи анотацією @Bindable, а в set-методах викликати notifyPropertyChanged метод, який і буде повідомляти біндинг про зміни значення поля.

У layout все буде як зазвичай.

Двосторонній біндинг

Біндинг може працювати в обидва боки. Тобто він буде не тільки передавати дані в View, а й отримувати їх звідти.

Розглянемо на прикладі пари полів у Employee:

public class Employee {

   public String name = "";
   public boolean enabled = true;

}

Поле name і статус enabled. Налаштуємо біндинг цих полів в EditText і CheckBox.

При цьому зробимо так, щоб біндинг працював в обидва боки. Для цього треба в рядку біндингу додати символ = між @ і {...}

<EditText
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@={employee.name}"/>

<CheckBox
   android:text="enabled"
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:checked="@={employee.enabled}"/>

Тепер при зміні тексту в EditText, біндинг передаватиме нове значення в employee.name. А під час увімкнення\вимкнення чекбокса, біндинг передаватиме поточний стан цього чекбокса в поле employee.enabled.

Тобто зміни вмісту View будуть відображені в Employee об'єкті, який ми передавали в біндинг. Якщо необхідно, можна використовувати і Observable поля. З ними це теж буде працювати.

До речі, якщо після передачі в біндинг ви ніде не зберігаєте у себе об'єкт employee, то ви завжди можете отримати його назад методом binding.getEmployee().