Введение
Профессионально андроид-разработкой занимаюсь чуть больше года, до этого разрабатывал по Windows Phone и мне понравилась возможность связывать данные из вью модели с самим View при помощи механизма Bindings. А после изучения RX, многие задачи стали решаться более чисто, вью-модель полностью отделилась от View. Она стала оперировать только моделью, совсем не заботясь о том, как она будет отображаться.
В Android такой строгости я не заметил, Activity или Fragment как простейшие представители контроллера чаще всего имеют полный доступ как ко View, так и к модели, зачастуя решая, какой View будет видим, решая таким образом чисто вьюшные задачи. Поэтому я довольно радостно воспринял новость о появлении Data Binding в Android на прошедшем Google IO.
Пока что это только бета релиз, но уже можно протестировать функционал и увидеть направление, в котором двигаются разработчики из Google.
Начало
Я использую Android Studio 1.3.
Для сборки используется новый android плагин для Gradle (нужна версия 1.3.0-beta1 и старше). Так как связи отрабатываются во время компиляции, нам понадобиться ещё один плагин к Gradle ‘com.android.databinding:dataBinder:1.0-rc0’. В отличие от того же Windows Phone где механизм привязок реализован глубоко по средством DependencyProperty и в RealTime, в Android эта функция реализуется как бы поверх обычных свойств, во время компиляции и дополнительной кодогенерации, поэтому в случае ошибок будьте готовы разбирать ответ от компилятора.
Итак, заходим в файл build.gradle, который лежит в корневом каталоге проекта (в нём идут настройки Gradle для всего проекта). В блоке dependencies вставляем:
dependencies { classpath 'com.android.tools.build:gradle:1.3.0-beta2' classpath 'com.android.databinding:dataBinder:1.0-rc0' }
Теперь подключим плагин к конкретному модулю, откроем build.gradle файл, который лежит внутри модуля. По умолчанию app/build.gradle и добавим строчку:
apply plugin: 'com.android.databinding'
Настройка Layout
Мы должны обернуть наш внешний View в тег <layout>
. Cтудия сейчас подчеркивает его красным и требует ему назначить атрибуты высоты и ширины; игнорируем её, иначе получим ошибку дубликации атрибутов. Этим действием вы сказали плагину биндинга генерировать класс связывания. По умолчанию имя этого класс будет выбираться на основе названия файла разметки. Для main_activity — MainActivityBinding, который будет доступен для импорта из пакета, для меня com.georgeci.bindingssample.databinding.ActivityMainBinding
Важно: если в разметке не будет View с Id или тега <data>
класс биндинг не создастся.
<layout xmlns:android="http://schemas.android.com/apk/res/android"> <data> <variable name="user" type="com.georgeci.bindingssample.User"/> </data> <LinearLayout xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" android:paddingLeft="@dimen/activity_horizontal_margin" android:paddingRight="@dimen/activity_horizontal_margin" android:paddingTop="@dimen/activity_vertical_margin" android:paddingBottom="@dimen/activity_vertical_margin" tools:context=".MainActivity"> <TextView android:id="@+id/bindTv" android:layout_width="wrap_content" android:layout_height="wrap_content"/> </RelativeLayout> </layout>
Уже сейчас можно начать его использовать класс Binding для доступа к элементам интерфейса, без использования findViewById. В MainActivity добавим поле и перепишем метод onCreate:
ActivityMainBinding binder; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_main); binding.bindTv.setText("Some text"); }
Название поля берётся из Id View, без Id в биндере поле не появиться, если изменить Id View, то поле в биндере сразу же переметнуться. Если с зажатым CTRL нажать на название поля View, то сразу перейдешь к нему в файле разметки. Как по мне так уже одного такого функционала достаточно для того чтобы начать использовать биндинги.
Привязка данных
Например у нас есть карточка пользователя имя и возраст.
public class User { public String name; public int age; public User(String name, int age) { this.nam = name; this.age = age; } }
Изменим Layout, заменим содержимое LinearLayout на:
<TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{user.name}"/> <TextView android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{Integer.toString(user.age)}"/>
И в onCreate заменим последнюю строку на:
binding.setUser(new User("Some name", 27));
Запускаем. Всё работает.
Наверное у всех проектах в активити или в фрагментах встречается такие строчки:
someView.setVisibility(isVisible : View.VISIBLE : View.GONE);
Тут то мы и начинаем использовать непосредственно привязки данных. Перепишем модель:
public class User { public String name; public int age; public User(String name, int age, boolean isAdult) { this.name = name; this.age = age; this.isAdult = isAdult; } public boolean isAdult; }
И добавим в Layout:
<TextView android:id="@+id/adult_content" android:layout_width="wrap_content" android:layout_height="wrap_content" android:visibility="@{user.isAdult ?View.VISIBLE :View.GONE}" android:text="Some content only for adult"/>
На красные выделения студии игнорируем.
Так как мы используем класс View, то его нужно импортировать, добавим в ноду <data>
:
<import type="android.view.View"/>
Или используем его вместе с названием пакета:
android:visibility="@{user.isAdult ? android.view.View.VISIBLE : android.view.View.GONE}"
Так же возможно в ноде <import>
задать псевдоним для класса:
<import type="android.view.View" alias="SomeAlias"/> ... android:visibility="@{user.isAdult ? SomeAlias.VISIBLE : SomeAlias.GONE}"
Конвертеры
Импорт в свою очередь даёт возможность писать конвертеры. Добавим в модель поле с датой рождения и удалим возраст:
public class User { public String name; public long birthday; public boolean isAdult; public User(String name, long birthday, boolean isAdult) { this.name = name; this.birthday = birthday; this.isAdult = isAdult; } }
Напишем конвертер:
public class UnixDateConverter { public static String convert(long timestamp) { Calendar mydate = Calendar.getInstance(); mydate.setTimeInMillis(timestamp * 1000); SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy"); return sdf.format(mydate.getTime()); } }
Импортируем его в разметку:
<import type="com.georgeci.bindingssample.UnixDateConverter"/> ... <TextView android:id="@+id/birthday" android:layout_width="wrap_content" android:layout_height="wrap_content" android:text="@{UnixDateConverter.convert(user.birthday)}"/>
Обратная связь и Binding
Попробуем сменить имя пользователя.
Добавим в Layout:
<variable name="clicker" type="android.view.View.OnClickListener"/> ... <Button android:text="Some button" app:onClickListener="@{clicker}" android:layout_width="match_parent" android:layout_height="wrap_content"/>
Перепишем onCreate:
protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_main); final User user = new User("Some name", 668714400L, false); binding.setUser(user); binding.setClicker(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(MainActivity.this, "ckick!", Toast.LENGTH_SHORT).show(); user.name="from btn"; } }); }
Запускаем и кликаем, тост всплыл, но имя не изменилось. Это случилось из-за того, что модель ни как не известила binder о своём изменении.
Можно создать новую модель и вставить её, но с точки зрения памяти это расточительно:
binding.setUser(new User("New model", 668714400L, false));
Или вытащить старую, заменить данные и вставить опять:
User user1 = binding.getUser(); user1.name = "old model"; binding.setUser(user1);
Но тогда обновятся все View, связанные с этой моделью. Лучшим вариантом будет связать модель с binder, чтобы она могла его оповестить о своём изменении. Для этого перепишем класс модели, добавив геттеры и сеттеры,
помечая геттеры атрибутом @Bindable, и добавив в сеттеры вызов notifyPropertyChanged(BR.lastName);
public class User extends BaseObservable { private String name; private long birthday; private boolean adult; public User(String name, long birthday, boolean isAdult) { this.name = name; this.birthday = birthday; this.adult = isAdult; } @Bindable public String getName() { return name; } public void setName(String name) { this.name = name; notifyPropertyChanged(com.georgeci.bindingssample.BR.name); } @Bindable public long getBirthday() { return birthday; } public void setBirthday(long birthday) { this.birthday = birthday; notifyPropertyChanged(com.georgeci.bindingssample.BR.birthday); } @Bindable public boolean isAdult() { return adult; } public void setAdult(boolean adult) { this.adult = adult; notifyPropertyChanged(com.georgeci.bindingssample.BR.adult); } }
Видим новый класс BR, в котором содержатся идентификаторы полей, чьи геттеры помечены атрибутом @Bindable
. В Layout оставляем android:text="@{user.name}", меняем только isAdult на adult, c ‘is’ в названии поля возникли проблемы. Запускаем всё работает.
ObservableFields
В пакете android.databinding есть классы, которые могут упростить нотификацию binder об изменении модели:
- Обёртки над элементарными типами
- ObservableField<T>
- ObservableArrayMap<K, V>
- ObservableArrayList<T>
Попробуем изменить модель:
public class User extends BaseObservable { @Bindable public final ObservableField<String> name = new ObservableField<>(); @Bindable public final ObservableLong birthday = new ObservableLong(); @Bindable public final ObservableBoolean adult = new ObservableBoolean(); public User(String name, long birthday, boolean isAdult) { this.name.set(name); this.birthday.set(birthday); this.adult.set(isAdult); } }
По коллекциям аналогично, единственное приведу пример обращения ко ключу к Map:
<TextView android:text='@{user["lastName"]}' android:layout_width="wrap_content" android:layout_height="wrap_content"/>
Из View в Model
Теперь попробуем взять новое имя из UI, привязав EditText к некоторой модели. Так как внутри EditText крутится Editable, то и привязать будет к ObservableField<Editable>
. Но где именно держать этот объект? Вот тут и настаёт время для MVVM. Создаём класс SimpleViewMode:
public class Vm extends BaseObservable { @Bindable public final ObservableField<Editable> edit = new ObservableField<>(); public Vm() { this.edit.set(Editable.Factory.getInstance().newEditable("")); } }
Изменю MainActivity:
public class MainActivity extends AppCompatActivity { User user; ActivityMainBinding binding; Vm vm = new Vm(); @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); binding = DataBindingUtil.setContentView(this, R.layout.activity_main); user = new User("Some name", 668714400L, false); binding.setUser(user); binding.setVm(vm); binding.setClicker(new View.OnClickListener() { @Override public void onClick(View v) { Toast.makeText(getApplicationContext(), vm.edit.get().toString(), Toast.LENGTH_SHORT).show(); } }); } }
А в разметку добавлю:
<EditText android:id="@+id/edit_text" android:layout_width="match_parent" android:text="@{vm.edit}" android:layout_height="wrap_content"/>
Вот тут возникает проблема, если привязать ObservableField<Editable>
к EditText, то всё будет работать только в сторону View. Как я понял, проблема в том, что Editable, который лежит внутри ObservableField, отличается от того, который лежит внутри EditText.
Если у кого есть идеи — делитесь.
Итог
Очень любопытно было увидеть библиотеку для поддержки Data Binding в Android от Google. В документации тоже нет информации про обратную связь данных, но я надеюсь на скорую её реализацию. После официального выхода стабильной версии можно будет посмотреть на интеграцию с JavaRX.
[ Оффициальная документация ]
[ Ссылка на мой простенький пример ]
ссылка на оригинал статьи http://habrahabr.ru/post/260317/
Добавить комментарий