Android Architecture Components: ViewModel

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

Omówiliśmy już Data BindingLifeCycles w poprzednich postach. Teraz przyszedł czas na ViewModel. Jedną z kluczowych funkcji Androida, którą zawsze lubiłem, było to, że po kilku dniach można otworzyć aplikację i być dokładnie tam, gdzie ją zostawiłem. Aby takia magia zadziałała coś musi być wdrożone. Do tego po części służy ViewModel.

Czym jest ViewModel?

Klasa ViewModel umożliwia przechowywanie danych z uwzględnieniem cyklu życia komponentu. Cechą tej biblioteki jest to, że może przetrwać ponowne utworzenie aktywności lub fragmentu w wyniku zmiany konfiguracji urządzenia, takiej jak rotacja urządzenia. Przechowuje dane związane z interfejsem użytkownika i zarządza nimi w sposób świadomy. ViewModel może być łatwo użyty zarówno z aktywnościami jak i fragmentami, które nazywamy kontrolerami UI. Oprócz tego obsługuje komunikację między aktywnościa a fragmentami. Kontrolery interfejsu użytkownika są w stanie obserwować zmiany w ViewModelu poprzez LiveData lub Data Binding. Poniższy diagram przedstawia cykl życia komponentu ViewModel.
Illustrates the lifecycle of a ViewModel as an activity changes state.

Na diagramie można zobaczyć cykl życia aktywności, która podlega rotacji telefonu, a następnie zostaje zniszczona. Żywotność ViewModel jest wyświetlana po prawej stronie. ViewModel istnieje od momentu, w którym najpierw żądasz ViewModel (zazwyczaj w metodzie onCreate()), dopóki aktywność nie zostanie zakończona lub nie zostanie zniszczone przez system. Niezależnie od tego ile razy byś obracał urządzenie ViewModel przetrwa.

Uzupełnij wiedze o: Tworzymy przewijany Toolbar

Podsumowując, ViewModel może przeżyć zmianę konfiguracji urządzenia, a mimo to nadal będziesz mieć ten sam ViewModel z tymi samymi danymi co na początku startu kontrolera. Dzięki temu:

  • nie musisz martwić się o cykl życia posiadacza danych UI,
  • dane będą zawsze aktualne — otrzymasz te same dane po obrocie telefonu. Nie trzeba przekazywać ręcznie danych do nowej aktywności,
  • jeśli wykonujesz połączenie API lub z bazą danych a wynik zostanie dostarczony, zanim aktywność zostanie odtworzona, masz pewność, że dane będą przechowywane w ViewModel i możesz je natychmiast użyć po odtworzeniu aktywności.

ViewModel vs onSaveInstanceState()

Chwilka, mamy już metodą onSaveInstanceState(), którą używamy zwykle do przechowywania i przywracania danych po zmianach konfiguracji. To po co nam kolejna jakaś biblioteka? Odpowiedź jest prosta: ViewModel nie zastępuje onSaveInstanceState().

Metoda onSaveInstanceState() może przetrwać niszczenie aktywności przez system. Funkcja onSaveInstanceState(), niestety ma też kilka wad:

  • możesz przechowywać niewielką ilość danych,
  • dane muszą być proste, więc nie jest tak łatwo wstawić i przywrócić obiekty.

Kiedy więc powinniśmy użyć metody onSaveInstanceState()? Używaj ViewModel do przechowywania rzeczywistych danych dla interfejsu użytkownika (np. lista komentarzy artykułu). A metodę onSaveInstanceState() do przechowywania niezbędnych danych (np. id artykułu).

Dzięki temu rozpatrzymy oba przypadki. Jeśli nasza aktywność zostanie zabita przez system, będziemy mogli pobrać listę komentarzy artykułu, ponieważ mamy identyfikator artykułu, który został zapisany przez onSaveInstanceState(). A kiedy dostaniemy listę, możemy ją umieścić w ViewModelu i używać nawet po zmianie konfiguracji urządzenia.

Jak korzystać z ViewModel?

Wystarczą trzy kroki:

  1. Stworzyć klasę i rozszerzyć ją o ViewModel.
  2. Skonfigurować komunikację między ViewModel a kontrolerem interfejsu użytkownika.
  3. Wykorzystać ViewModelu w aktywności lub fragmencie.

Kontroler interfejsu użytkownika musi wiedzieć o Twoim ViewModelu. Spowodowane to jest tym. że aktywność lub fragment wyświetla i aktualizuje dane, gdy występują interakcje z użytkownikiem, takie jak naciśnięcie przycisku.

ViewModels nie powinien posiadać odniesienia do kontekstu aktywności lub fragmentu. Ponadto ViewModels nie powinien zawierać elementów, które zawierają odniesienia do kontrolerów interfejsu użytkownika, takich jak widoki, ponieważ utworzy to pośrednie odniesienie do kontekstu. Powodem, dla którego nie powinieneś przechowywać tych obiektów, jest to, że ViewModel przeżywa określone instancje kontrolera interfejsu użytkownika — jeśli obrócisz trzykrotnie urządzenie, wówczas utworzyłbyś trzy różne instancje aktywności, ale masz tylko jeden ViewModel.

Istnieje jeden wyjątek od reguły. Czasami może być potrzebny kontekst aplikacji (nie mylić z kontekstem aktywności) do skorzystania z usług systemowych. Przechowywanie kontekstu aplikacji w ViewModelu jest w porządku, ponieważ kontekst aplikacji jest powiązany z cyklem życia aplikacji. Różni się od kontekstu aktywności, który jest powiązany z cyklem życia aktywności. W rzeczywistości, jeśli potrzebujesz skorzystać z usług systemowych, powinieneś rozszerzyć klasę ViewModel o AndroidViewModel.

Zasadniczo tworzysz klasę ViewModel dla każdego ekranu w aplikacji. Ta klasa ViewModel będzie przechowywać wszystkie dane powiązane z ekranem i będzie mieć metody pobierające i ustawiające dla przechowywanych danych. Dzięki temu oddzielamy kod odpowiedzialny za wyświetlenie interfejs użytkownika od  danych, które teraz znajdują się w ViewModelu.

Uzupełnij wiedze o: findViewById w pętli for

Trochę kodu

Poniżej została przedstawiona klasa Activity oraz ViewModel. W klasie Activity pobieramy ViewModel, a następnie pobieramy dane i przypisujemy je do klasy News. Dalej ustawiamy wartości na layoucie. W klasie ViewModel mamy metodę loadNews() ustawiłem na sztywno dane, ale dobrym pomysłem jest stworzenie modułów pobierających i ustawiających w celu lepszego enkapsulacji danych. Metoda ta jest raz wykonywana podczas pierwszego uruchomienia aktywności. Zgodnie z wcześniejszymi informacjami klasa przechowuje dane aż do zniszczenia aktywności. Po zmianie konfiguracji urządzenia będzie tylko wywoływana metoda getNews().

public class ViewModelActivity extends AppCompatActivity {
    TextView title, comments;
    ImageView image, premium;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.viewmodel_basic_activity);
        init();
        MyViewModelBasic model = ViewModelProviders.of(this).get(MyViewModelBasic.class);
        News news = model.getNews();
        title.setText(news.getTitle());
        comments.setText("Comments: "+String.valueOf(news.getComment()));
        Glide.with(this)
                .load(news.getImage())
                .into(image);
        if (news.isPremium())
            premium.setVisibility(View.VISIBLE);
    }
    private void init() {
        title = findViewById(R.id.title);
        comments = findViewById(R.id.comments);
        image = findViewById(R.id.image);
        premium = findViewById(R.id.premium);
    }
}
public class MyViewModelBasic extends ViewModel {
    private News news;
    public News getNews() {
        if (news == null) {
            loadNews();
        }
        return news;
    }
     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(true);
    }
}
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_height="match_parent"
    android:layout_width="match_parent">
    <ImageView
        android:id="@+id/premium"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:src="@android:drawable/star_big_on"
        android:visibility="invisible"
        android:layout_margin="10dp"
        app:layout_constraintRight_toRightOf="parent"
        app:layout_constraintTop_toTopOf="parent"
        />
    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="513dp"
        android:orientation="vertical" >
        <ImageView
            android:id="@+id/image"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_gravity="center_horizontal"
            android:src="@drawable/ic_launcher_background" />
        <TextView
            android:id="@+id/title"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="10dp"
            android:textStyle="bold"
            android:textSize="20sp" />
        <TextView
            android:id="@+id/comments"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="10dp"
            android:textStyle="italic" />
        <Button
            android:id="@+id/changePremium"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Change premium" />
    </LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

Aktualizacja danych

Ok, mamy aktywność, dane się wyświetlają. Teraz musimy jakoś pomyśleć nad dynamiczną zmianą danych. Rozbudowujmy nasz przykład o przycisk, który będzie ustawiał czy artykuł jest premium czy nie. Oto kod, który musisz dodać do aktywności i klasy ViewModel.

Button changePremiumBtn = findViewById(R.id.changePremium);
changePremiumBtn.setOnClickListener(view -> {
            model.changePremium();
            if (news.isPremium())
                premium.setVisibility(View.VISIBLE);
            else
                premium.setVisibility(View.INVISIBLE);
        });
void changePremium() {
        if (news.isPremium())
            news.setPremium(false);
        else
            news.setPremium(true);
        Log.e("TAG", "premium= "+  news.isPremium() );
    }

Gdy będziemy naciskać przycisk, nasza gwiazdka będzie się wyświetlać w zależności od tego czy artykuł jest premium czy nie. Nie jest to może elegancki sposób, ale celem tego wpisu było pokazanie w najprostrzy sposób jak działa omawiany komponent. A skoro znamy już Data Binding to warto go tutaj wdrożyć 🙂

Podsumowanie

ViewModel jest bardzo fajny do oddzielania kodu kontrolera od danych, które wypełniają interfejs użytkownika. Nie jest lekarstwem na utrwalanie danych i zapisywanie stanu aplikacji. Następnym razem zajmiemy się komponentem LiveData.
Zachęcam do komentowania i pisania propozycji tematów, o których chciałbyś poczytać w nadchodzących wpisach.
Nie zapomnij polubić stronę 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 🙂