Android Architecture Components: LiveData

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

Znamy już takie komponenty architektury Androida: Data Binding, LifeCycles oraz ViewModel. Przyszedł czas na kolejną bibliotekę, a mianowicie na LiveData. Aby lepiej zrozumieć ten komponent wymagana jest znajomość ViewModel. Jeśli nie widziałeś poprzednich wpisów wejdź tutaj.

Android Architecture Components: LiveData

Załóżmy, że chcesz wykonywać pewne działania w ramach odpowiedzi na zmiany w danych zgodnie z cyklem życia Androida. Możesz także chcieć obserwować te wartości, które zmieniają się podczas zmiany konfiguracji. Osoby, które miały podobny problem, próbowały wielu różnych rozwiązań. Na przykład korzystając z wzorców MVP, MVVM itp. Wdrożenie tych wzorów jest również dużym problemem dla początkujących programistów. W tym celu właśnie została stworzona biblioteka LiveData, która ma ułatwić prace ze zmianą danych.
LiveData — jest częścią architektury Androida. Jest obserwowalnym posiadaczem danych. Służy do obserwacji zmian w danych i aktualizowania widoku. Chwilka, chwilka czy to nie kojarzy się z Data Binding? Tak, ale trzeba wziąć pod uwagę, że ten komponent jest świadomy cyklu życia.
Przekazując odniesienie do aktywności lub fragmentu, LiveData wie, czy interfejs użytkownika jest na pierwszym planie czy zniszczona została aktywność. Powiadamia właściciela cyklu życia (aktywność, fragment, usługa) o aktualizacjach następuje, gdy dane ulegną zmianie, a następnie interfejs użytkownika aktualizuje się. I w tym momencie może wkroczyć Data Binding. LiveData nie zastępuje Data Binding! Można łączyć te dwa komponenty w celu lepszej optymalizacji aplikacji.
Wiemy, że ViewModel służą do przekazywania danych do widoku. Samo korzystanie z ViewModels może być uciążliwe i kosztowne, ponieważ musimy wykonywać wiele połączeń za każdym razem, gdy dane będą musiały zmienić widok. LiveData opiera się na wzorcu obserwacyjnym i sprawia, że ​​komunikacja pomiędzy ViewModel a interfejsem użytkownika jest prosta. Obserwuje ona zmiany w  danych i automatycznie powiadamia o tym aktywność, fragment. Możemy także używać LiveData w usługach.
LiveData powiadamia komponent wtedy gdy jest on w stanie aktywnym. Co to znaczy w stanie aktywnym? To oznacza, że komponent systemu Android (aktywność, fragment, usługa) musi znajdować się w stanie STARTED lub RESUMED. Jeżeli klasa obserwatora jest w innym stanie, LiveData nie powiadomi o zmianach.

Zalety korzystania z LiveData

Dzięki korzystaniu z LiveData:

  • nie musisz się martwić o wycieki pamięci. Obserwatorzy są związani z obiektami cyklu życia i „oczyszczają się” po zniszczeniu komponentu Androida,
  • brak awarii z powodu zatrzymanych aktywności, fragmentu. Oznacza to, że jeśli aktywność znajduje się na tylnym stosie, to nie otrzymuje żadnego strumienia od LiveData,
  • dane zawsze aktualne. Podobnie jak ViewModel, aktywność lub fragment odbiera najnowsze dane po ponownym uruchomieniu aplikacji,
  • koniec z ręczną obsługą cyklu życia, 
  • wsparcie dla usług systemowych.

Uzupełnij wiedze o: Task i Back Stack w Androidzie

Jeszcze raz o LiveData

LiveData to tylko typ danych, który powiadamia obserwatora, gdy dane się zmienią. Komponent architektury Androida jest trochę podobny do RxJava, z wyjątkiem tego, że LiveData jest świadomy istnienia cyklu życia. Nie zaktualizuje danych w widoku, jeśli widok jest w tle. Pomaga nam to w unikaniu wyjątków, takich jak IllegalStateException.
Bibliotekę tą najlepiej łączyć z ViewModel. Kiedy rejestrujemy obserwatora w aktywności, musimy nadpisać metodę onChanged(). Metoda ta wywoływana jest po każdej zmianie danych. W ten sposób możemy zaktualizować interfejs użytkownika.
Aby powiadomić obserwatora o zmianach możemy użyć metod:

  • setValue() – działa na głównym wątku,
  • postValue() – działa na wątku w tle.

I tutaj musisz uważać. Jeżeli klasę rozszerzasz o LiveData nie będziesz miał bezpośredniego dostępu do tych metod. Są one chronione musisz je w swojej klasie nadpisać. Lepszym sposobem jest skorzystanie z MutableLiveData. Jest to po prostu klasa, która rozszerza klasę LiveData i daję bezpośredni dostęp do powyższych metod.
Mamy jeszcze do dyspozycji metodę getValue(), która zwraca bieżące dane. Przejdzmy teraz do przykładu.

Przykład LiveData

Poniżej znajduje się prosty przykład gdzie mamy klasę MyLiveDataNews, w której pobieramy informacje o artykule – metoda loadNews(). W metodzie getNews() zwracamy obiekt typu MutableLiveData<News>. Metoda changePremium() zmienia nam wartość premium i wywołuje setValue() w celu poinformowania obserwatorów, że dane uległy zmianie.
W klasie LiveDataActivity robimy odpowiednie referencje do pliku xml – metoda init(). Oczywiście tutaj też możemy zastosować Data Binding. W metodzie onCreate() pobieramy naszą klasę LiveData i zaczynamy obserwować zmiany. Jeśli dane ulegną zmianie to metoda onChange() zmieni wartości. Również nadsłuchujemy na reakcji z przycisku. Jeśli zostanie naciśniety to wartość premium się zmieni.

public class MyLiveDataNews {
    private MutableLiveData<News> newsLiveData;
    private News news;
    public LiveData<News> getNews() {
        if (newsLiveData == null) {
            loadNews();
        }
        Log.e("TAG", "getNews");
        return newsLiveData;
    }
     private void loadNews() {
        news = new News();
        news.setTitle("Android Architecture Components: Data Binding");
        news.setSite("www.myenv.net");
        news.setImage("https://dev.myenv.net/wp-content/uploads/2018/06/logo_myenv_black.png");
        news.setComment(10);
        news.setPremium(false);
        newsLiveData = new MutableLiveData<>();
        newsLiveData.setValue(news);
        Log.e("TAG", "loadNews");
    }
    void changePremium() {
        if (news.isPremium())
            news.setPremium(false);
        else
            news.setPremium(true);
        newsLiveData.setValue(news);
        Log.e("TAG", "premium= "+  news.isPremium() );
    }
}
public class LiveDataActivity extends AppCompatActivity {
    private TextView title, comments;
    private ImageView image, premium;
    private Button changePremiumBtn;
    private Context context;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.livedata_basic_activity);
        context = this;
        getLifecycle().addObserver(new ObserverLifecycle() );
        init();
        MyLiveDataNews myLiveDataBasic = new MyLiveDataNews();
        myLiveDataBasic.getNews().observe(this, new Observer<News>() {
            @Override
            public void onChanged(News news) {
                title.setText(news.getTitle());
                comments.setText("Comments: "+String.valueOf(news.getComment()));
                Glide.with(context)
                        .load(news.getImage())
                        .into(image);
                if (news.isPremium())
                    premium.setVisibility(View.VISIBLE);
                else
                    premium.setVisibility(View.INVISIBLE);
            }
        });
        changePremiumBtn.setOnClickListener(view -> {
            myLiveDataBasic.changePremium();
        });
    }
    private void init() {
        title = findViewById(R.id.title);
        comments = findViewById(R.id.comments);
        image = findViewById(R.id.image);
        premium = findViewById(R.id.premium);
        changePremiumBtn = findViewById(R.id.changePremium);
    }
}
public class News {
    private String title;
    private String site;
    private String image;
    private boolean premium;
    private 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 String getImage() {
        return image;
    }
    public void setImage(String url) {
        this.image = url;
    }
}

Przykład jest prosty, który obrazuje jak zastosować w najprostszej formie LiveData. Przykład ma jednak wadę. Dane nie są trwałe. To znaczy, że przy każdej zmianie konfiguracji urządzenia dane są na nowo wczytywane. Na przykład przy rotacji urządzenia. Aby rozwiązać ten problem wystarczy rozszerzyć klasę MyLiveDataNewso o ViewModel. Oraz w aktywności zamienić kod:

//MyLiveDataNews myLiveDataBasic = new MyLiveDataNews();
MyLiveDataNews myLiveDataBasic = ViewModelProviders.of(this).get(MyLiveDataNews.class);

Wtedy dane będą trwałe, nawet po zniszczeniu aktywności. Więcej na ten temat pisałem w poprzednim wpisie.
Inną wadą tego przykładu jest to, że gdy aktualizujemy jedną wartość to wszystkie dane są na nowo aktualizowane w metodzie onChange(). Jeśli chcesz zaktualizować tylko jedno pole z obiektu zamiast zaktualizować cały obiekt, powinieneś zwielokrotnić MutableLiveData<>.

// View Model
private MutableLiveData<String> titleLiveData;
private MutableLiveData<String> siteLiveData;
...
titleLiveData.setValue("Title");
titleLiveData.setValue("site");
// Activity
myLiveData.getTitleLiveData().observe(this, s -> {
    // titleLiveData has been modified
});
myLiveData.getSiteLiveData().observe(this, s -> {
    // siteLiveData has been modified
});

Jest to jakieś rozwiązanie, ale sporo kodu musisz napisać. Sam musisz zdecydować i przetestować, które rozwiązanie dla Twojego projektu jest najlepsze.

Podsumowanie

Szybko i sprawnie mówiliśmy kompoenent LiveData. Opisany komponent można traktować jako pojemnik zawierający odniesienie do danych i powiadamiający potencjalnego obserwatora o zmianach tych danych. Ok co dalej?
Zachęcam do komentowania i pisania propozycji tematów, o których chciałbyś (chciałabyś) poczytać w nadchodzących wpisach.
Nie zapomnij polubić fanpage MYENV na Facebooku oraz śledzić nowe wpisy za pomocą kanału RSSTwittera. Oraz 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 🙂