Ricerca…


Osservazioni

Sintassi stranezze con DataBinding

Quando si associa una funzione viewModel a una proprietà in xml, determinati prefissi di funzione come get o is vengono rilasciati. Per esempio. ViewModel::getFormattedText sul ViewModel diventerà @{viewModel.formattedText} quando lo si lega a una proprietà in xml. Analogamente con ViewModel::isContentVisible -> @{viewModel.contentVisible} (notazione Java Bean)

Le classi di bind generate come ActivityMainBinding prendono il nome dal xml per cui stanno creando i collegamenti per, non la classe java.

Binding personalizzati

In activity_main.xml ho impostato l'attributo textColor app e non lo spazio dei nomi di android . Perché? Poiché esiste un setter personalizzato definito per l'attributo textColor che risolve un ID risorsa ColorRes inviato da ViewModel a un colore effettivo.

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

}

Per i dettagli su come funziona controlla DataBinding Library: Custom Setters

Aspetta ... c'è una logica nel tuo xml !!!?

Si potrebbe argomentare che le cose che faccio in xml per android:visibility e app:textColor sono errate / anti-pattern nel contesto MVVM perché c'è una logica di visualizzazione nella mia vista. Tuttavia, direi che per me è più importante mantenere le dipendenze Android dal mio ViewModel per motivi di test.

Inoltre, cosa fa realmente app:textColor ? Risolve solo un puntatore di risorse per il colore effettivo associato ad esso. Quindi il ViewModel decide ancora quale colore viene mostrato in base ad alcune condizioni.

Per quanto riguarda l' android:visibility che sento a causa di come viene chiamato il metodo, in realtà è ok per usare qui l'operatore ternario. Poiché il nome isLoadingVisible e isContentVisible non vi è alcun dubbio su cosa dovrebbe risolvere ogni risultato nella vista. Quindi ritengo che sia piuttosto l'esecuzione di un comando dato da ViewModel piuttosto che fare una logica di visualizzazione.

D'altra parte sono d'accordo che l'uso di viewModel.isLoading ? View.VISIBLE : View.GONE sarebbe una brutta cosa da fare perché stai facendo delle supposizioni nella vista che cosa significa questo stato per la vista.

Materiale utile

Le seguenti risorse mi hanno aiutato molto nel cercare di capire questo concetto:

Esempio MVVM utilizzando la libreria DataBinding

L'intero punto di MVVM consiste nel separare i livelli contenenti la logica dal livello di vista.

Su Android possiamo usare la libreria DataBinding per aiutarci con questo e rendere la maggior parte della nostra unità logica testabile senza preoccuparci delle dipendenze di Android.

In questo esempio mostrerò i componenti centrali per un'app semplice e stupida che fa quanto segue:

  • All'avvio simula una chiamata di rete e mostra uno spinner di caricamento
  • Mostra una vista con un contatore dei clic TextView, un messaggio TextView e un pulsante per incrementare il contatore
  • Sul contatore dei clic aggiorna contatore e aggiorna il colore del contatore e il testo del messaggio se il contatore raggiunge un numero

Iniziamo con il livello vista:

activity_main.xml :

Se non hai familiarità con il funzionamento di DataBinding dovresti probabilmente impiegare 10 minuti per familiarizzarti con esso. Come puoi vedere, tutti i campi che dovresti aggiornare con i setter sono legati alle funzioni della variabile viewModel.

Se hai una domanda su android:visibility o app:textColor proprietà app:textColor controlla la sezione "Note".

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

Successivo il livello del modello. Qui ho:

  • due campi che rappresentano lo stato dell'app
  • getter per leggere il numero di clic e lo stato di eccitazione
  • un metodo per incrementare il conteggio dei clic
  • un metodo per ripristinare alcuni stati precedenti (importante per le modifiche di orientamento)

Inoltre definisco qui uno 'stato di eccitazione' che dipende dal numero di clic. Questo verrà in seguito utilizzato per aggiornare il colore e il messaggio sulla vista.

È importante notare che non ci sono ipotesi fatte nel modello su come lo stato potrebbe essere visualizzato all'utente!

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

Avanti il ​​ViewModel.

Ciò attiverà le modifiche sul modello e formatterà i dati dal modello per mostrarli sulla vista. Si noti che è qui dove si valuta quale rappresentazione della GUI è appropriata per lo stato dato dal modello ( resolveCounterColor e resolveLabelText ). Quindi, per esempio, potremmo facilmente implementare un UnderachieverClickerModel che ha soglie più basse per lo stato di eccitazione senza toccare alcun codice nel viewModel o nella vista.

Si noti inoltre che ViewModel non contiene alcun riferimento per visualizzare oggetti. Tutte le proprietà sono associate tramite le annotazioni @Bindable e aggiornate quando notifyChange() (segnala che tutte le proprietà devono essere aggiornate) o notifyPropertyChanged(BR.propertyName) (segnala che queste proprietà devono essere aggiornate).

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

}

Legando tutto insieme nell'attività!

Qui vediamo la vista di inizializzare viewModel con tutte le dipendenze di cui potrebbe aver bisogno, che devono essere istanziate da un contesto Android.

Dopo che viewModel è inizializzato, è associato al layout xml tramite DataBindingUtil (si prega di controllare la sezione 'Sintassi' per la denominazione delle classi generate).

Gli abbonamenti alle note sono sottoscritti su questo livello perché dobbiamo gestire l'annullamento dell'iscrizione quando l'attività viene messa in pausa o distrutta per evitare perdite di memoria e NPE. Qui viene attivato anche il persistere e il ricaricamento della viewState su 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;
    }
}

Per vedere tutto in azione controlla questo esempio di progetto .



Modified text is an extract of the original Stack Overflow Documentation
Autorizzato sotto CC BY-SA 3.0
Non affiliato con Stack Overflow