最近のDroidConモスクワ2016で、データバインディングライブラリを使用したMVVMについての講演と、MVPでの作業に役立つMoxyライブラリについての講演がありました。 実際、過去6か月間、ライブプロジェクトで両方のアプローチをテストすることができました。 そして、Databinding Libraryをマスターし、実稼働環境で MVVMプロジェクトをリリースしてから、このパターンを使用したくない理由を理解するまでの私の道についてお話したいと思います 。
Databinding Libraryに夢中になり、MVVMでアプリケーションを構築することを決めたすべての人に献身的に、あなたは勇敢な人です!
データバインディングライブラリ
Databinding Libraryを理解し始めて、感銘を受けました。 既にそれをよく知っている人は私を理解するでしょう、そして、残りのために、このライブラリでの仕事は次のようになります:
Databinding Libraryを使用すると、次のことができます。
-
findViewById
およびsetOnClickListener
をsetOnClickListener
。 つまり、 xmlでidを指定すると、binding.viewId
介してビューにアクセスできます。 また、 xmlから直接メソッド呼び出しを設定できます。 - データをビュー要素に直接関連付けます。
binding.setUser(user)
を呼び出し、 xmlで、たとえば、android:text = “@{user.name}”
指定しますandroid:text = “@{user.name}”
; - カスタム属性を作成します。 たとえば、Picassoライブラリを使用してImageViewに画像をロードする場合、「imageUrl」属性のBindingAdapterを作成し、 xmlに
bind:url=”@{user.avatarUrl}”
ます。
このようなBindingAdapterは次のようになります。
@BindingAdapter("bind:imageUrl") public static void loadImage(ImageView view, String url) { Picasso.with(view.getContext()).load(url).into(view); }
- ビューステートデータを依存させます。 たとえば、読み込みインジケータが表示される場合、データがあるかどうかによって異なります。
幸運は常に複雑なトピックであったため、最後のポイントは私にとって特に喜ばしいことです。 画面に3つの状態(読み込み、データ、エラー)を表示する必要がある場合は、問題ありません。 ただし、データに応じて要素の状態にさまざまな要件がある場合(たとえば、テキストが空でない場合にのみテキストを表示したり、値に応じて色を変更したりする場合)、インターフェイス状態のすべての可能なオプションを備えた大きなスイッチ 、または多くのフラグとコードが必要になる場合があります要素に値を設定する方法で。
したがって、Databinding Libraryにより状態の操作が容易になるという事実は大きなプラスです。 たとえば、 xml android:visibility=”@{user.name != null ? View.VISIBLE : View.GONE}”
android:visibility=”@{user.name != null ? View.VISIBLE : View.GONE}”
、ユーザー名でTextViewを非表示または表示するタイミングを考えることはできなくなりました。 名前を付けるだけで、表示は自動的に変更されます。
ViewModel
しかし、 データバインディングをより積極的に使用し始めると、 xmlでより多くのコードを取得できます。 そして、 レイアウトをダンプに変えないために、このコードを取り出すクラスを作成します。 また、 xmlではプロパティ呼び出しのみが残ります。 例を挙げましょう。 Userクラスがあるとします:
public class User { public firstname; public lastname; }
そして、UIでフルネームを確認し、 xmlで記述します。
<TextView android:text="@{user.firstname + user.lastname}" />
私はこれをxmlで実際に見たくないので、このロジックを転送するクラスを作成します。
public class UserViewModel extends BaseObservable { private String name; @Bindable public String getFullname() { return name; } public void setUser(User user) { name = user.firstname + user.lastname; notifyPropertyChanged(BR.name); } }
ライブラリの作成者は、そのようなクラスをViewModelと呼ぶことをお勧めします(驚くほど、MVVMパターンのように)。
例では、クラスはBaseObservableを継承し、コードではnotifyPropertyChanged()を呼び出しますが、これが唯一の方法ではありません。 ObservableFieldでフィールドをラップすることもでき、依存UI要素は自動的に更新されます。 しかし、この方法の柔軟性は低く、ほとんど使用していません。
これで、 xmlに次のようになります。
<TextView android:text="@{viewmodel.name}" />
はるかに良いですね。
そのため、データとビューの間のレイヤーとして機能するViewModelクラスがあります 。 データ変換を処理し、どのフィールド(および関連するUI要素)を制御し、更新されると、一部のフィールドが他のフィールドに依存する方法のロジックが含まれます。 これにより、コードからxmlをクリアできます。 さらに、このクラスを使用して、 ビューからのイベント(クリックなど)を処理すると便利です。
そして、ここで考えが浮かびます : 既にdatabindingがある場合、表示ロジックを含むViewModelクラスがあります。それでは、なぜMVVMパターンを使用しないのでしょうか?
この考えは必然的に生じます。 現時点で私たちが持っているものは、MVVMパターンと非常に近いためです。 それを簡単に見てみましょう。
MVVM
Model-View-ViewModelパターンには、3つの主要なコンポーネントがあります。
- モデル 表示するデータを提供するアプリケーションのビジネスロジック。
- 表示する ユーザーが画面に表示するすべてのUI要素の外観、場所、および構造を担当します。
- ViewModel ビューとモデルの間のブリッジとして機能し、表示ロジックを処理します。 Modelにデータを要求し、Viewが簡単に使用できるビューでそれをViewに渡します。 また、ボタンをクリックするなど、ビューでアプリケーションユーザーによってコミットされたイベントの処理も含まれます。 さらに、ViewModelは、たとえばロードが進行中かどうかなど、表示する必要がある追加のViewステートを定義する役割を果たします。
写真で見るこれらのコンポーネント間の関係と相互作用:
矢印は依存関係を示します。ViewはViewModelを認識し、ViewModelはModelを認識しますが、モデルはViewModelを認識せず、ViewModelはViewを認識しません。
プロセスは次のとおりです。ViewModelはデータをモデルに要求し、必要に応じて更新します。 モデルは、データがあることをViewModelに通知します。 ViewModelはデータを取得して変換し、UIのデータの準備ができたことをViewに通知します。 ViewModelとViewの関係は、データと表示を自動的にリンクすることによって行われます。 私たちの場合、これはデータバインディングライブラリを使用することで達成されます。 データバインディングでは 、ViewModelのデータを使用してビューが更新されます。
自動データバインディングの存在は、このパターンとPresentationModelおよびMVPパターンとの主な違いです(MVPでは、Presenterは提供されたインターフェイスを介してメソッドを呼び出すことでビューを変更します)。
Android MVVM
それで、プロジェクトでMVVMを使い始めました。 しかし、プログラミングではよくあることですが、理論と実践は同じものではありません。 そして、プロジェクトの完了後、私はまだ不満を感じていました。 このアプローチには何か問題があり、何かが好きではありませんでしたが、それが何であるか理解できませんでした 。
次に、AndroidでMVVM図を描くことにしました。
結果を考慮してください:
ViewModelには、 XMLでデータバインディングに使用されるフィールド( android:text=”@{viewmodel.username}”
)が含まれ、Viewによってトリガーされるイベントを処理します( android:onClick=”@{viewmodel::buttonClicked}”
)。 彼女はモデルにデータを要求し、それらを変換し、 データバインディングの助けを借りて、このデータを表示します。
フラグメントは、2つの役割を同時に実行します。初期化とシステムとの通信を提供する入力ポイントと、Viewです。
フラグメント(またはアクティビティ)がMVPおよびMVVMのパターンを理解する上でビューと見なされるという事実は、すでに一般的な慣行になっているため、これについては触れません。
アクティビティのねじれと再作成を乗り切るために、Viewが再作成されている間はViewModelをそのままにしておきます(この場合はFragment)。 これは、 短剣 スコープとカスタムスコープを使用して実現されます。 詳細については触れませんが、 短剣に関する多くの良い記事がすでに書かれています。 あなた自身の言葉では、次のことが起こります:
- ViewModelは短剣を使用して作成され(そのインスタンスはその中に存在します)、フラグメントは必要に応じてそれを使用します。
- フラグメントが回転時に停止すると、ViewModelでdetachView()を呼び出します。
- ViewModelは引き続き動作し、そのバックグラウンドプロセスも非常に便利です。
- 次に、フラグメントが再作成されると、 attachView()を呼び出し、それ自体をビューとして渡します(インターフェースを使用)。
- フラグメントがターンのためではなく完全に死んだ場合、 スコープを強制終了し(目的の短剣コンポーネントがゼロになり、ViewModelがこのコンポーネントとともにガベージコレクタによってアセンブルされます)、ViewModelが死にます。 これはBaseFragmentに実装されています 。
フラグメントがMvvmViewインターフェースを使用してViewModelに渡されるのはなぜですか? これは、ビューでコマンドを「手動で」呼び出すために必要です。 データバインディングライブラリですべてを実行できるわけではありません。
システムがアプリケーションを強制終了した場合に状態を保存する必要がある場合、 savedInstanceStateフラグメントを使用してViewModel状態を保存および復元できます。
それが動作する方法です。
気配りのある読者は、「Fragmentをコンテナとして使用し、その中でsetRetainInstance(true)
を呼び出すことができるのに、なぜダガーカスタムスコープに setRetainInstance(true)
れるのですか?」と尋ねます。はい、できます。 しかし、図を描く際には、ビューとしてActivityまたはViewGroupを使用できることを考慮しました。
最近、私が描いた構造を完全に反映したMVVM実装の良い例を見つけました。 いくつかのニュアンスを除いて、すべてが非常にうまく行われました。 興味があれば見てください。
双対問題
図を描いてすべてを熟考した結果、このアプローチで作業している間、私にはまったく合わないことがわかりました。 もう一度図を見てください。 太い矢印「 データバインディング 」と「 表示する手動コマンド 」を参照してください。 ここにある。 ここで、さらに詳しく説明します。
databindingがあるため、ほとんどのデータはxmlを使用してViewにインストールできます(必要に応じて、必要なBindingAdapterを作成します)。 しかし、このアプローチに適合しない場合があります 。 これらには、ダイアログ、 トースト 、アニメーション、遅延アクション、およびビュー要素を含むその他の複雑なアクションが含まれます。
TextViewで例を思い出してください。
<TextView android:text="@{viewmodel.name}" />
view.post(new Runnable())
を使用してこのテキストを設定する必要がある場合はどうなりますか? (私たちは、なぜだとは思わない、どう思うか)
「byPost」属性を作成するBindingAdapterを作成し、要素上のリストされた属性の存在を考慮に入れることができます。
@BindingAdapter(value = {"text", "byPost"}, requireAll = true) public static void setTextByPost(TextView textView, String text, boolean byPost) { if (byPost) { textView.post(new Runnable { public void run () { textView.setText(text); } }) } else { textView.setText(text); } }
そして、TextViewの両方の属性が指定されるたびに、このBindingAdapterが使用されます。 属性をxmlに追加します。
<TextView android:text="@{viewmodel.name}" bind:byPost="@{viewmodel.usePost}" />
ViewModelには、値を設定するときにview.post()
を使用する必要があることを示すプロパティがあります。 追加してください:
public class UserViewModel extends BaseObservable { private String name; private boolean usePost = true; // only first time @Bindable public String getFullname() { return name; } @Bindable public boolean getUsePost() { return usePost; } public void setUser(User user) { name = user.firstname + user.lastname; notifyPropertyChanged(BR.name); notifyPropertyChanged(BR.usePost); usePost = false; } }
非常に単純なアクションを実装するために必要な量を確認してください。
したがって、このようなことをViewで行う方がはるかに簡単です。 つまり、フラグメントによって実装されているMvvmViewインターフェイスを使用し、Viewメソッドを呼び出します(通常はMVPで行われているのと同じ方法で)。
ここで、 双対性の問題が現れます。2つの異なる方法でViewを操作します 。 1つは(データの状態を介して)自動で、2つ目は(表示中のコマンドの呼び出しを介して)手動です。 個人的には好きではありません。
状態の問題
次に、別の問題について説明します。 電話の回転の状況を想像してください。
- アプリケーションを起動しました。 ViewModelとView(フラグメント)は生きています。
- 彼らは電話を回しました-フラグメントは死に、ViewModelは生き続けました。 彼女のバックグラウンドタスクはすべて引き続き機能します。
- 新しいフラグメントが作成され、結合されました。 ViewModelから受信した保存状態(フィールド)をデータバインディングで表示します 。 すべてがクールです。
- しかし、フラグメント(View)が切断されたときに、バックグラウンドプロセスがエラーで終了し、 トーストを表示したい場合はどうでしょうか。 フラグメント(Viewの役割を果たしている)は死んでおり、そのメソッドを呼び出すことはできません。
- この結果は失われます。
ViewModelフィールドのセットで表されるView状態だけでなく、ViewModelがViewで呼び出すメソッドも何らかの方法で保存する必要があることがわかります 。
この問題は、個々のこのような場合にViewModelにフラグフィールドを挿入することで解決できます。 あまり美しくなく、普遍的でもありません。 しかし、それは機能します。
状態について
状態の問題により、オブジェクトの状態は 、 状態を特徴付ける一連のパラメーター、またはオブジェクトを目的の状態にするために実行する必要のある一連のアクションの2つの方法で再作成できると考えるようになりました 。
ルービックキューブを想像してください。 彼の状態は、顔の1つに9色で記述できます。 そして、あなたは彼を初期状態から望ましい状態に導く一連の動きをすることができます。
1ターンだけかかる場合もあれば、9ターン以上かかる場合もあります。 状況に応じて、状態を説明する何らかの方法の方が良いか悪いかがわかります(必要なデータは少ない)。
モクシー
状態を再現する方法を考えたとき、Moxyライブラリーを思い出すしかありませんでした。 私の同僚は、MVPパターンとこのライブラリを使用してプロジェクトを並行して行いました。 詳細については説明しませんが、著者からの優れた記事は既にあります。
私の推論の文脈では、 Moxyの機能の 1つは興味深いものです。 このビューで呼び出される一連のコマンドとしてビューの状態を保存します 。 そして、私がそれについて最初に学んだとき、それは私には奇妙に思えました。
しかし、今、すべての考え(上記であなたと共有した)の後、これは非常に良い解決策だと思います。
なぜなら:
- データ(フィールド)のみで状態を表すことが常に可能(便利)であるとは限りません。
- MVPでは、Viewとの通信はコマンド呼び出しを介して行われます。 使ってみませんか?
- 実際には、その状態を再作成するために必要なビューフィールドの数は、呼び出されるコマンドの数よりもはるかに多くなる可能性があります。
さらに、このアプローチは別のプラスを提供します。 Databinding Libraryと同様に、さまざまな状態の問題を独自の方法で解決します。 また、一連のメソッド呼び出しによって変更が再作成されるため、フィールドセットまたは状態の1つの名前に応じてUIを変更する大きなスイッチを記述する必要もありません。
それでも、Moxyについては何も言えません。 私の意見と同僚の意見では、今日、MVPパターンを扱うのに役立つ最高のライブラリです。 コード生成を使用して、開発者の労力を最小限に抑えます。 パターンの実装について考えることはできませんが、プロジェクトの機能について考えることはできません。 これはいいです。
しかし、MVPについては十分です。 それでも、私たちはMVVMについて話しているので、在庫を確認する時が来ました。
結論
私はMVVMをパターンとして気に入っており、その利点に異議を唱えません。 しかし、大部分は他のパターンと同じか、開発者の好みの問題です。 そして、主なプラスは、パターンそのものではなく、まだデータバインディングです。
MVVMへの共感に駆られて、プロジェクトを実装しました。 私は長い間トピックを研究し、熟考し、議論し、このパターンのマイナスのセットを自分で作りました:
- MVVMは、 データバインディングとViewメソッドの2つの方法でViewを同時に使用するよう強制します。
- MVVMでは、状態の問題(ViewがViewModelから切断されたときに呼び出されるViewメソッドへの呼び出しを保存する必要性)を美しく解決することはできません。
- データバインディングライブラリの高度な使用が必要であり、マスターするには時間がかかります。
- 誰もがxmlコードを好むわけではありません。
はい、これらのマイナスに慣れることができます。 しかし、よく考えた結果、アプローチの断片化を引き起こすパターンを使用したくないという結論に達しました。 そして、MVPとMoxyを使用して次のプロジェクトを書くことにしました。
このパターンを使用してください-自分で決めてください。 しかし、私はあなたに警告しました。
PS:データバインディングライブラリ
おそらく、私たちが始めたものであるDatabinding Libraryで終わらせましょう。 私はまだ彼女が好きです。 しかし、私は限られた量でそれを使用するつもりです:
-
findViewById
およびsetOnClickListener
を記述しないため。 - また、BindingAdapter-sを使用して便利なxml-属性を作成するには(たとえば、
bind:font=”Roboto.ttf”
)。
それだけです。 これは利点をもたらしますが、MVVMの方向には引き付けません。
データバインディングライブラリを使用する予定がある場合は、次の役立つ情報を参照してください。
- 変数をバインディングに設定した後、
onViewCreated()
binding.executePendingBindings()
を呼び出しbinding.executePendingBindings()
。 これは、コードから新しく作成されたビューで何かを変更する場合に役立ちます。 そのビューがまだ準備できていないことを知った後、view.post()
を書くview.post()
はありません。 - <fragment>タグに変数を渡すことはできません( <include>で可能な場合): https : //code.google.com/p/android/issues/detail?id=175338 。
- 機能を備えたDatabinding Libraryのxmlのラムダ。 角かっこ(
() -> method()
)なしでは記述できません。 コードをブロックできません。 ただし、メソッドで使用されない場合はパラメーターを省略できます(android:onClick=”@{() -> handler.buttonClicked()}”
)。 - 二重引用符(“)の代わりにバックティック( `)を使用できます。
- BindingAdaptersでは、属性(
@BindingAdapter(“attributeName”)
)のみを書き込みますが、 名前空間は引き続き無視されます。 また、 xmlでは 、 名前空間が何であるかは関係ありません。 しかし、しばしばbindを使用して区別します(bind:attributeName=”...”
)。 - ここで生成されたデータバインディングクラスを検索します:app / build / Intermediates / classes / debug
- 既製のアダプターはこちらでご覧いただけます 。
- ドキュメント以外の読み物:
https://realm.io/news/data-binding-android-boyar-mount/
https://www.bignerdranch.com/blog/descent-into-databinding/
https://halfthought.wordpress.com/2016/03/23/2-way-data-binding-on-android/