2020年3月27日金曜日

LVMC (a.k.a. MVVM: ViewModel + DataBinding) と Fat View

LVMC(Android の MVVM)モデルや DataBinding について大体の感じはつかめてきたが、先日、一つ失敗して、学習したことがあった。

ViewModel + DataBinding を使うことによって、LifeCycle でグチャグチャに分断された Activity のあちこちに詰め込まれたコードを ViewModel や View に退避して、Activity をスッキリさせることには成功した。

一方、Activity から追い出したコードを、ViewModel に所属させるのか、View に所属させるのかという部分で間違いを犯した。DataBinding で View のプロパティを LiveData でリアルタイムに反映させることができるのだからと、ほとんどの部分を View に移植したのだ。これが失敗だった。

これは ViewModel が導入される以前の、Activity の肥大化に悩んだ当時の開発者たちが採った Fat View と呼ばれるアプローチと同じもので、Activity が一手に引き受けていた複雑な処理を、View が肩代りするだけの話に過ぎない。もちろん、通常は、Activity と View は一対多の関係だから、Activity に複数の View にまつわる処理が集結するよりはマシだが、単純に View の数に分割されただけで、全体としての複雑さの総量に変化はない。

今回は ViewModel を使っているにもかかわらず、単なる Fat View 的なアプローチと同じ罠に陥るという過ちを犯してしまったわけである。

Fat View の何がいけないかというと、View のプロパティにセットした値が、View で処理される順序が、何ら保証されていないという点である。今回は、ImageView を拡張して、Glide によるリモート画像の読み込みを行うようなことを実現したかったのだが、リモートのアドレスをバインドされた LiveData を通じてダイナミックに設定しようとした。ところが、リモートのアドレスをいくつかの諸条件から組み合わせて動的な生成を行う処理を View 内部に持たせ、バラバラの諸条件を View のプロパティとして入力しようとしたため、アドレスが null の段階で Glide で読み込もうとしたりと、処理が時系列的に制御不能な状態になった。

結局、そのような Fat View アプローチが間違っていたわけで、「View に考えさせてはいけない」ということに至った。「複雑に変化する諸条件から導き出されるアドレスについて思考する部分は ViewModel 内部において行い」、最終的な URL のみを LiveData として View に対して晒すというのが正解だったわけである。

最初はともかく、Activity をスッキリさせることだけに主眼があったが、まだそれだけでは片手落ちだったと思う。(1) Activity からコードを追い出すことで、Activity のライフサイクルからは解脱することができる。そうなったら次は、(2) ViewModel と View の間の、Model と View としての役割分担であり、この間は DataBinding による Observed と Observer の関係をよく弁えた配分が必要になる。

2020年3月17日火曜日

LVMC (a.k.a. MVVM) サンプル

前日の記事「Android で MVVM ならぬ“LMVC”」のサンプルコード

build.gradle (:app):

apply plugin: 'com.android.application'

android {
    compileSdkVersion 29
    buildToolsVersion '29.0.3'

    defaultConfig {
        applicationId 'com.scaredeer.lmvcsample'
        minSdkVersion 26
        targetSdkVersion 29
        versionCode 1
        versionName '1.0'
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }

    compileOptions {
        sourceCompatibility = 1.8
        targetCompatibility = 1.8
    }

    dataBinding {
        enabled true
    }
}

dependencies {
    implementation fileTree(dir: 'libs', include: ['*.jar'])

    implementation 'androidx.appcompat:appcompat:1.1.0'
    implementation 'androidx.lifecycle:lifecycle-extensions:2.2.0'
}

タイムスタンプをフォーマットするのに Java8 の新しい機能 Instant.ofEpochSecond を使ってしまっているが故に、minSdkVersion 26 となってしまっているが、このサンプルの趣旨としては本当はどうでもいい部分である。

lifecycle-extentions は、MutableLiveData の初期値の代入をシンプルな形で記述するために必要だった。

main_activity.xml:

<layout xmlns:android="http://schemas.android.com/apk/res/android">

    <data>
        <variable
            name="viewModel"
            type="com.scaredeer.lmvcsample.MainViewModel" />
    </data>

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="vertical">

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{viewModel.datetime}"
            android:textAlignment="center" />

        <Button
            android:id="@+id/button"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:onClick="@{viewModel::onClick}"
            android:text="Button" />

    </LinearLayout>
</layout>

MainActivity.java:

package com.scaredeer.lmvcsample;

import androidx.appcompat.app.AppCompatActivity;
import androidx.databinding.DataBindingUtil;
import androidx.lifecycle.ViewModelProvider;

import android.os.Bundle;

import com.scaredeer.lmvcsample.databinding.MainActivityBinding;

public class MainActivity extends AppCompatActivity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.main_activity);

        MainViewModel viewModel = new ViewModelProvider(this).get(MainViewModel.class);

        MainActivityBinding binding = DataBindingUtil.setContentView(this, R.layout.main_activity);
        // https://developer.android.com/topic/libraries/data-binding/architecture
        // https://developer.android.com/topic/libraries/data-binding/architecture#livedata
        // https://developer.android.com/topic/libraries/data-binding/architecture#viewmodel
        binding.setLifecycleOwner(this);
        binding.setViewModel(viewModel);
    }
}

MainActivity でやっていることは、レイアウトファイルからバインディングを生成し、バインディングのライフサイクルはこの MainActivity に従って(消滅・再生成等)されることを設定し、さらに ViewModelProvider 経由(ViewModelProvider を経由しないと、毎回 ViewModel が Activity と連動して新生されてしまい、画面回転による Activity の再生成に巻き込まれたりして、ライフサイクルの独立性を得ることができなくなる)で入手した ViewModel をバインディングの一方の端として結び付けることである。

Activity の記述で行うことは以上で、あとは、ViewModel 側で、バインドされた各値に対してピンポイントで操作を行うコードスタイルとなる。

MainViewModel.java (1/2):

package com.scaredeer.lmvcsample;

import android.view.View;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;

import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

public class MainViewModel extends ViewModel {
    private MutableLiveData<Long> _time;
    public LiveData<String> datetime;

    public MainViewModel() {
        _time = new MutableLiveData<>(0L);
        datetime = Transformations.map(
                _time,
                time -> Instant
                        .ofEpochSecond(time / 1000L)
                        .atZone(ZoneId.of("JST"))
                        .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"))
        );
    }

    public void onClick(View view) {
        _time.setValue(System.currentTimeMillis());
    }
}

ViewModel の内部では MutableLiveData として保持・操作しているが、外部公開用の LiveData と区別するため、Transformations.map を使ってこのような構成にしてみた。上流側の _time に対する変更が Transformations.map によって下流の datetime に連動し、datetime を参照している View 側に反映される。

MainViewModel.java (2/2):

package com.scaredeer.lmvcsample;

import android.view.View;

import androidx.lifecycle.LiveData;
import androidx.lifecycle.MutableLiveData;
import androidx.lifecycle.Transformations;
import androidx.lifecycle.ViewModel;

import java.time.Instant;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;

public class MainViewModel extends ViewModel {
    private MutableLiveData<Long> _time;
    public LiveData<String> datetime;

    public MainViewModel() {
        _time = new MutableLiveData<>(0L);
        datetime = Transformations.switchMap(
                _time,
                time -> toTimestamp(time)
        );
    }

    private LiveData<String> toTimestamp(Long time) {
        String string = Instant
                .ofEpochSecond(time / 1000L)
                .atZone(ZoneId.of("JST"))
                .format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
        return new MutableLiveData<>(string);
    }

    public void onClick(View view) {
        _time.setValue(System.currentTimeMillis());
    }
}

Transformations.map() でやったのと同じことを比較のため、Transformations.switchMap() でやるとこうなる。要するに、戻り値が LiveData<T> の中身の Type で戻して LiveData<T> にラップされて戻される .map() に対し、.switchMap() は別の LiveData<T> 全体を戻すという違いがある。

2020年3月16日月曜日

Android で MVVM ならぬ“LMVC”

MVC と MVVM(参考:2015年に備えて知っておきたいリアクティブアーキテクチャの潮流)の比較:

やはり、MVC の場合と MVVM の場合で、Model の意味合いが変っており、MVVM の方の M は基本的にもう Model と呼ぶべきものではないものとなっていると思う。

MVVMPattern

基本的にプレゼンテーション層とデータ・ビジネスロジック層を分けるという観点で全体を階層化させているわけだから、MVC のように、入力と出力を分けるという観点とは根本的に異っている。MVC の場合は、入力を抽象化して内部データ化するという処理の部分と、内部データを具象化して出力するという処理の部分を交錯させないで分離するという観点で、その抽象化された内部データの属する部分を Model と呼んでいるわけだ。

なので、MVC と MVVM で名称的に共通するのは View だけであり、Model 部分は紛らわしいので、別のネーミングにすべきではなかったかと思う。むしろ、ViewModel の部分の方が、View にとっての抽象化されたデータの参照元(Model of View となる部分)としての意味で ViewModel と呼ぶのは、まだ理解しうる。Model の方は Model と呼ばずに、Logic などと名付けて LVVM とでもすべきではなかったのだろうか(LVVM では別のタームと被ってしまうから、ViewModel の方をこそ Model と呼んで、LMV (Logic-Model-View) とか LMVC (Logic-Model-ViewControl) でもいいんじゃないかと思う)。

なので以下、敢えて LMVC の用語で行こうと思うが、Logic 層と ViewControl 層の両者の中間に Model 層(俗に言う ViewModel)を設ける意義は、処理の時系列的な連動性を断ち、時間的なバッファを設けることが可能となるからだと思う。ネットワークの非同期処理などを抱える Logic 層、Activity のライフサイクルなどによって頻繁にアプリ外の都合と要因で破棄・再生成されるライフサイクルを持つ ViewControl 層で、それぞれが所属する時間の流れ、ライフサイクルの様相が大きく食い違っており、この異なるフェイズが混じり合うことないように分離するには、中間にこのような Model 層を設けてバッファとするわけである。

例えるならば、コンビニエンスストアーやスーパーストアーでの、バックヤードに相当するのが Model 層である。Logic 層に相当するのはロジスティック(問屋・運送)関係者で、彼らは店頭の事情など気にすることなく、バックヤードに運び込むことだけを考えればいいし、一方、ViewControl 層に相当する店頭に立つ販売員は、バックヤードから適宜店頭の棚に商品を補充すればよい。

LMVC 以前の従来の Logic 層と ViewControl 層が分離されておらず互いに混じり合ってしまっている状況(Activity / Fragment にネットワークロジックなども記述している状態)は、例えるならば、店頭の棚に運送業者が直接納入するので、店頭の棚の事情によって、運送業者がせっかく運んできた品物を持ち帰らされて再配達を余儀なくされたり、運送業者の納入を待って販売員が客の相手をできなかったりといったギクシャクした運営状態で例えられるだろう。

Model 層の存在・設置意義(わざわざ従来の Logic や ViewControl から分離・独立させて設ける意味)はまずはそういったことになる。

そして、一旦 Model 層を設けることについての議論が終ったならば、次は Logic 層や ViewControl 層との関係性についてである。

まず、Model 層と ViewControl 層の間の関係としては、LiveData が登場する。ViewControl 層は Android で言うところの Activity / Fragment に相当するものであり、それ自体は本来、処理としては比較的複雑・大規模なものである。通常の Model のデータを参照して、それを元に ViewControl を再構築するとなると、コードがその分、肥大化する。LiveData の場合は、画面の中の一部の文字列だけ、Model のデータの変化に応じて描き変えたいといったような場合に、関数型プログラミング的なアプローチでコードを記述することが可能になる。関数型プログラミングでは、関数を記述することに主眼があり、「再度変数に代入し直す」という手続処理的な記述(ボイラープレートコード化すること)を回避できる。LiveData によって、このような明示的な再代入手続をコードの記述上、避けることができるのである(API 側が裏側でやってくれる)。

つまり、Model に対する ViewControl の処理は、LiveData によってかなりスッキリと記述できるようになるわけである。Model の該当する変数の変化を ViewControl に暗黙的に反映できるようになる。一旦、ViewControl をそのように定義しておけば、あとは Model のデータを操作することだけにプログラマーの意識が集中できるような記述になるわけである。

また実際、水面下では、コールバックなどを通じて Model 側から Observer である ViewControl へ通知されるという仕組みになっているものと思われるが、コールバックというものが Observe したい側ではなく、Observe される側の Model 側から行うというところがミソで、これが非同期プログラミングの問題の温床となっている。Observe したい側の ViewControl が停止・破棄されてしまうと、Model 側が通知しようとするその通知先である ViewControl へのポインターが null となり、NullPointerException が発生するわけである。なので、ViewControl 側では、ライフサイクルにおける終了時に必ず、Observer 登録を解除する処理を記述せねばならず、これまた典型的なボイラープレートコードとなるわけだ。これらのボイラープレート処理が、LiveData の場合はライフサイクル面を考慮して API 側が面倒を見てくれて記述する必要がなくなるので、大いに助かるわけである。

ちなみに、DataBinding は ViewControl から一部のパラメーター(変数)を抜き出して、Model の変数として連関させて、ViewControl のオブジェクトを意識しないでも、直接各パラメーターにアクセスできるようにする感じでコードを記述できるという、あくまでも「記述上の」便宜に過ぎないと考えるといいだろう。LMVC 的な構成論とは直接関係のない話題と言うことができる。

次に、Logic と Model の関係も、Model と ViewControl の関係と同様に考えることができる。非同期でタイミングを確定できない Logic のデータを、LiveData として定義し、Model 側から Observe する関係として設計する。こうすることによって、例えばアプリケーションの終了と共に Model は ViewControl と共に停止・消滅するがそのライフサイクルによって後に非同期処理が Model へコールバックを試みることで NullPointerException につながるようなことを、LiveData を介在した Observe 設計によって防止できるわけである。

最後に、以上のようにして、Logic-Model-ViewControl で互いのライフサイクルが絡み合うことないように分離したのに、それを台無しにしかねないのが、ライフサイクルのより長い、Model 側で ViewControl (Activity / Fragment) のインスタンスへのポインターを保持したり、Logic 側で Model のインスタンスへのポインターを保持したりするような真似である。これは特に LMVC 的な話題としてではなくとも、Android のライフサイクルの関係する話題で随所に出てくる話である。要するにガーベージコレクションを妨げ、メモリーリークするので、避けなければならない。

また最後に付け加えるならば、Model(Android では ViewModel クラスの派生オブジェクトに相当)に Logic を埋め込んで統合せず、分けることを Google 公式が推奨する理由は、Logic については「concern 毎にクラスを分けるべき」というプログラミングの一般論的なポリシーによる。特に LMVC (MVVM) 固有の次元の話ではない。