Recherche…


Remarques

Syntaxe quirks avec DataBinding

Lors de la liaison d'une fonction viewModel à une propriété dans xml, certains préfixes de fonctions tels que get ou is sont supprimés. Par exemple. ViewModel::getFormattedText sur le ViewModel deviendra @{viewModel.formattedText} lors de la liaison à une propriété au format XML. De même avec ViewModel::isContentVisible -> @{viewModel.contentVisible} (notation Java Bean)

Les classes de liaison générées, telles que ActivityMainBinding portent le nom du fichier XML pour lequel elles créent des liaisons, et non la classe Java.

Fixations personnalisées

Dans le fichier activity_main.xml, je définis l'attribut textColor sur l' app et non l'espace de noms android . Pourquoi donc? Étant donné qu'un attribut personnalisé est défini pour l'attribut textColor qui résout un ID de ressource ColorRes envoyé par ViewModel à une couleur réelle.

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

}

Pour plus d'informations sur son fonctionnement, consultez DataBinding Library: Custom Setters

Attendez ... il y a de la logique dans votre xml !!!

Vous pourriez faire valoir que les choses que je fais dans XML pour android:visibility et app:textColor sont fausses / anti-patterns dans le contexte MVVM car il y a une logique de vue à mon avis. Cependant, je pense qu'il est plus important pour moi de garder les dépendances Android hors de mon ViewModel pour des raisons de test.

En outre, que fait vraiment app:textColor ? Il ne résout qu'un pointeur de ressource sur la couleur réelle qui lui est associée. Donc, le ViewModel décide toujours quelle couleur est affichée en fonction de certaines conditions.

En ce qui concerne l' android:visibility me semble liée à la manière dont la méthode est nommée. Il est donc normal d'utiliser l'opérateur ternaire ici. En raison du nom isLoadingVisible et isContentVisible il n'y a vraiment aucun doute sur la résolution de chaque résultat dans la vue. Je pense donc qu’il s’agit plutôt d’exécuter une commande donnée par ViewModel que de faire de la logique de vue.

D'un autre côté, je suis d'accord que l'utilisation de viewModel.isLoading ? View.VISIBLE : View.GONE serait une mauvaise chose à faire car vous faites des suppositions dans la vue de ce que cet état signifie pour la vue.

Matériel utile

Les ressources suivantes m'ont beaucoup aidé à comprendre ce concept:

Exemple MVVM utilisant la bibliothèque DataBinding

L'intérêt de MVVM est de séparer les couches contenant la logique de la couche de vue.

Sur Android, nous pouvons utiliser la bibliothèque DataBinding pour nous aider avec ceci et rendre la plus grande partie de notre logique testable sans nous soucier des dépendances Android.

Dans cet exemple, je vais montrer les composants centraux d'une application simple et stupide qui effectue les opérations suivantes:

  • Au démarrage, simulez un appel réseau et affichez un compteur de chargement
  • Afficher une vue avec un compteur de clics TextView, un message TextView et un bouton pour incrémenter le compteur
  • Sur le bouton cliquez sur le compteur de mise à jour et mettez à jour la couleur du compteur et le texte du message si le compteur atteint un certain nombre

Commençons par la couche de vue:

activity_main.xml :

Si vous ne connaissez pas le fonctionnement de DataBinding, vous devriez probablement prendre 10 minutes pour vous familiariser avec celui-ci. Comme vous pouvez le voir, tous les champs que vous mettrez à jour avec setters sont liés à des fonctions de la variable viewModel.

Si vous avez une question à propos de l' android:visibility propriétés android:visibility ou app:textColor , consultez la section "Remarques".

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

Suivant la couche modèle. J'ai ici:

  • deux champs qui représentent l'état de l'application
  • les getters à lire le nombre de clics et l'état d'excitation
  • une méthode pour incrémenter mon nombre de clics
  • une méthode pour restaurer un état précédent (important pour les changements d'orientation)

Je définis également ici un «état d'excitation» qui dépend du nombre de clics. Cela sera utilisé plus tard pour mettre à jour la couleur et le message sur la vue.

Il est important de noter qu'il n'y a pas d'hypothèses dans le modèle sur la manière dont l'état peut être affiché pour l'utilisateur!

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

Suivant le ViewModel.

Cela déclenchera des modifications sur le modèle et formatera les données du modèle pour les afficher dans la vue. Notez que c'est ici que nous évaluons quelle représentation graphique est appropriée pour l'état donné par le modèle ( resolveCounterColor et resolveLabelText ). Ainsi, nous pourrions par exemple facilement implémenter un UnderachieverClickerModel qui présente des seuils inférieurs pour l’état d’excitation sans toucher au code de viewModel ou de la vue.

Notez également que ViewModel ne contient aucune référence pour afficher les objets. Toutes les propriétés sont liées via les annotations @Bindable et mises à jour lorsque notifyChange() (signale que toutes les propriétés doivent être mises à jour) ou notifyPropertyChanged(BR.propertyName) (signale que ces propriétés doivent être mises à jour).

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

}

Lier tout ensemble dans l'activité!

Nous voyons ici la vue initialisant le viewModel avec toutes les dépendances dont il pourrait avoir besoin, qui doivent être instanciées à partir d'un contexte android.

Une fois le viewModel initialisé, il est lié à la mise en page XML par le biais de DataBindingUtil (veuillez vérifier la section "Syntaxe" pour la dénomination des classes générées).

Notez que les abonnements sont abonnés à cette couche car nous devons les désabonner lorsque l'activité est suspendue ou détruite pour éviter les fuites de mémoire et les NPE. La persistance et le rechargement de la vueState on OrientationChanges se déclenchent ici

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

Pour voir tout ce qui se passe, consultez cet exemple de projet .



Modified text is an extract of the original Stack Overflow Documentation
Sous licence CC BY-SA 3.0
Non affilié à Stack Overflow