Suche…


Bemerkungen

Syntax-Macken mit DataBinding

Beim Binden einer viewModel-Funktion an eine Eigenschaft in xml werden bestimmte Funktionspräfixe wie get oder is gelöscht. Z.B. ViewModel::getFormattedText auf dem ViewModel wird zu @{viewModel.formattedText} wenn es an eine Eigenschaft in XML gebunden wird. Ähnlich bei ViewModel::isContentVisible -> @{viewModel.contentVisible} (Java Bean-Notation)

Die generierten Bindungsklassen wie ActivityMainBinding sind nach der XML-Datei benannt, für die sie Bindungen erstellen, nicht nach der Java-Klasse.

Kundenspezifische Bindungen

In der activity_main.xml habe ich das textColor-Attribut für die app und nicht den android Namespace festgelegt. Warum das? Weil für das Attribut textColor ein benutzerdefinierter Setter definiert ist, der eine vom ViewModel textColor ColorRes-Ressourcen-ID in eine tatsächliche Farbe auflöst.

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);
    }

}

Einzelheiten zur Funktionsweise von DataBinding Library: Custom Setters finden Sie hier

Warten Sie ... es gibt Logik in Ihrer XML !!!?

Sie könnten argumentieren, dass die Dinge, die ich in XML für android:visibility und app:textColor falsche / Anti-Muster im MVVM-Kontext sind, da in meiner Ansicht Ansichtslogik app:textColor . Ich würde jedoch argumentieren, dass es für mich wichtiger ist, Android-Abhängigkeiten zu Testzwecken aus meinem ViewModel herauszuhalten.

Was macht app:textColor wirklich? Es löst nur einen Ressourcenzeiger auf die tatsächlich zugeordnete Farbe auf. Das ViewModel entscheidet also immer noch, welche Farbe aufgrund bestimmter Bedingungen angezeigt wird.

Bei android:visibility empfinde ich aufgrund der Benennung der Methode als richtig, den ternären Operator hier zu verwenden. Aufgrund der Namen isLoadingVisible und isContentVisible besteht kein Zweifel daran, in was jedes Ergebnis in der Ansicht aufgelöst werden soll. Ich habe also das Gefühl, dass es eher einen Befehl ausführt, der vom ViewModel gegeben wird, als die View-Logik.

Andererseits stimme ich zu, dass viewModel.isLoading ? View.VISIBLE : View.GONE wäre eine schlechte Sache, weil Sie Annahmen in der Ansicht machen, was dieser Zustand für die Ansicht bedeutet.

Nützliches Material

Die folgenden Ressourcen haben mir sehr geholfen, dieses Konzept zu verstehen:

MVVM-Beispiel mit DataBinding-Bibliothek

Der springende Punkt von MVVM besteht darin, Layer mit Logik von der Ansichtsebene zu trennen.

Auf Android können wir die DataBinding-Bibliothek verwenden , um uns dabei zu helfen und die meisten unserer Logikeinheitstests zu testen, ohne sich über Android-Abhängigkeiten Gedanken zu machen.

In diesem Beispiel zeige ich die zentralen Komponenten für eine dumme einfache App, die Folgendes ausführt:

  • Beim Start fälschen Sie einen Netzwerkanruf und zeigen einen ladenden Spinner
  • Zeigen Sie eine Ansicht mit einem Klickzähler TextView, einer Nachricht TextView und einer Schaltfläche zum Erhöhen des Zählers an
  • Klicken Sie auf die Schaltfläche "Zähler aktualisieren" und aktualisieren Sie die Zählerfarbe und den Nachrichtentext, wenn der Zähler eine bestimmte Anzahl erreicht

Beginnen wir mit der Ansichtsebene:

activity_main.xml :

Wenn Sie mit der Funktionsweise von DataBinding nicht vertraut sind, sollten Sie sich wahrscheinlich zehn Minuten Zeit nehmen , um sich damit vertraut zu machen. Wie Sie sehen, sind alle Felder, die Sie normalerweise mit Setters aktualisieren würden, an Funktionen der viewModel-Variablen gebunden.

Wenn Sie eine Frage zu android:visibility oder app:textColor Eigenschaften haben, überprüfen Sie den Abschnitt "Anmerkungen".

 <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>

Als nächstes die Modellebene. Hier habe ich:

  • zwei Felder, die den Status der App darstellen
  • Getter, um die Anzahl der Klicks und den Erregungszustand zu lesen
  • eine Methode, um meine Klickanzahl zu erhöhen
  • eine Methode zum Wiederherstellen eines früheren Zustands (wichtig für Orientierungsänderungen)

Auch definiere ich hier einen "Aufregungszustand", der von der Anzahl der Klicks abhängig ist. Dies wird später verwendet, um Farbe und Nachricht in der Ansicht zu aktualisieren.

Es ist wichtig zu beachten, dass im Modell keine Annahmen darüber gemacht werden, wie der Status für den Benutzer angezeigt werden kann!

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;
        }
    }
}

Als nächstes das ViewModel.

Dadurch werden Änderungen am Modell und an den Formatdaten des Modells ausgelöst, um sie in der Ansicht anzuzeigen. Beachten Sie, dass hier ausgewertet wird, welche GUI-Darstellung für den vom Modell resolveCounterColor resolveLabelText geeignet ist ( resolveCounterColor und resolveLabelText ). So könnten wir beispielsweise problemlos ein UnderachieverClickerModel implementieren, das niedrigere Schwellenwerte für den Erregungszustand aufweist, ohne Code im viewModel oder in der Ansicht zu berühren.

Beachten Sie auch, dass das ViewModel keine Referenzen auf Ansichtsobjekte enthält. Alle Eigenschaften werden über die Annotationen @Bindable gebunden und aktualisiert, wenn entweder notifyChange() (signalisiert, dass alle Eigenschaften aktualisiert werden müssen) oder notifyPropertyChanged(BR.propertyName) (bedeutet, dass diese Eigenschaften aktualisiert werden müssen).

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;
        }
    }

}

Alles in der Aktivität zusammen binden!

Hier sehen wir die Ansicht, die das viewModel mit allen Abhängigkeiten initialisiert, die es möglicherweise benötigt, die aus einem Android-Kontext instanziiert werden müssen.

Nachdem das viewModel initialisiert wurde, wird es über das DataBindingUtil an das XML-Layout gebunden. (Benennen Sie die generierten Klassen bitte im Abschnitt "Syntax").

Hinweis Abonnements werden auf dieser Ebene abonniert, da das Abbestellen der Abonnements erforderlich ist, wenn die Aktivität angehalten oder zerstört wird, um Speicherlecks und NPEs zu vermeiden. Hier wird auch das persistierende und erneute Laden des viewState bei OrientationChanges ausgelöst

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;
    }
}

Um alles in Aktion zu sehen, schauen Sie sich dieses Beispielprojekt an .



Modified text is an extract of the original Stack Overflow Documentation
Lizenziert unter CC BY-SA 3.0
Nicht angeschlossen an Stack Overflow