Android Architecture Components: Data Binding

Jest to cykl artykułów poświęcony komponentom architektury Androida. Wszystkie artykuły znajdziesz na tej stronie.

Rozpoczynamy serię postów na temat komponentów architektury Androida. Przeanalizujemy je na bardzo prostych przykładach, postaram się wyjaśnić, jak każdy z komponentów działa indywidualnie i jak możemy z nich korzystać. Na poczatku poznamy podstawy Data Binding,

Android Architecture Components: Data Binding

Kiedy tworzymy aplikację dla systemu Android, musimy połączyć dane w kodzie Java z plikiem układu XML. W tym celu możemy skorzystać z metody findViewById() lub Butter Knife do wiązania widoków. Następnie musimy ręcznie ustawić wartości dla poszczególnych widoków. Aby zminimalizować to zadanie, Google opublikowało bibliotekę DataBinding.
Jest to biblioteka, która umożliwia powiązanie interfejsu użytkownika z logiką biznesową, dzięki czemu wartości interfejsu użytkownika mogą być automatycznie aktualizowane. Zmniejsza to ilość kodu w logice biznesowej, które zazwyczaj zapisujesz w celu zsynchronizowania interfejsu użytkownika, gdy dostępne są nowe dane. Data Binding to jeden z elementów architektury Androida zasugerowanych przez Google.
Ta technika jest dobrze znana w wielu różnych językach i platformach. Powiązanie danych pozwala łączyć dane i zdarzenia użytkowników bezpośrednio w pliku XML.

Konfiguracja środowiska

Biblioteka wymaga systemu Android 2.1 (API 7+) lub nowszego. Aby rozpocząć korzystanie z Data Binding, musisz najpierw włączyć tę funkcję w swoim projekcie. W tym celu otwórz plik build.gradle (module) i włącz moduł Data Binding w sekcji Android. Po włączeniu Zsynchronizuj projekt.

apply plugin: 'com.android.application'
android {
    dataBinding {
        enabled = true
    }
    compileSdkVersion 28
    ...
}
dependencies {
    ...
}

Tworzenie layoutu

Pliki układów wyglądu rozpoczynają się od głównego znacznika <layout>, po którym następuje element <data> i <view>.
Znacznik data opisują dane, które są dostępne do wiązania. Wewnątrz elementu data znajduje się znacznik <variable>. Tag ten przyjmuje dwa atrybuty: „name  i „type” . atrybut name będzie aliasem naszej klasy News, a type powinno wskazywać na klasę obiektu. W naszym przypadku ścieżka prowadzi do klasy News.
Element view zawiera hierarchię główną wyglądu. Odwołania do elementów danych lub wyrażeń w układzie są zapisywane we właściwościach atrybutu za pomocą @{} lub @={}, Spójrz na poniższy przykład,

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="news"
            type="net.myenv.architecturecomponents.News" />
    </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="@{news.title}" />
    </LinearLayout>
</layout>

Data Binding w układach <include>

Zwykle oddzielamy główny układ od zawartości w dwóch różnych układach tj. activity_main.xml i content_main.xml. Element content_main zostanie uwzględniony w głównym układzie za pomocą znacznika <include>. Jak to połączyć z data binding? Zobaczmy

<?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"
    xmlns:bind="http://schemas.android.com/tools">
    <data>
        <variable
            name="news"
            type="net.myenv.architecturecomponents.News" />
    </data>
    <android.support.design.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <include
            android:id="@+id/content"
            layout="@layout/content_main"
            bind:news="@{news}" />
        <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_margin="10dp"
            app:srcCompat="@android:drawable/ic_dialog_email" />
    </android.support.design.widget.CoordinatorLayout>
</layout>
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="news"
            type="net.myenv.architecturecomponents.News" />
    </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="@{news.title}" />
    </LinearLayout>
</layout>

Znacznik <layout> jest używany w układzie activity_main.xml do włączenia powiązania danych. Ponadto znaczniki <data> i <variable> służą do wiązania obiektu News. Aby przekazać dołączony układ content_main używamy bind:news=”@{news}”. Bez tego obiekt News nie będzie dostępny w układzie content_main. W pliku content_main również musimy zdefiniować znaczniki <data> i <variable>. 
Pamiętaj, że znaczniki <layout>, <data> i <variable> są niezbędne zarówno w układach macierzystych, jak i dołączonych.
Uzupełnij wiedzę o: Jak wykonać kopię zapasową danych Twojej aplikacji w chmurze

Import i tagi zmiennych

Jeżeli potrzebujemy użyć zmiennych lub stałych, musimy zadeklarować je w pliku układu. Aby to osiągnąć, musimy użyć tagów <import> oraz <variable> i umieścić je w <data>. Na przykład załóżmy, że mamy klasę z wiadomościami z polami: title (String) i premium (boolean). Chcemy zawsze wyświetlać nazwę, a jeśli dany artykuł jest premium wyświetlamy taką informacje. Nasz układ będzie wyglądał tak:

<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="news"
            type="net.myenv.architecturecomponents.News" />
        <import type="android.view.View" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">
        <TextView
            android:id="@+id/premium"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@string/premium"
            android:visibility="@{ news.premium == true ? View.VISIBLE: View.GONE }" />
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{news.title}" />
    </LinearLayout>
</layout>

Znacznik import wymaga tylko typu. Klasa String jest importowana automatycznie, więc nie musimy jej importować ręcznie, ale w przypadku View jest to konieczne (potrzebujemy klasy View do użycia stałych widoczności) – linia 8. Jeśli dany artykuł będzie w wersji premium to etykieta pojawi się w przeciwnym wypadku TextView będzie ukryty – linia 21.
Możemy także odwoływać się do list, tablic czy kolekcji.

<data>
    <variable name="list" type="List<String>"/>
    <import type="java.util.List"/>
</data>
...
android:text="@{list[index]}"

Jeśli przekażemy liczbę całkowitą w android:text to środowisko Android będzie używać wartości zasobów o tym identyfikatorze. Aby uniknąć błędu, musimy naszą zmienną umieścić w String.valueOf(), aby uzyskać wartość tekstową o wartości numerycznej.

android:text="@{ String.valueOf(news.numberComments) } "

Nic nie stoi na przeszkodzie aby łączyć zmienne.

android:text="@{news.title +' '+ news.site }"

Albo dodawać liczby.

android:text="@{String.valueOf(news.comment + 10)}"

Więcej operatorów możesz znaleźć tutaj.

Data Binding w kodzie

Mamy przygotowany layout, jak teraz wypełnić go danymi? Klasy Binding są generowane automatycznie jeśli tak się nie stanie to musisz wygenerować klasę ręcznie opartą na tym układzie. Wywołaj z menu Build -> Clean Project Build -> Rebuild Project. Spowoduje to wygenerowanie niezbędnych klas wiążących. Wygenerowane klasy są zgodne z konwencją nazewnictwa uwzględniającą nazwę pliku układu, w której włączono wiązanie. W naszym przypadku layout o nazwie activity_main.xml wygeneruje klasę ActivityMainBinding. Dla contact_activity będzie to ContactActivityBinding itd.
Poniżej została przedstawiona klasa News oraz MainActivity.

public class News {
    String title;
    String site;
    boolean premium;
    int comment;
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
    }
    public String getSite() {
        return site;
    }
    public void setSite(String site) {
        this.site = site;
    }
    public boolean isPremium() {
        return premium;
    }
    public void setPremium(boolean premium) {
        this.premium = premium;
    }
    public int getComment() {
        return comment;
    }
    public void setComment(int comment) {
        this.comment = comment;
    }
}
public class MainActivity extends AppCompatActivity {
    private News news;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        news = new News();
        news.setTitle("Android Architecture Components: Data Binding");
        news.setSite("www.myenv.net");
        news.setComment(10);
        news.setPremium(true);
        binding.setNews(news);
    }
}

Aby powiązać dane z interfejsem użytkownika, należy najpierw powiązać układ przy użyciu wygenerowanych klas wiążących. W przypadku aktywności musimy zastąpić metodę setContentView() na DataBindingUtil.setContentView(). Zauważ, że nie używamy metody findViewById(). Metoda binding.setNews()wiąże obiekt News z układem.
Jeśli korzystasz z biblioteki we fragmentach, wówczas kod metody onCreate() będzie wyglądać następująco:

@Nullable
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
    FragmentMainBinding binding = DataBindingUtil.inflate(inflater, R.layout.fragment_main, container, false);
    return binding.getRoot();
}

Obsługa zdarzeń

Wiemy już jak umieszczać dane w layout. Teraz musimy zająć się obsługą przycisków i innych zdarzeń.

<?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"
    xmlns:bind="http://schemas.android.com/tools">
    <data>
        <variable
            name="news"
            type="net.myenv.architecturecomponents.News" />
        <variable
            name="handlers"
            type="net.myenv.architecturecomponents.MyClickHandlers" />
    </data>
    <android.support.design.widget.CoordinatorLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">
        <include
            android:id="@+id/content"
            layout="@layout/content_main"
            bind:news="@{news}" />
        <Button
            android:id="@+id/buttonWithParam"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onClick="@{(v) -> handlers.onButtonClickWithParam(v, news)}"
            android:text="Button with Paraterms"
            app:layout_anchor="@+id/content"
            app:layout_anchorGravity="bottom"
            android:layout_gravity="bottom" />
        <Button
            android:id="@+id/buttonLongPress"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:onLongClickListener="@{handlers::onButtonLongPressed }"
            android:text="LongClick Button"
            app:layout_anchor="@+id/buttonWithParam"
            app:layout_anchorGravity="bottom"
            android:layout_gravity="bottom" />
        <android.support.design.widget.FloatingActionButton
            android:id="@+id/fab"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="bottom|end"
            android:layout_margin="10dp"
            android:onClick="@{handlers::onFabClicked}" />
    </android.support.design.widget.CoordinatorLayout>
</layout>
public class MyClickHandlers {
    Context context;
    public MyClickHandlers(Context context) {
        this.context = context;
    }
    public boolean onButtonLongPressed(View view) {
        Toast.makeText(context, "Button long pressed!", Toast.LENGTH_SHORT).show();
        return false;
    }
    public void onButtonClickWithParam(View view, News news) {
        Toast.makeText(context, "Button clicked! Name: " + news.title, Toast.LENGTH_SHORT).show();
    }
}
public class MainActivity extends AppCompatActivity {
    private News news;
    private MyClickHandlers clickHandlers;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        news = new News();
        news.setTitle("Android Architecture Components: Data Binding");
        news.setSite("www.myenv.net");
        news.setComment(10);
        news.setPremium(true);
        binding.setNews(news);
        clickHandlers = new MyClickHandlers(this);
        binding.setHandlers(clickHandlers);
    }
}
public class MyClickHandlers {
    Context context;
    public MyClickHandlers(Context context) {
        this.context = context;
    }
    public void onFabClicked(View view) {
        Toast.makeText(context, "FAB clicked", Toast.LENGTH_SHORT).show();
    }
    public boolean onButtonLongPressed(View view) {
        Toast.makeText(context, "Button long pressed!", Toast.LENGTH_SHORT).show();
        return false;
    }
    public void onButtonClickWithParam(View view, News news) {
        Toast.makeText(context, "Title: " + news.title, Toast.LENGTH_SHORT).show();
    }
}

Powyżej znajduje się przykład reakcji na przyciski.
Zdarzenia mogą być bezpośrednio powiązane z metodamiJedną z głównych zalet w porównaniu z atrybutem onClick jest to, że wyrażenie jest przetwarzane w czasie kompilacji, więc jeśli metoda nie istnieje lub jej sygnatura jest niepoprawna, pojawi się błąd.
Aby przypisać zdarzenie użyj zwykłego wyrażenia wiążącego, którego wartością jest nazwa metody, która ma zostać wywołana. Jeszcze innym sposobem wywołania akcji jest wyrażenie lambda, które są wywoływane w momencie wystąpienia zdarzenia.
Uzupełnij wiedzę o: Integracja Bitbucket z Android Studio

Import Method Binding

Możesz także powiązać funkcje z elementami interfejsu użytkownika. Powiedzmy, że chcesz wykonać operację na wartości przed jej wyświetleniem. Możesz to zrobić za pomocą znacznika <import>, a następnie wywołać odpowiednie wyrażenie w atrybucie android:text.

public class MethodBinding {
    public static String capitalize(String text) {
        return text.toUpperCase();
    }
}
<?xml version="1.0" encoding="utf-8"?>
<layout xmlns:android="http://schemas.android.com/apk/res/android">
    <data>
        <variable
            name="news"
            type="net.myenv.architecturecomponents.News2" />
        <import type="android.view.View" />
        <import type="net.myenv.architecturecomponents.MethodBinding" />
    </data>
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical">
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="@{ MethodBinding.capitalize( news.title +' '+ news.site ) }" />
    </LinearLayout>
</layout>

Powyższy przykład zamienia wartości name oraz site na wielkie litery przed ich wyświetleniem.

Aktualizacja danych za pomocą Observables

Observables umożliwia automatyczną synchronizację interfejsu użytkownika z danymi bez jawnego wywoływania metod. UI zostanie zaktualizowany, gdy wartości ulegną zmienię w obiekcie.
Aby uczynić to , rozszerz klasę z obiektami o BaseObservable. Aby umożliwić obserwację właściwości, użyj adnotacji @Bindable na metodzie getter. Wywołaj notifyPropertyChanged(BR.property) w metodzie zmieniającej dane, aby zaktualizować interfejs użytkownika. Klasy BR będą generowane automatycznie, gdy powiązanie danych jest włączona.
Poniżej znajduje się zmodyfikowana klasa News oraz MyClickHandlers. Możesz zauważyć tutaj, że notifyPropertyChanged jest wywoływany po przypisaniu nowych wartości. W klasie MyClickHandlers zmodyfikowana jest tylko metoda onButtonClickWithParam(), która zmienia wartość premium. Layout i MainActivity nie zmieniają się z poprzedniego punktu.

public class News extends BaseObservable {
    String title;
    String site;
    boolean premium;
    int comment;
    @Bindable
    public String getTitle() {
        return title;
    }
    public void setTitle(String title) {
        this.title = title;
        notifyPropertyChanged(net.myenv.architecturecomponents.BR.title);
    }
    @Bindable
    public String getSite() {
        return site;
    }
    public void setSite(String site) {
        this.site = site;
        notifyPropertyChanged(BR.site);
    }
    @Bindable
    public boolean isPremium() {
        return premium;
    }
    public void setPremium(boolean premium) {
        this.premium = premium;
        notifyPropertyChanged(BR.premium);
    }
    @Bindable
    public int getComment() {
        return comment;
    }
    public void setComment(int comment) {
        this.comment = comment;
        notifyPropertyChanged(BR.comment);
    }
}
public class MyClickHandlers {
    Context context;
    public MyClickHandlers(Context context) {
        this.context = context;
    }
    public void onFabClicked(View view) {
        Toast.makeText(context, "FAB clicked", Toast.LENGTH_SHORT).show();
    }
    public boolean onButtonLongPressed(View view) {
        Toast.makeText(context, "Button long pressed!", Toast.LENGTH_SHORT).show();
        return false;
    }
    public void onButtonClickWithParam(View view, News news) {
        if (news.isPremium())
            news.setPremium(false);
        else
            news.setPremium(true);
    }
}

Aktualizacja za pomocą ObservableFields

Istnieje również inny sposób na aktualizację interfejsu użytkownika. Jeśli twoja klasa obiektów ma mało właściwości do aktualizacji lub jeśli nie chcesz obserwować każdego pola w obiekcie, możesz użyć ObservableFields, Możesz zadeklarować zmienną jako ObservableField i po ustawieniu nowych danych interfejs użytkownika zostanie zaktualizowany. Zobaczmy jakby wyglądał nasz przykład z etykietką premium.

ublic class MainActivity extends AppCompatActivity {
    private News news;
    private MyClickHandlers clickHandlers;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        //setContentView(R.layout.activity_main);
        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        news = new News();
        news.title.set("Android Architecture Components: Data Binding");
        news.site.set("www.myenv.net");
        news.comment.set(10);
        news.premium.set(true);
        binding.setNews(news);
        clickHandlers = new MyClickHandlers(this);
        binding.setHandlers(clickHandlers);
    }
}
public class News  {
    ObservableField<String> title = new ObservableField<>();
    ObservableField<String> site = new ObservableField<>();
    ObservableBoolean premium = new ObservableBoolean();
    ObservableInt comment = new ObservableInt();
    public ObservableField<String> getTitle() {
        return title;
    }
    public void setTitle(ObservableField<String> title) {
        this.title = title;
    }
    public ObservableField<String> getSite() {
        return site;
    }
    public void setSite(ObservableField<String> site) {
        this.site = site;
    }
    public ObservableBoolean isPremium() {
        return premium;
    }
    public void setPremium(ObservableBoolean premium) {
        this.premium = premium;
    }
    public ObservableInt getComment() {
        return comment;
    }
    public void setComment(ObservableInt comment) {
        this.comment = comment;
    }
}
public class MyClickHandlers {
    Context context;
    public MyClickHandlers(Context context) {
        this.context = context;
    }
    public void onFabClicked(View view) {
        Toast.makeText(context, "FAB clicked", Toast.LENGTH_SHORT).show();
    }
    public boolean onButtonLongPressed(View view) {
        Toast.makeText(context, "Button long pressed!", Toast.LENGTH_SHORT).show();
        return false;
    }
    public void onButtonClickWithParam(View view, News news) {
        Toast.makeText(context, "Title: " + news.title, Toast.LENGTH_SHORT).show();
        if (news.premium.get())
            news.premium.set(false);
        else
            news.premium.set(true);
    }
}

Niestandardowe konwertery z BindingAdapter

Czasami trzeba wykonać złożone konwersje danych. W tym celu można zarejestrować niestandardowy konwerter za pomocą adnotacji @BindingAdapter. Ta metoda może być umieszczona w dowolnym miejscu kodu i może zastąpić domyślną konwersję pola do modelu danych.

Na przykład, chcemy przypisać pole modelu danych do widoku obrazu. W tym celu używamy Glide do pobrania obrazu.
<ImageView
    android:id="@+id/image"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center_horizontal"
    android:src="@{news.image }" />
ObservableField<String> image =new ObservableField<>();
public ObservableField<String> getImage() {
    return image;
}
public void setImage(ObservableField<String> url) {
    this.image = url;
}
@BindingAdapter({"android:src"})
public static void loadImage(ImageView view, String imageUrl) {
    Glide.with(view.getContext())
            .load(imageUrl)
            .into(view);
}

Nic nie szkodzi na przeszkodzie, aby dać inną nazwę atrybutowi. Możesz zmienić android:src na android:image i również zadziała 🙂
Podsumowanie

Mamy już za sobą pierwszy komponent komponentów architektury Androida – Data Binding. Poznaliśmy podstawy. Nie opisałem wszystkich możliwości tej biblioteki. Dlatego bądź na bieżąco z nowymi wpisami! Polub stronę MYENV na Facebooku lub śledź za pomocą kanału RSSTwittera.
Nie zapomnij udostępnić ten wpis innym osobom, aby też mogli skorzystać z tej wiedzy. W tym celu skorzystaj z przycisków poniższych. Będę Ci za to bardzo wdzięczny. Dzięki!
Miłego dnia i Miłego kodowania ?