Android Data Binding. Код у layout. Доступ до View

Продовжуємо говорити про DataBinding. Ми вже розглянули, як можна поміщати значення з об'єктів у TextView. Але біндинг цим не обмежується і дає нам можливість писати код прямо в layout.

Давайте розглянемо приклади, коли це може знадобитися. Є клас 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;
}

Ми хочемо виводити на екран ім'я, адресу і зарплату.

Екран буде таким:

<?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="@{employee.address}" />
 
       <TextView
           android:layout_width="wrap_content"
           android:layout_height="wrap_content"
           android:text="@{employee.salary}" />
 
   </LinearLayout>
 
</layout>

А виклик біндингу таким:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
 
   Employee employee = new Employee(1, "John Smith", "London", 10000);
 
   MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
   binding.setEmployee(employee);
}

Нічого незвичайного. Усе так само, як і в минулому уроці.

Але під час запуску отримаємо помилку: android.content.res.Resources$NotFoundException: String resource ID #0x2710

Так сталося, тому що біндінг спробував відобразити поле salary в TextView. Він просто виконав код setText(employee.salary). І оскільки salary у нас має тип int, то TextView вирішив, що йому передають ідентифікатор рядкового ресурсу. І, звісно, він не знайшов такий рядок у strings.xml.

Це досить часта помилка, що виникає. І в коді ми зазвичай вирішуємо її за допомогою String.valueOf():

textView.setText(String.valueOf(employee.salary));

Біндінг дає змогу зробити нам те саме прямо в layout:

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

Тобто всередині @{ ... } ми можемо писати найпростіший код і він буде виконаний.

Запустивши додаток, ми побачимо зарплату в TextView.

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

Відображення в одному TextView відразу двох полів Employee

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

Зверніть увагу на лапки. Оскільки нам потрібні подвійні лапки, щоб додати кому між name і address, то весь цей код ми поміщаємо в одинарні лапки.

Видимість View залежно від значення поля

<TextView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@{employee.address}"
   android:visibility="@{TextUtils.isEmpty(employee.address) ? View.GONE : View.VISIBLE}"/>

Адреса буде відображена, тільки якщо вона не порожня. А за порожньої адреси видимість цього TextView буде GONE.

Зверніть увагу, що ми тут використовуємо класи TextUtils і View. Якщо зараз спробувати запустити додаток, то ми отримаємо таку помилку: Identifiers must have user defined types from the XML file. TextUtils is missing it

Біндінг каже, що не знає нічого про TextUtils. Нам треба додати його в import. Робиться це в секції data.

<data>
   <import type="android.view.View"/>
   <import type="android.text.TextUtils"/>
 
   <variable .../>
 
</data>

Тепер біндінг знає, які класи ми маємо на увазі

Тобто це аналогічно тому, як у java коді ви пишете:

import android.text.TextUtils;
import android.view.View;

і після цього можете використовувати ці класи.

Використання resources значення: strings, dimens тощо.

<TextView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@{TextUtils.isEmpty(employee.address) ? @string/empty_address : employee.address}"/>

Якщо адреса порожня, то показуємо заглушку зі strings.

У прикладах вище ми використовували в layout тільки одну змінну - Employee. Давайте додамо ще одну.

Створимо новий клас, який міститиме інформацію про відділ

public class Department {
 
   public Department(long id, String name) {
       this.id = id;
       this.name = name;
   }
 
   public long id;
 
   public String name;
}

Додамо змінну типу Department у layout

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

І використовуємо її:

<TextView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text='@{employee.name + "(" + department.name + ")"}' />

В одному TextView показуємо дані з двох змінних.

Код виконання біндингу матиме такий вигляд:

@Override
protected void onCreate(Bundle savedInstanceState) {
   super.onCreate(savedInstanceState);
 
   Employee employee = new Employee(1, "John Smith", "", 10000);
   Department department = new Department(100, "IT");
 
   MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
   binding.setEmployee(employee);
   binding.setDepartment(department);
}

Для змінної Department у класі MainActivityBinding було згенеровано окремий метод setDepartment.

Можна трохи ускладнити логіку і показувати назву відділу, тільки якщо ми передали об'єкт Department у біндинг:

<TextView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text='@{employee.name + (department == null ? "" : " (" + department.name + ")") }' />

Біндінг вміє працювати і з колекціями. Наприклад, якщо в Employee є поле зі списком хобі:

public List<String> hobbies;

то, в layout ми можемо відобразити перше хобі зі списку таким чином:

<TextView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@{employee.hobbies[0]}" />

Якщо нам необхідно використовувати список як окрему змінну в layout, то variable матиме такий вигляд:

<variable
   name="hobbies"
   type="java.util.List&lt;String&gt;" />

Обмеження XML не дають змоги просто так використовувати символи < і >. Тому їх доводиться замінювати спецсимволами < і >.

Той самий опис змінної, але List винесено в імпорт:

<import type="java.util.List"/>
 
<variable
   name="hobbies"
   type="List&lt;String&gt;" />

Map колекції описуються аналогічно:

<import type="java.util.Map"/>
 
<variable
   name="map"
   type="Map&lt;String, String&gt;"/>

Отримання значення за ключем:

<TextView
   android:layout_width="wrap_content"
   android:layout_height="wrap_content"
   android:text="@{map[`key`]}" />

В офіційній документації ви можете подивитися повний список можливостей написання коду в layout.

View

Якщо нам потрібні будь-які View з нашого layout, то їх можна отримати з біндингу. Для цього необхідно, щоб View мало id.

Наприклад, якщо в layout є поле:

<TextView
    android:id="@+id/name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content" />

то ми можемо отримати його з біндингу так:

TextView textViewName = binding.name;

Також можна отримати кореневе View методом getRoot:

View rootView = binding.getRoot();