Szukaj…


Uwagi

Dziwna składnia za pomocą DataBinding

Podczas wiązania funkcji viewModel z właściwością w pliku XML niektóre prefiksy funkcji, takie jak get lub is są usuwane. Na przykład. ViewModel::getFormattedText na ViewModel zmieni się na @{viewModel.formattedText} po powiązaniu go z właściwością w xml. Podobnie z ViewModel::isContentVisible -> @{viewModel.contentVisible} (notacja Java Bean)

Wygenerowane klasy powiązań, takie jak ActivityMainBinding są nazwane na podstawie pliku XML, dla którego tworzą powiązania, a nie klasy Java.

Wiązania niestandardowe

W Activity_main.xml ustawiam atrybut textColor w app a nie w przestrzeni nazw android . Dlaczego? Ponieważ dla atrybutu textColor zdefiniowano niestandardowy obiekt ustawiający, który rozwiązuje identyfikator zasobu ColorRes wysyłany przez ViewModel do rzeczywistego koloru.

public class CustomBindings {

    @TargetApi(23)
    @BindingAdapter({"bind:textColor"})
    public static void setTextColor(TextView textView, int colorResId) {
        final Context context = textView.getContext();
        final Resources resources = context.getResources();
        final int apiVersion = Build.VERSION.SDK_INT;
        int color;

        if (apiVersion >= Build.VERSION_CODES.M) {
           color = resources.getColor(colorResId, context.getTheme());
        } else {
            color = resources.getColor(colorResId);
        }

        textView.setTextColor(color);
    }

}

Aby uzyskać szczegółowe informacje na temat tego, jak to działa, sprawdź DataBinding Library: Custom Setters

Poczekaj ... w twoim xml jest logika !!!?

Można argumentować, że rzeczy, które robię w XML dla android:visibility i app:textColor są nieprawidłowe / anty-wzorce w kontekście MVVM, ponieważ w moim widoku jest logika widoku. Jednak twierdzę, że ważniejsze jest dla mnie utrzymanie zależności Androida od mojego ViewModel ze względów testowych.

Poza tym, co tak naprawdę robi app:textColor ? Rozpoznaje on tylko wskaźnik zasobów do rzeczywistego koloru z nim związanego. Więc ViewModel nadal decyduje, który kolor jest wyświetlany na podstawie pewnych warunków.

Co do android:visibility Czuję, że ze względu na nazwę tej metody można w tym miejscu użyć operatora trójskładnikowego. Ze względu na nazwę isLoadingVisible i isContentVisible naprawdę nie ma wątpliwości co do tego, co każdy wynik powinien rozwiązać w widoku. Więc wydaje mi się, że raczej wykonuje polecenie podane przez ViewModel niż naprawdę robi logikę widoku.

Z drugiej strony zgodziłbym się, że używając viewModel.isLoading ? View.VISIBLE : View.GONE byłoby złym viewModel.isLoading ? View.VISIBLE : View.GONE , ponieważ przyjmujesz założenia w widoku, co ten stan oznacza dla widoku.

Przydatny materiał

Następujące zasoby bardzo mi pomogły w zrozumieniu tej koncepcji:

Przykład MVVM z wykorzystaniem biblioteki DataBinding

Celem MVVM jest oddzielenie warstw zawierających logikę od warstwy widoku.

Na Androidzie możemy skorzystać z Biblioteki DataBinding, aby nam w tym pomóc i uczynić większość naszej logiki testowalną Jednostką, nie martwiąc się o zależności Androida.

W tym przykładzie pokażę główne komponenty głupiej prostej aplikacji, która wykonuje następujące czynności:

  • Przy uruchomieniu fałszywe połączenie sieciowe i pokaż ładujący spinner
  • Pokaż widok za pomocą licznika kliknięć TextView, komunikatu TextView i przycisku zwiększającego licznik
  • Na przycisku kliknij aktualizuj licznik i aktualizuj kolor licznika oraz tekst wiadomości, jeśli licznik osiągnie określoną liczbę

Zacznijmy od warstwy widoku:

activity_main.xml :

Jeśli nie wiesz, jak działa DataBinding, prawdopodobnie powinieneś poświęcić 10 minut na zapoznanie się z nim. Jak widać, wszystkie pola, które zwykle aktualizujesz ustawiaczami, są powiązane z funkcjami zmiennej viewModel.

Jeśli masz pytanie dotyczące android:visibility lub app:textColor właściwości app:textColor , sprawdź sekcję „Uwagi”.

 <layout
    xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools">

    <data>

        <import type="android.view.View" />

        <variable
            name="viewModel"
            type="de.walled.mvvmtest.viewmodel.ClickerViewModel"/>
    </data>

    <RelativeLayout
        android:id="@+id/activity_main"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:padding="@dimen/activity_horizontal_margin"

        tools:context="de.walled.mvvmtest.view.MainActivity">

        <LinearLayout
            android:id="@+id/click_counter"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="60dp"
            android:visibility="@{viewModel.contentVisible ? View.VISIBLE : View.GONE}"

            android:padding="8dp"

            android:orientation="horizontal">

            <TextView
                android:id="@+id/number_of_clicks"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                style="@style/ClickCounter"

                android:text="@{viewModel.numberOfClicks}"
                android:textAlignment="center"
                app:textColor="@{viewModel.counterColor}"

                tools:text="8"
                tools:textColor="@color/red"
            />

            <TextView
                android:id="@+id/static_label"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:layout_marginLeft="4dp"
                android:layout_marginStart="4dp"
                style="@style/ClickCounter"

                android:text="@string/label.clicks"
                app:textColor="@{viewModel.counterColor}"
                android:textAlignment="center"

                tools:textColor="@color/red"
            />
        </LinearLayout>


        <TextView
            android:id="@+id/message"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/click_counter"
            android:layout_centerHorizontal="true"
            android:visibility="@{viewModel.contentVisible ? View.VISIBLE : View.GONE}"

            android:text="@{viewModel.labelText}"
            android:textAlignment="center"
            android:textSize="18sp"

            tools:text="You're bad and you should feel bad!"
        />

        <Button
            android:id="@+id/clicker"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_below="@id/message"
            android:layout_centerHorizontal="true"
            android:layout_marginTop="8dp"
            android:visibility="@{viewModel.contentVisible ? View.VISIBLE : View.GONE}"

            android:padding="8dp"

            android:text="@string/label.button"

            android:onClick="@{() -> viewModel.onClickIncrement()}"
        />

        <android.support.v4.widget.ContentLoadingProgressBar
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="90dp"
            android:layout_centerHorizontal="true"
            style="@android:style/Widget.ProgressBar.Inverse"
            android:visibility="@{viewModel.loadingVisible ? View.VISIBLE : View.GONE}"

            android:indeterminate="true"
        />

    </RelativeLayout>

</layout>

Następnie warstwa modelu. Oto mam:

  • dwa pola reprezentujące stan aplikacji
  • pobierający odczytać liczbę kliknięć i stan podniecenia
  • metoda zwiększania liczby kliknięć
  • metoda przywracania poprzedniego stanu (ważna dla zmian orientacji)

Tutaj również definiuję „stan podniecenia”, który zależy od liczby kliknięć. Będzie to później wykorzystane do aktualizacji koloru i komunikatu w Widoku.

Należy zauważyć, że w modelu nie ma żadnych założeń dotyczących sposobu wyświetlania stanu użytkownikowi!

ClickerModel.java

import com.google.common.base.Optional;

import de.walled.mvvmtest.viewmodel.ViewState;

public class ClickerModel implements IClickerModel {

    private int numberOfClicks;
    private Excitement stateOfExcitement;

    public void incrementClicks() {
        numberOfClicks += 1;
        updateStateOfExcitement();
    }

    public int getNumberOfClicks() {
        return Optional.fromNullable(numberOfClicks).or(0);
    }

    public Excitement getStateOfExcitement() {
        return Optional.fromNullable(stateOfExcitement).or(Excitement.BOO);
    }

    public void restoreState(ViewState state) {
        numberOfClicks = state.getNumberOfClicks();
        updateStateOfExcitement();
    }

    private void updateStateOfExcitement() {
        if (numberOfClicks < 10) {
            stateOfExcitement = Excitement.BOO;
        } else if (numberOfClicks <= 20) {
            stateOfExcitement = Excitement.MEH;
        } else {
            stateOfExcitement = Excitement.WOOHOO;
        }
    }
}

Następnie ViewModel.

Spowoduje to zmiany w modelu i sformatowanie danych z modelu, aby pokazać je w widoku. Zauważ, że tutaj oceniamy, która reprezentacja GUI jest odpowiednia dla stanu podanego przez model ( resolveCounterColor i resolveLabelText ). Więc moglibyśmy na przykład łatwo zaimplementować UnderachieverClickerModel że ma niższe progi do stanu podniecenia bez dotykania żadnego kodu w ViewModel lub widoku.

Należy również pamiętać, że ViewModel nie przechowuje żadnych odniesień do obiektów widoku. Wszystkie właściwości są powiązane za pomocą adnotacji @Bindable i aktualizowane, gdy albo notifyChange() (sygnalizuje, że wszystkie właściwości muszą zostać zaktualizowane), albo notifyPropertyChanged(BR.propertyName) (sygnalizuje, że te właściwości muszą zostać zaktualizowane).

ClickerViewModel.java

import android.databinding.BaseObservable;

import android.databinding.Bindable;
import android.support.annotation.ColorRes;
import android.support.annotation.StringRes;
    
import com.android.databinding.library.baseAdapters.BR;
    
import de.walled.mvvmtest.R;
import de.walled.mvvmtest.api.IClickerApi;
import de.walled.mvvmtest.model.Excitement;
import de.walled.mvvmtest.model.IClickerModel;
import rx.Observable;

public class ClickerViewModel extends BaseObservable {

    private final IClickerApi api;
    boolean isLoading = false;
    private IClickerModel model;

    public ClickerViewModel(IClickerModel model, IClickerApi api) {
        this.model = model;
        this.api = api;
    }

    public void onClickIncrement() {
        model.incrementClicks();
        notifyChange();
    }

    public ViewState getViewState() {
        ViewState viewState = new ViewState();
        viewState.setNumberOfClicks(model.getNumberOfClicks());
        return viewState;
    }

    public Observable<ViewState> loadData() {
        isLoading = true;
        return api.fetchInitialState()
                .doOnNext(this::initModel)
                .doOnTerminate(() -> {
                    isLoading = false;
                    notifyPropertyChanged(BR.loadingVisible);
                    notifyPropertyChanged(BR.contentVisible);
                });
    }

    public void initFromSavedState(ViewState savedState) {
        initModel(savedState);
    }

    @Bindable
    public String getNumberOfClicks() {
        final int clicks = model.getNumberOfClicks();
        return String.valueOf(clicks);
    }

    @Bindable
    @StringRes
    public int getLabelText() {
        final Excitement stateOfExcitement = model.getStateOfExcitement();
        return resolveLabelText(stateOfExcitement);
    }

    @Bindable
    @ColorRes
    public int getCounterColor() {
        final Excitement stateOfExcitement = model.getStateOfExcitement();
        return resolveCounterColor(stateOfExcitement);
    }

    @Bindable
    public boolean isLoadingVisible() {
        return isLoading;
    }

    @Bindable
    public boolean isContentVisible() {
        return !isLoading;
    }

    private void initModel(final ViewState viewState) {
        model.restoreState(viewState);
        notifyChange();
    }

    @ColorRes
    private int resolveCounterColor(Excitement stateOfExcitement) {
        switch (stateOfExcitement) {
            case MEH:
                return R.color.yellow;
            case WOOHOO:
                return R.color.green;
            default:
                return R.color.red;
        }
    }

    @StringRes
    private int resolveLabelText(Excitement stateOfExcitement) {
        switch (stateOfExcitement) {
            case MEH:
                return R.string.label_indifferent;
            case WOOHOO:
                return R.string.label_excited;
            default:
                return R.string.label_negative;
        }
    }

}

Wiązanie wszystkiego razem w działaniu!

Tutaj widzimy widok inicjujący viewModel ze wszystkimi niezbędnymi zależnościami, które muszą być tworzone z kontekstu Androida.

Po zainicjowaniu viewModel jest on powiązany z układem xml poprzez DataBindingUtil (proszę sprawdzić sekcję „Składnia”, aby nazwać generowane klasy).

Uwaga: subskrypcje są subskrybowane na tej warstwie, ponieważ musimy poradzić sobie z ich anulowaniem, gdy aktywność jest wstrzymana lub zniszczona, aby uniknąć wycieków pamięci i NPE. Wywoływane jest również utrzymywanie i ponowne ładowanie widoku Stan na Orientacji Zmiany

MainActivity.java

import android.databinding.DataBindingUtil;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;

import de.walled.mvvmtest.R;
import de.walled.mvvmtest.api.ClickerApi;
import de.walled.mvvmtest.api.IClickerApi;
import de.walled.mvvmtest.databinding.ActivityMainBinding;
import de.walled.mvvmtest.model.ClickerModel;
import de.walled.mvvmtest.viewmodel.ClickerViewModel;
import de.walled.mvvmtest.viewmodel.ViewState;
import rx.Subscription;
import rx.subscriptions.Subscriptions;

public class MainActivity extends AppCompatActivity {

    private static final String KEY_VIEW_STATE = "state.view";

    private ClickerViewModel viewModel;
    private Subscription fakeLoader = Subscriptions.unsubscribed();

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);

        // would usually be injected but I feel Dagger would be out of scope
        final IClickerApi api = new ClickerApi();
        setupViewModel(savedInstanceState, api);

        ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
        binding.setViewModel(viewModel);
    }

    @Override
    protected void onPause() {
        fakeLoader.unsubscribe();
        super.onPause();
    }

    @Override
    protected void onDestroy() {
        fakeLoader.unsubscribe();
        super.onDestroy();
    }

    @Override
    protected void onSaveInstanceState(Bundle outState) {
        outState.putSerializable(KEY_VIEW_STATE, viewModel.getViewState());
    }

    private void setupViewModel(Bundle savedInstance, IClickerApi api) {
        viewModel = new ClickerViewModel(new ClickerModel(), api);
        final ViewState savedState = getViewStateFromBundle(savedInstance);

        if (savedState == null) {
            fakeLoader = viewModel.loadData().subscribe();
        } else {
            viewModel.initFromSavedState(savedState);
        }
    }

    private ViewState getViewStateFromBundle(Bundle savedInstance) {
        if (savedInstance != null) {
            return (ViewState) savedInstance.getSerializable(KEY_VIEW_STATE);
        }
        return null;
    }
}

Aby zobaczyć wszystko w akcji, sprawdź ten przykładowy projekt .



Modified text is an extract of the original Stack Overflow Documentation
Licencjonowany na podstawie CC BY-SA 3.0
Nie związany z Stack Overflow