Sök…


Anmärkningar

Syntax-förfrågningar med DataBinding

När binda en Viewmodel funktion till en fastighet i XML vissa funktions prefix som get eller is tappas. T.ex. ViewModel::getFormattedText på ViewModel blir @{viewModel.formattedText} när det binds till en egenskap i xml. På liknande sätt med ViewModel::isContentVisible -> @{viewModel.contentVisible} (Java Bean-notation)

De genererade bindningsklasserna som ActivityMainBinding namnges efter den xml de skapar bindningar för, inte java-klassen.

Anpassade bindningar

I Activity_main.xml ställde jag in textColor-attributet på app och inte på android namnområdet. Varför är det så? Eftersom det finns en anpassad setter definierad för attributet textColor som löser ett ColorRes-resurs-ID som skickas ut av ViewModel till en faktisk färg.

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

}

För information om hur detta fungerar, kolla DataBinding Library: Custom Setters

Vänta ... det finns logik i din xml !!!

Du kan hävda att de saker jag gör i xml för android:visibility och app:textColor är fel / antimönster i MVVM-sammanhanget eftersom det finns visningslogik enligt min åsikt. Men jag skulle hävda att det är viktigare för mig att hålla androidberoende från min ViewModel av testskäl.

Vad gör app:textColor ? Den löser bara en resurspekare till den faktiska färgen som är associerad med den. Så ViewModel bestämmer fortfarande vilken färg som ska visas baserat på något tillstånd.

När det gäller android:visibility Jag känner på grund av hur metoden heter det är det faktiskt okej att använda den ternära operatören här. På grund av namnet isLoadingVisible och isContentVisible finns det verkligen ingen tvekan om vad varje utfall borde lösa i vyn. Så jag känner att det är snarare att utföra ett kommando som ges av ViewModel än att verkligen göra visningslogik.

Å andra sidan skulle jag hålla med om att använda viewModel.isLoading ? View.VISIBLE : View.GONE skulle vara en dålig sak att göra eftersom du gör antaganden i vyn vad det tillståndet betyder för vyn.

Användbart material

Följande resurser har hjälpt mig mycket att försöka förstå detta koncept:

MVVM Exempel med användning av DataBinding Library

Målet med MVVM är att separera lager som innehåller logik från visningsskiktet.

På Android kan vi använda DataBinding-biblioteket för att hjälpa oss med detta och göra det mesta av vår logiska enhet-testbar utan att oroa oss för Android-beroenden.

I det här exemplet visar jag de centrala komponenterna för en dum enkel app som gör följande:

  • Vid starten startar du ett nätverkssamtal och visar en laddningsspinner
  • Visa en vy med en klickräknare TextView, ett meddelande TextView och en knapp för att öka räknaren
  • På knappen klickar du på uppdateringsräknare och uppdaterar räknarfärg och meddelandetext om räknaren når något nummer

Låt oss börja med vynlagret:

activity_main.xml :

Om du inte känner till hur DataBinding fungerar bör du antagligen ta tio minuter att bekanta dig med det. Som du ser är alla fält som du vanligtvis uppdaterar med inställare bundna till funktioner i variabeln viewModel.

Om du har en fråga om android:visibility eller app:textColor avsnittet "Kommentarer".

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

Nästa modellskikt. Här har jag:

  • två fält som representerar appens tillstånd
  • bokstäver för att läsa antalet klick och spänningstillståndet
  • en metod för att öka mitt klickantal
  • en metod för att återställa vissa tidigare tillstånd (viktigt för orienteringsändringar)

Här definierar jag också ett "spänningstillstånd" som är beroende av antalet klick. Detta kommer senare att användas för att uppdatera färg och meddelande i vyn.

Det är viktigt att notera att det inte finns några antaganden i modellen om hur tillståndet kan visas för användaren!

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

Nästa ViewModel.

Detta utlöser förändringar på modellen och formaterar data från modellen för att visa dem i vyn. Observera att det är här vi utvärderar vilken GUI-representation som är lämplig för det tillstånd som ges av modellen ( resolveCounterColor och resolveLabelText ). Så vi kan till exempel enkelt implementera en UnderachieverClickerModel som har lägre trösklar för spänningstillståndet utan att röra någon kod i viewModel eller view.

Observera också att ViewModel inte har några referenser för att visa objekt. Alla egenskaper är bundna via @Bindable kommentarerna och uppdateras när antingen notifyChange() (signalerar att alla egenskaper måste uppdateras) eller notifyPropertyChanged(BR.propertyName) (signalerar att dessa egenskaper måste uppdateras).

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

}

Binda det hela tillsammans i aktiviteten!

Här ser vi vyn som initialiserar viewModel med alla beroenden den kan behöva, som måste instanseras från ett Android-sammanhang.

När viewModel har initierats är det bundet till xml-layouten via DataBindingUtil (se "Syntax" -avsnittet för namngivning av genererade klasser).

Anteckningsabonnemang prenumereras på detta lager eftersom vi måste hantera att avbeställa dem när aktiviteten är pausad eller förstörd för att undvika minnesläckor och NPE. Här fortsätter och fortsätter och ladda om vynState on OrientationChanges

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

För att se allt i aktion, kolla in detta exempelprojekt .



Modified text is an extract of the original Stack Overflow Documentation
Licensierat under CC BY-SA 3.0
Inte anslutet till Stack Overflow