サーチ…


備考

データバインディングでの文法の構文

viewModel関数をxmlのプロパティにバインドすると、 getisような特定の関数接頭辞が削除されます。例えば。 ViewModel::getFormattedTextは、xmlのプロパティにバインドするときに@{viewModel.formattedText}になります。同様にViewModel::isContentVisible - > @{viewModel.contentVisible} (Java Bean表記法)

ActivityMainBindingような生成されたバインディングクラスは、Javaクラスではなく、バインディングを作成するXMLの名前に基づいています。

カスタムバインディング

activity_main.xmlでは、 android名前空間ではなく、 app textColor属性を設定しました。何故ですか? ViewModelによって実際の色に送出されるColorResリソースIDを解決するtextColor属性用に定義されたカスタムセッターが存在するためです。

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

}

この動作の詳細については、 DataBinding Library:Custom Settersを参照してください。

待って...あなたのXMLには論理があります!!!

あなたは私のビューにビューロジックがあるので、私はXMLでandroid:visibilityapp:textColorをMVVMコンテキストで間違っている/反パターンしていると主張できます。しかし、テストの理由からViewModelからアンドロイド依存性を取り除くことが重要であると私は主張します。

それに、本当にapp:textColorは何をするのですか?それは、それに関連付けられた実際の色への再ソースポインタを解決するだけです。したがって、ViewModelは、何らかの条件に基づいて表示される色を決定します。

android:visibilityについては、メソッドがどのように名前付けされているかのために、ここでは三項演算子を実際に使用しても構いません。 isLoadingVisibleisContentVisibleという名前のため、それぞれの結果がどのように解決されるべきかについて疑いはありません。だから私は実際にビューロジックを実行するよりもむしろViewModelによって与えられたコマンドを実行していると感じています。

一方、私はviewModel.isLoading ? View.VISIBLE : View.GONEを使用することに同意するでしょうviewModel.isLoading ? View.VISIBLE : View.GONEはビューにとってその状態の意味を前提にしているため、実行するのが悪いことです。

有用な材料

以下のリソースは、私がこのコンセプトを理解しようとするのを助けてくれました。

データバインディングライブラリを使用したMVVMの例

MVVMの全体のポイントは、ロジックを含むレイヤーをビューレイヤーから分離することです。

Androidでは、データバインディングライブラリを使用してこれを手助けし、Androidの依存関係を気にすることなく、ほとんどのロジックをユニットテスト可能にすることができます。

この例では、次のことをする愚かな単純なアプリケーションの中心的なコンポーネントを示します:

  • 起動時にネットワークコールを偽装し、ローディングスピナーを表示する
  • クリックカウンタTextView、メッセージTextView、およびカウンタをインクリメントするボタンを含むビューを表示する
  • ボタンをクリックして更新カウンタをクリックし、カウンタがある数に達するとカウンタの色とメッセージテキストを更新する

ビューレイヤーから始めましょう:

activity_main.xml

DataBindingの仕組みに慣れていない場合は、おそらくそれに精通するために10分かかるでしょう。ご覧のとおり、通常setterで更新するすべてのフィールドは、viewModel変数の関数にバインドされています。

android:visibilityまたはapp:textColorプロパティに関する質問がある場合は、「備考」セクションを確認してください。

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

次に、モデルレイヤー。私はここにいる:

  • アプリの状態を表す2つのフィールド
  • gettersはクリック数と興奮状態を読む
  • クリック数を増やす方法
  • 以前の状態を復元する方法(方向変更のために重要)

また、ここでは、クリック数に依存する「興奮の状態」を定義します。これは後でビューの色とメッセージを更新するために使用されます。

状態がどのようにユーザーに表示されるかについて、モデルには仮定がないことに注意することが重要です。

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

次にViewModel。

これにより、モデルの変更がトリガーされ、モデルのデータがビューに表示されます。ここでは、モデルによって与えられた状態( resolveCounterColorおよびresolveLabelText )に対して、どのGUI表現が適切であるかを評価します。たとえば、ViewModelまたはビューのコードに触れることなく、興奮の状態のしきい値が低いUnderachieverClickerModelを簡単に実装できます。

また、ViewModelはビューオブジェクトへの参照を保持していないことにも注意してください。すべてのプロパティは@Bindableアノテーションでバインドされ、 notifyChange() (すべてのプロパティを更新する必要があることをnotifyPropertyChanged(BR.propertyName) )またはnotifyPropertyChanged(BR.propertyName) (このプロパティを更新する必要があることをnotifyPropertyChanged(BR.propertyName)いずれかのときに更新されます。

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

}

アクティビティですべてを一緒にしてください!

ここでは、必要なすべての依存関係を持つviewModelを初期化し、アンドロイドのコンテキストからインスタンス化する必要があるビューを確認します。

viewModelが初期化されると、DataBindingUtil(生成されたクラスの命名については '構文'のセクションを確認してください)を介してxmlレイアウトにバインドされます。

注意:サブスクリプションは、メモリリークやNPEを避けるために、アクティビティが一時停止または破棄されたときにサブスクライブ解除する必要があるため、このレイヤでサブスクライブされます。 OrientationChangesでのviewStateの永続化と再ロードもここでトリガーされます

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

アクションのすべてを見るには、このサンプルプロジェクトをチェックしてください。



Modified text is an extract of the original Stack Overflow Documentation
ライセンスを受けた CC BY-SA 3.0
所属していない Stack Overflow