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> 全体を戻すという違いがある。

0 件のコメント:

コメントを投稿