Android
RxJava
DataBinding

RxPropertyでRxJavaとAndroid Data Bindingを連携する

More than 1 year has passed since last update.

みじかくまとめ

RxJavaとAndroid Data Bindingを連携してMVVMするライブラリを作りました→RxProperty Android
(2017/02/09 v3.0.0バージョンアップに伴い編集)

はじめに

Android標準の非同期処理の面倒さから、Android開発にRxJavaを導入する方が増えています。
実際にRxJavaを使用してみると、非同期処理のみならずイベントや各層のI/Fなど、ありとあらゆるものをストリームとして表現したくなると思います。
その結果、ストリーム末尾のsubscribeはほとんどがView操作となり、エミットされた値をセットするだけのような単純な処理を繰り返すことになります。

当然「io.reactivex.Observableに流れてきた値がそのままViewに反映されないかな」との発想が生じますが、Android Data Bindingでバインディングを行うにはObservableFieldを介す必要があります。
そこで、io.reactivex.ObservableObservableFieldをよしなに繋ぎ、双方向バインディングを実現するライブラリ、RxProperty Androidを作成しました。
Rx.NETの使い手ならば一度は聞いたことのあるMVVMライブラリ、ReactivePropertyと同様の機能・使い勝手を提供することを目標としています。

使い方

build.gradleの記述は下記の通り。

app/build.gradle
repositories {
    maven { url "https://jitpack.io" }
}

dependencies {
    // 本体
    compile 'com.github.k-kagurazaka.rx-property-android:rx-property:3.0.0'

    // Kotlinですっきり書くための拡張関数集
    compile 'com.github.k-kagurazaka.rx-property-android:rx-property-kotlin:3.0.0'
}

まずはViewModel用のクラスを定義します。
Viewの状態を反映する変数がRxPropertyで、ユーザ操作を抽象化したコマンドがRxCommandになります。

ViewModel.java
public class ViewModel {
  public final RxProperty<String> input; // 双方向 (RxProperty <-> View)
  public final ReadOnlyRxProperty<String> output; // 単方向 (RxProperty -> View)
  public final RxCommand<Nothing> command; // コマンド

  public JavaViewModel() {
    // 初期値が空文字のプロパティ。空文字禁止のバリデーション付き。
    input = new RxProperty<>("")
        .setValidator(it -> TextUtils.isEmpty(it) ? "Text must not be empty!" : null);

    // inputに入っている文字列を大文字化するプロパティ
    output = new ReadOnlyRxProperty<>(
        input.map(it -> it == null ? "" : it.toUpperCase())
    );

    // inputのバリデーションをパスしている場合のみ有効になるコマンド
    command = new RxCommand<>(input.onHasErrorsChanged().map(it -> !it));
    // コマンドが実行されたらinputの文字を変える
    command.subscribe(it -> { input.set("clicked!"); });
  }

次にこのViewModelクラスとバインディングするView用のXMLを作ります。
@={}記法による双方向バインディングと、@{}記法による単方向バインディングの両方に対応しています。

activity_main.xml
<layout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto">
    <data>
        <variable name="viewModel" type="ViewModel" />
    </data>

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

        <EditText
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:inputType="text"
            android:text="@={viewModel.input.value}" /> <!-- 双方向バインディング -->

        <TextView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="@{viewModel.output.value}" /> <!-- 単方向バインディング -->

        <Button
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:text="Is not empty?"
            app:rxCommandOnClick="@{viewModel.command}" /> <!-- コマンドバインディング -->
    </LinearLayout>
</layout>

最後にDataBindingUtilを使ってバインディングを実行します。

MainActivity.java
  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    ActivityMainBinding binding = DataBindingUtil.setContentView(this, R.layout.activity_main);
    binding.setViewModel(new ViewModel());
  }

動作させると以下のようになります。

Demo

出力用のTextViewの中身とButtonenabledが入力用のEditTextの変化に追従していますね。

バリデーション

前述の例にも出てきましたが、RxProperty#setValidatorによってプロパティにバリデーション機能を持たせることができます。

public final RxProperty<String> input = new RxProperty<>("")
        .setValidator(it -> TextUtils.isEmpty(it) ? "Text must not be empty!" : null);

また、TextInputLayoutを使用したエラー表示にも対応しています。

<android.support.design.widget.TextInputLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:error="@{viewModel.input.error}"
    app:errorEnabled="@{viewModel.input.hasError}">

    <EditText
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:inputType="text"
        android:text="@={viewModel.input.value}"/>
</android.support.design.widget.TextInputLayout>

エラーメッセージはObservableで取得できるので、それをRxProperty化すればより柔軟なエラー表示も可能です。

RxCommandとトリガーバインディング

RxCommandは標準だとView#onClickとのバインディングしか提供していません(app:rxCommandOnClick)。
任意のビューイベントをRxCommandに紐付けるには、カスタムBindingAdapterを記述するか、トリガーバインディングを使用します。
RxCommand#bindTriggerメソッドによってコマンドをキックするObservableを指定できるので、RxBindingを併用することでイベントObservableRxCommandに変換できます。

// R.id.some_menuのメニューアイテムがクリックされた時にsomeMenuCommandを実行
viewModel.someMenuCommand.bindTrigger(RxMenuItem.clicks(menu.findItem(R.id.some_menu)));

Kotlinで書こう

Kotlin用の拡張メソッドモジュールも別途提供しています。
拡張メソッドのおかげで本家ReactivePropertyと同じような記述が可能です。

ViewModel.kt
class ViewModel {
    val input = RxProperty("")
            .setValidator { if (TextUtils.isEmpty(it)) "Text must not be empty!" else null }

    val output = input.map { it?.toUpperCase() ?: "" }
            .toReadOnlyRxProperty()

    val command = input.onHasErrorsChanged()
            .map { !it }
            .toRxCommand<Nothing>()
}

非常にシンプルでわかりやすいですね!

Fat Activityをぶっ潰せ!

MVVMの良いところは、ビュー自身とその状態を切り離して管理できることから、ビューコントローラであるActivityの肥大化を防げる点にあります。
TodoMVCを実装したサンプルActivityは以下で全てです。

TodoActivity.kt
class TodoActivity : AppCompatActivity() {
    private lateinit var viewModel: TodoViewModel

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        viewModel = TodoViewModel()
        val binding = DataBindingUtil.setContentView<ActivityTodoBinding>(this, R.layout.activity_todo)
        binding.setVariable(BR.todoVM, viewModel)
    }

    override fun onDestroy() {
        viewModel.unsubscribe()
        super.onDestroy()
    }

    override fun onCreateOptionsMenu(menu: Menu): Boolean {
        menuInflater.inflate(R.menu.menu_todo, menu)
        viewModel.deleteDoneCommand.bindTrigger(RxMenuItem.clicks(menu.findItem(R.id.clear_done)))
        return true
    }
}

ViewとViewModelの生成および一部コマンドのトリガーバインディング意外に必要なことはありません。

おわりに

RxProperty Androidを使えば、RxJavaの世界からAndroid Data Bindingの世界への橋渡しをしてくれます。
リソース管理などをちゃんとやっているサンプルもあるので、是非一度お試しください。