Zoeken…


Opmerkingen

Syntax-eigenaardigheden met DataBinding

Bij het binden van een viewModel-functie aan een eigenschap in xml worden bepaalde functievoorvoegsels zoals get of is . Bijv. ViewModel::getFormattedText op het ViewModel wordt @{viewModel.formattedText} wanneer het wordt @{viewModel.formattedText} aan een eigenschap in xml. Op dezelfde manier met ViewModel::isContentVisible -> @{viewModel.contentVisible} (Java Bean-notatie)

De gegenereerde bindklassen zoals ActivityMainBinding zijn vernoemd naar de xml waarvoor ze bindingen maken, niet naar de Java-klasse.

Aangepaste bindingen

In de activity_main.xml heb ik het kenmerk textColor ingesteld op de app en niet de naamruimte van android . Waarom is dat? Omdat er een aangepaste setter is gedefinieerd voor het kenmerk textColor die een ColorRes-resource-ID oplost dat door het ViewModel naar een werkelijke kleur wordt verzonden.

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

}

Raadpleeg DataBinding Library: Custom Setters voor meer informatie over hoe dit werkt

Wacht ... er zit logica in je xml !!!?

Je zou kunnen beweren dat de dingen die ik doe in xml voor android:visibility en app:textColor zijn verkeerd / anti-patronen in de MVVM-context omdat er volgens mij app:textColor is. Ik zou echter beweren dat het voor mij belangrijker is om Android-afhankelijkheden uit mijn ViewModel te houden om testredenen.

Trouwens, wat doet app:textColor eigenlijk? Hiermee wordt een ressource-aanwijzer alleen omgezet in de werkelijke kleur die eraan is gekoppeld. Dus het ViewModel bepaalt nog steeds welke kleur wordt weergegeven op basis van een voorwaarde.

Wat betreft de android:visibility voel ik vanwege de naam van de methode, het is eigenlijk prima om de ternaire operator hier te gebruiken. Vanwege de naam isLoadingVisible en isContentVisible er echt geen twijfel over wat elke uitkomst zou moeten oplossen in de weergave. Dus ik heb het gevoel dat het eerder een commando van het ViewModel uitvoert dan dat het viewlogica echt uitvoert.

Aan de andere kant zou ik het ermee eens zijn dat viewModel.isLoading ? View.VISIBLE : View.GONE zou een slechte zaak zijn om te doen, omdat je aannames maakt in de weergave wat die status voor de weergave betekent.

Nuttig materiaal

De volgende bronnen hebben me enorm geholpen om dit concept te begrijpen:

MVVM-voorbeeld met behulp van DataBinding-bibliotheek

Het hele punt van MVVM is om lagen die logica bevatten te scheiden van de beeldlaag.

Op Android kunnen we de DataBinding-bibliotheek gebruiken om ons hierbij te helpen en het grootste deel van onze logische unit-testbaar te maken zonder ons zorgen te maken over Android-afhankelijkheden.

In dit voorbeeld laat ik de centrale componenten zien voor een domme eenvoudige app die het volgende doet:

  • Bij het opstarten nep een netwerkoproep en toon een laadspinner
  • Toon een weergave met een clickteller TextView, een bericht TextView en een knop om de teller te verhogen
  • Klik op knop update teller en update tellerkleur en berichttekst als teller een aantal bereikt

Laten we beginnen met de viewlaag:

activity_main.xml :

Als u niet bekend bent met hoe DataBinding werkt, moet u er waarschijnlijk 10 minuten over doen om ermee vertrouwd te raken. Zoals u kunt zien, zijn alle velden die u gewoonlijk bij de instellingen zou bijwerken, gebonden aan functies in de viewModel-variabele.

Als je een vraag hebt over de android:visibility of app:textColor eigenschappen, kijk dan in het gedeelte 'Opmerkingen'.

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

Vervolgens de modellaag. Hier heb ik:

  • twee velden die de status van de app vertegenwoordigen
  • getters om het aantal klikken en de staat van opwinding te lezen
  • een methode om mijn aantal klikken te verhogen
  • een methode om een eerdere status te herstellen (belangrijk voor oriëntatiewijzigingen)

Ook definieer ik hier een 'staat van opwinding' die afhankelijk is van het aantal klikken. Dit wordt later gebruikt om de kleur en het bericht in de weergave bij te werken.

Het is belangrijk op te merken dat er in het model geen veronderstellingen worden gedaan over hoe de status aan de gebruiker kan worden weergegeven!

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

Vervolgens het ViewModel.

Hierdoor worden wijzigingen in het model geactiveerd en worden gegevens uit het model opgemaakt om ze in de weergave te tonen. Merk op dat we hier evalueren welke GUI-weergave geschikt is voor de status die door het model wordt gegeven ( resolveCounterColor en resolveLabelText ). Dus we kunnen bijvoorbeeld gemakkelijk een UnderachieverClickerModel implementeren dat lagere drempels heeft voor de staat van opwinding zonder code in het viewModel of view aan te raken.

Merk ook op dat het ViewModel geen verwijzingen bevat om objecten te bekijken. Alle eigenschappen zijn gebonden via de @Bindable annotaties en bijgewerkt wanneer ofwel notifyChange() (signalen dat alle eigenschappen moeten worden bijgewerkt) of notifyPropertyChanged(BR.propertyName) (signalen dat deze eigenschappen moeten worden bijgewerkt) wordt bijgewerkt.

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 samenbinden in de activiteit!

Hier zien we de weergave die het viewModel initialiseert met alle afhankelijkheden die het nodig heeft, die moeten worden geïnstantieerd vanuit een Android-context.

Nadat het viewModel is geïnitialiseerd, is het gebonden aan de xml-indeling via de DataBindingUtil (controleer de sectie 'Syntaxis' voor het benoemen van gegenereerde klassen).

Op abonnementen wordt geabonneerd op deze laag omdat we moeten afhandelen wanneer ze worden onderbroken of de activiteit wordt onderbroken om geheugenlekken en NPE's te voorkomen. Ook wordt het aanhouden en opnieuw laden van de viewState op OrientationChanges hier geactiveerd

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

Bekijk dit voorbeeldproject om alles in actie te zien.



Modified text is an extract of the original Stack Overflow Documentation
Licentie onder CC BY-SA 3.0
Niet aangesloten bij Stack Overflow