
Кожного разу, коли ви дивитесь на ці численні рядки коду з findViewById та тернарними операціями з видимістю, вам хочеться, щоб Ктулху нарешті з'явився на Землі, щоб ви більше не бачили цього, чи не так? Але повірте, є спосіб. І ми покажемо його вам далі.
План
- Що це за звір
- Інтеграція
- Перший досвід
- Розваги тільки починаються
- Кодуємо з задоволенням
- І це ще не все!
- Висновки
- Нічого ідеального
- Висновок
1. Що це за звір
Ми всі знаємо, що найнудніша частина роботи розробника — це описувати поведінку UI в залежності від логіки зміни даних. Численні години були витрачені на написання та підтримку тривіального і майже безглуздого коду, що займає десятки або навіть сотні рядків у майже будь-якій активності чи фрагменті. Не кажучи вже про проблеми читабельності, які вони викликають: щоб пробратися через стіни коду або знайти в ньому помилки, потрібно бути невтомним мандрівником.
Отже, послання зрозуміле: нам потрібен спосіб автоматизувати цей процес. Бібліотека DataBinding робить саме це — вона допомагає помітно зменшити обсяг коду для логіки застосунку, зв'язуючи його з інтерфейсом.
2. Інтеграція
Щоб показати вам потенціал цього рішення, ми напишемо невеликий застосунок з профілем користувача. Спочатку давайте підключимо цей чудовий інструмент до нашого проєкту.
Для інтеграції прив'язки нам потрібно використовувати Gradle 1.5.0 або новішу версію. Давайте оновимо файл build.gradle проєкту, додавши наступний рядок:
buildscript { ... dependencies{ classpath 'com.android.tools.build:gradle:1.5.0'}}
Тепер додайте dataBinding у модульний build.gradle:
android { ... dataBinding{ enabled =true}}
Синхронізуйте Gradle — тепер DataBinding доступний у нашому проєкті :)
3. Перший досвід
Час перейти до нашого зразкового застосунку. Ми почнемо з малого: створимо активність, що відображає основну інформацію про користувача (ім'я, прізвище, статус та індикатор онлайн).
Це проста задача, тож давайте зробимо це! Створіть модель User з необхідними полями:
publicclass User { /* конструктор */ privateString name;privateString surname;privateString status;privateboolean isOnline; /* геттери та сеттери */}
Тепер давайте створимо макет. Ми будемо використовувати лише простий TextView для відображення імені та статусу, а також View для індикатора:
<layout xmlns:android="http://schemas.android.com/apk/res/android"><data><variable name="user" type="com.example.models.User"></variable></data> <relativelayout android:layout_width="match_parent" android:layout_height="match_parent"><view android:layout_width="@dimen/indicator_size" android:layout_height="@dimen/indicator_size" android:background="@{user.isOnline ? @drawable/shape_online : @drawable/shape_offline}" ...></view><textview android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.name + ' ' + user.surname}" ...></textview><textview android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.status}" ...></textview></relativelayout></layout>
Ось пояснення для тих незвичних тегів у макеті:
<layout>
— ми поміщаємо кореневий елемент між цими тегами, щоб повідомити компілятору, що файл макета стосується прив'язки. Варто зазначити, що його завжди слід ставити в кореневий елемент.<data>
завжди розміщується в макеті і слугує обгорткою для змінних, які використовуються в макеті.<variable>
містить ім'я та тип, що описують ім'я змінної та її повне ім'я відповідно (включаючи ім'я пакету).@{}
контейнер, який використовується для опису виразу. Наприклад, об'єднання імені та прізвища в один рядок або просте відображення поля. Ми повернемося до виразів пізніше.
Тепер давайте подивимося, як виглядатиме наш клас активності:
@Override protectedvoid onCreate(Bundle savedInstanceState){super.onCreate(savedInstanceState); ActivityMainBinding binding = DataBindingUtil.setContentView(this, LAYOUT); binding.setUser(Demo.getUser());}
Тут ми використовуємо DataBindingUtil для ініціалізації макета та отримання ActivityMainBinding, який був згенерований для нас у відповідь (за замовчуванням, клас прив'язки генерується на основі назви файлу макета, перетвореного в CamelCase з додаванням суфікса “Binding”). ActivityMainBinding використовується для збереження посилань на елементи нашого інтерфейсу. Щоб заповнити їх даними, потрібно вказати об'єкт користувача за допомогою методу setUser (назва цього об'єкта залежить від назви змінної в полі назви блоку змінних).
Давайте поглянемо на результат:
Так, цього коду достатньо, щоб відобразити потрібні дані :) Але...
4. Розваги тільки починаються
DataBinding має власну мову виразів для файлів макетів. Вона відповідає Java-виразам і має досить вражаючі можливості. Нижче ви можете побачити список усіх доступних операторів:
- математичні оператори;
- конкатенація рядків;
- логічні оператори;
- бінарні оператори;
- унарні оператори;
- зсув бітів;
- оператори порівняння;
- instanceof;
- групування;
- літерали: рядкові, числові, символічні, null;
- приведення типів;
- виклики методів та доступ до полів;
- доступ до елементів масиву та списку;
- тернарний оператор “?:”.
Але щоб уникнути перетворення файлу макету на збірку логіки програми, кілька операторів не підтримуються:
- this;
- super;
- new;
- явне виконання типізованих методів.
Оператор злиття null “??”, який дозволяє виконувати більшість перевірок на null, також варто згадати. Ось як він виглядає:
android:text="@{user.status ?? user.lastSeen}" android:text="@{user.status != null ? user.status : user.lastSeen}"
Обидва рядки коду виконують одну й ту ж задачу. Це ж чудово? :)
Код, згенерований бібліотекою DataBinding, також автоматично перевіряє всі об'єкти та запобігає NullPointerException. Наприклад, якщо в виразі @{user.status} поле статусу дорівнює null, його значення буде рівним значенню за замовчуванням, тобто “null”. Цей принцип також працює для примітивних типів даних.
<data>
тег, який ми вже знаємо, має ще одну властивість — ви можете використовувати його для імпорту типів, необхідних у вашій роботі. Крім того, ви можете скоротити їх для зручності та економії місця, використовуючи псевдоніми.
<data><import type="android.view.View" alias="v"></import></data><view android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="@{user.isNinja ? v.GONE : v.VISIBLE}"></view>
Робота з ресурсами безумовно заслуговує на згадку: майже всі ресурси можна викликати безпосередньо та поєднувати з логічними операторами. Чудово! Приклад з індикатором онлайн демонструє одну з можливих реалізацій: вставка необхідного Drawable в атрибут android:background залежно від того, чи користувач онлайн чи ні. А тепер просто уявіть гнучкість макету, використовуючи всі можливі посилання:
Тип | Звичайне посилання | Посилання у виразі |
String[] | @array | @stringArray |
int[] | @array | @intArray |
TypedArray | @array | @typedArray |
Animator | @animator | @animator |
StateListAnimator | @animator | @stateListAnimator |
Color int | @color | @color |
ColorStateList | @color | @colorStateList |
Окрім цього, @BindingAdapter також заслуговує на увагу. Ви можете використовувати його для переоприділення поведінки існуючих атрибутів та створення власних атрибутів, не думаючи про attrs.xml.
Щоб це зробити, потрібно створити публічний статичний метод, який приймає на вхід View необхідного типу та значення, яке ми вказуємо в макеті. Сам метод повинен мати анотацію @BindingAdapter, а в його тілі потрібно вказати рядок з назвою атрибута.
Наприклад, ось адаптер, який дозволяє прикріпити будь-який метод без параметрів як OnClickListener до виду:
@BindingAdapter("app:onClick")publicstaticvoid bindOnClick(View view, finalRunnable runnable){ view.setOnClickListener(v -> runnable.run());}
Адаптери можуть бути створені для кількох параметрів одночасно. Наприклад, ось як можна вказати url для зображення або ресурсу у разі помилки завантаження:
@BindingAdapter({"android:src", "app:error"})publicstaticvoid loadImage(ImageView view, String url, Drawable error){ Picasso.with(view.getContext()) .load(url) .error(error) .into(view);}
Але іноді потрібно автоматично перетворювати типи, наприклад, boolean в int (Visibility)). У таких випадках замініть адаптер на @BindingConversion:
@BindingConversion publicstaticint convertBooleanToVisibility(boolean visible){return visible ?View.VISIBLE:View.GONE;}
На відміну від адаптера, який повністю контролює поведінку, @BindingConversion має тип повернення, який пізніше буде застосовано до атрибута.
5. Програмування з задоволенням
Давайте покращимо наш додаток додатковими функціями. По-перше, нашому профілю не вистачає особистого дотику, оскільки немає фотографії. По-друге, він майже марний, оскільки немає жодних дій.
Отже, давайте додамо фото користувача та опцію "Додати в друзі". Додайте необхідні поля до моделі User:
privateString photo;privateboolean isFriend;
Додайте ImageView для аватара користувача в макет:
<imageview android:layout_width="@dimen/avatar_size" android:layout_height="@dimen/avatar_size" android:src="@{user.photo}"></imageview>
Адаптер loadImage, який ми написали раніше, буде використовуватися для завантаження зображення. Тепер давайте введемо кнопки для додавання та видалення друзів:
<relativelayout android:layout_width="match_parent" android:layout_height="wrap_content"> <button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/add_as_friend" android:visibility="@{!viewModel.isFriend}" app:onclick="@{viewModel.changeFriendshipStatus}"></button> <button android:layout_width="match_parent" android:layout_height="wrap_content" android:text="@string/already_friends" android:visibility="@{viewModel.isFriend}" app:onclick="@{viewModel.changeFriendshipStatus}"></button> </relativelayout>
Ви, напевно, помітили, що об'єкт користувача був перейменований на viewModel, хоча це зовсім інший об'єкт. ViewModel пов'язує View з моделлю даних, описуючи всю логіку поведінки. Наступного разу ми поговоримо про MVVM (Model-View-ViewModel), а поки що вам потрібно пам'ятати, що ви повинні помістити свою логіку поведінки в окремий об'єкт. Ось наш VM для профілю:
publicclass ProfileViewModel extends BaseObservable { publicstaticfinalint LOADING_SHORT =1000; privateboolean isLoaded;privateboolean isFriend; public ProfileViewModel(boolean isFriend){this.isFriend= isFriend;this.isLoaded=true;} @Bindable publicboolean getIsLoaded(){returnthis.isLoaded;} publicvoid setIsLoaded(boolean isLoaded){this.isLoaded= isLoaded; notifyPropertyChanged(BR.isLoaded);} @Bindable publicboolean getIsFriend(){returnthis.isFriend;} publicvoid setIsFriend(boolean isFriend){this.isFriend= isFriend; notifyPropertyChanged(BR.isFriend);} publicvoid changeFriendshipStatus(){ load(()-> setIsFriend(!isFriend));} privatevoid load(Runnable onLoaded){ setIsLoaded(false);new Handler().postDelayed(()->{ setIsFriend(!isFriend); setIsLoaded(true);}, LOADING_SHORT);}}
Як ви можете помітити, VM успадковується від BaseObservable. Це дозволяє нам сповіщати прив'язку про внутрішні зміни, використовуючи notifyPropertyChanged, де передається BR.variable. Назва змінної генерується так само, як id у стандартному класі R і позначена анотацією @Bindable (в геттерах у нашому випадку).
Поле isLoaded слугуватиме як прапорець для індикатора завантаження, який буде перемикатися на секунду щоразу, коли викликається changeFriendshipStatus. Тепер нам потрібно трохи змінити макет з кнопками та додати ProgressBar:
<relativelayout android:layout_width="match_parent" android:layout_height="wrap_content" android:visibility="@{viewModel.isLoaded}"><!-- кнопки --></relativelayout><progressbar android:layout_width="@dimen/small_progressbar_size" android:layout_height="@dimen/small_progressbar_size" android:layout_gravity="center" android:visibility="@{!viewModel.isLoaded}"></progressbar>
Тепер щоразу, коли ми натискаємо кнопку для додавання або видалення друзів, RelativeLayout замінюватиметься на ProgressBar на секунду і повертатиметься до нормального стану. Час запустити код і перевірити результат.
6. І це ще не все!
Чи помітили ви ці громіздкі @Bindable і notifyPropertyChanged геттери та сеттери в ViewModel? Вони захаращують код і змушують нас виконувати багато рутинної роботи (а саме цього ми хотіли уникнути з прив'язкою). Але все ще не втрачено — у нас є ObservableField<T>.
ObservableField — це автономні спостережувані об'єкти з одним полем. Ви можете отримувати до них доступ за допомогою методів get() і set(), які автоматично сповіщають View про зміни. Щоб використовувати це, потрібно створити публічне фінальне поле в класі VM. Давайте оновимо наш ProfileViewModel, щоб це зробити:
publicclass ProfileViewModel { publicstaticfinalint LOADING_SHORT =1000; publicfinal ObservableBoolean isLoaded =new ObservableBoolean(true);publicfinal ObservableBoolean isFriend =new ObservableBoolean();public ProfileViewModel(boolean isFriend){ isFriend.set(isFriend);} publicvoid changeFriendshipStatus(){ load(()-> isFriend.set(!isFriend.get()));} privatevoid load(Runnable onLoaded){ isLoaded.set(false);new Handler().postDelayed(()->{ isFriend.set(!isFriend.get()); isLoaded.set(true);}, LOADING_SHORT);}}
Зараз у нас очевидно менше коду, хоча він виконує ті ж функції. Варто зазначити, що майже кожен примітивний тип має спостережуваний аналог. Ми використали ObservableBoolean.
На жаль, бібліотека ще не має двостороннього зв'язування даних. Але у нас є всі інструменти, щоб реалізувати це самостійно! Двостороннє зв'язування даних означає, що дані впливають на те, що ми бачимо у View, а дані, введені користувачем, також змінюють модель. Давайте реалізуємо це для EditText.
По-перше, зверніть увагу, що ObservableString з якоїсь причини відсутній, тому нам потрібно його створити:
publicclass ObservableString extends BaseObservable { privateString value =""; public ObservableString(String value){this.value= value;} public ObservableString(){} publicString get(){return value !=null? value :"";} publicvoid set(String value){if(value ==null) value ="";if(!this.value.contentEquals(value)){this.value= value; notifyChange();}} publicboolean isEmpty(){return value ==null|| value.isEmpty();} publicvoid clear(){ set(null);}}
Логіка тут проста, тому ми не будемо її обговорювати і перейдемо до реалізації адаптера. Головний трюк полягає в тому, щоб додати TextWatcher до EditText і оновити наш ObservableString у зворотному виклику:
@BindingAdapter("android:text")publicstaticvoid bindEditText(EditText view, final ObservableString observableString){ Pair<observablestring textchangelistener> pair =(Pair) view.getTag(R.id.bound_observable);if(pair ==null|| pair.first!= observableString){if(pair !=null) view.removeTextChangedListener(pair.second); TextChangeListener watcher =new TextChangeListener((s, start, before, count)-> observableString.set(s.toString())); view.setTag(R.id.bound_observable, new Pair<>(observableString, watcher)); view.addTextChangedListener(watcher);}String newValue = observableString.get();if(!view.getText().toString().equals(newValue)) view.setText(newValue);}</observablestring>
Тепер ми можемо просто вказати наш ObservableString у XML, і цього буде достатньо!
<textview android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{viewModel.status}" style="@style/StatusTextView"></textview>
7. Висновки
Наша команда використовує цю бібліотеку вже досить довго, і ми створили кілька рішень, які спрощують наше життя. Ось деякі з них:
RecyclerBindingAdapter — це універсальний адаптер для простих списків. Один для всього проєкту. Тепер нам не потрібно створювати окремий для кожного списку :) Давайте подивимося, як це працює:
publicclass RecyclerBindingAdapter<t>extends RecyclerView.Adapter<recyclerbindingadapter.bindingholder>{privateint holderLayout, variableId;private AbstractList<t> items =new ArrayList<>();private OnItemClickListener<t> onItemClickListener;public RecyclerBindingAdapter(int holderLayout, int variableId, AbstractList<t> items){this.holderLayout= holderLayout;this.variableId= variableId;this.items= items;} @Override public RecyclerBindingAdapter.BindingHolder onCreateViewHolder(ViewGroup parent, int viewType){View v = LayoutInflater.from(parent.getContext()) .inflate(holderLayout, parent, false);returnnewBindingHolder(v);} @Override publicvoid onBindViewHolder(RecyclerBindingAdapter.BindingHolder holder, int position){final T item = items.get(position); holder.getBinding().getRoot().setOnClickListener(v ->{if(onItemClickListener !=null) onItemClickListener.onItemClick(position, item);}); holder.getBinding().setVariable(variableId, item);} @Override publicint getItemCount(){return items.size();}publicvoid setOnItemClickListener(OnItemClickListener<t> onItemClickListener){this.onItemClickListener= onItemClickListener;}publicinterface OnItemClickListener<t>{void onItemClick(int position, T item);}publicstaticclassBindingHolderextends RecyclerView.ViewHolder{private ViewDataBinding binding;publicBindingHolder(View v){super(v); binding = DataBindingUtil.bind(v);}public ViewDataBinding getBinding(){return binding;}}</t></t></t></t></t></recyclerbindingadapter.bindingholder></t>
Ви можете створити його однією стрічкою:
new RecyclerBindingAdapter<>(R.layout.item_holder, BR.data, list);
BR.data — це ім'я змінної в xml файлі, а list — наш зразок. Вкажіть RecyclerView і позбудьтеся цієї головного болю раз і назавжди. Трохи заздалегідь виникає питання: як ми можемо налаштувати RecyclerView без прямого звернення до зв'язування? І тут нам потрібна наступна функція — RecyclerConfiguration:
publicclass RecyclerConfiguration extends BaseObservable { private RecyclerView.LayoutManager layoutManager;private RecyclerView.ItemAnimator itemAnimator;private RecyclerView.Adapter adapter; /* @Bindable getters *//* notifyPropertyChanged setters */ @BindingAdapter("app:configuration")publicstaticvoid configureRecyclerView(RecyclerView recyclerView, RecyclerConfiguration configuration){ recyclerView.setLayoutManager(configuration.getLayoutManager()); recyclerView.setItemAnimator(configuration.getItemAnimator()); recyclerView.setAdapter(configuration.getAdapter());}}
Ця проста обгортка не засмічує код ObservableFileds і дозволяє вам вказати конфігурацію прямо в XML (але потрібно заповнити її з коду заздалегідь).
Ще однією цікавою практикою є <include>
тег. У прив'язці він має зовсім іншу функцію, а саме використовується для заміни простого CustomView. Давайте додамо лічильник фотографій, друзів та лайків до нашого проєкту. Елемент макету складатиметься з заголовка та самого лічильника. Щоб уникнути дублювання коду, ми помістимо його в item_counter.xml:
<layout xmlns:android="http://schemas.android.com/apk/res/android"><data><variable name="count" type="int"></variable><variable name="title" type="String"></variable></data><linearlayout android:layout_width="wrap_content" android:layout_height="wrap_content" android:orientation="vertical" android:gravity="center"><textview android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{Integer.toString(count)}"></textview><textview android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{title}"></textview></linearlayout></layout>
Щоб імпорт працював, вам потрібно обгорнути макет тегом <layout>
. Додавши <data>
, ви можете перетворити всі значення на змінні. Найцікавіше, що ви можете вказувати ці змінні ззовні, використовуючи атрибути з такими ж назвами! Ось як передати дані до лічильника друзів:
<include layout="@layout/item_counter" android:layout_width="wrap_content" android:layout_height="wrap_content" app:title="@{@plurals/friends(viewModel.friendsCount)}" app:count="@{viewModel.friendsCount}"></include>
А після внесення деяких доповнень до ViewModel ми отримаємо наступну імітацію подій в реальному часі:
8. Нічого не є досконалим
На сьогодні DataBinding все ще на стадії розробки, тому має кілька недоліків. По-перше, у нього немає двостороннього зв'язку даних і деякі поля доступні з коробки. Також є проблеми з Android Studio: часто вирази з макету помічені як помилки, клас BR або цілий пакет прив'язки зникає (щоб вирішити цю проблему, використовуйте Build → Clean Project). Існують проблеми з кодуванням у макеті (наприклад, замість “&&” потрібно писати “&&”) тощо. Але це просто тимчасові незручності, оскільки бібліотека активно розробляється, і варто дочекатися її поліпшення :)
9. Висновок
Це не всі переваги бібліотеки DataBinding, але час закінчувати цю статтю. Сподіваюся, ми розпалили ваш інтерес до цієї бібліотеки, ви дізналися щось нове і застосуєте ці знання на практиці.
Наступного разу ми обговоримо найкращий спосіб реалізації архітектури додатка MVVM, що тісно пов'язана з DataBinding.
Дякуємо за увагу!
Приклад додатка доступний на GitHub.
Потрібна розробка MVP, iOS та Android додатків або прототипування? Ознайомтесь з нашим портфоліо та зробіть замовлення сьогодні!