Androidアプリ開発 Rxjava2 BMI計算アプリを作成してPublishProcessorを理解する

2017年03月07日(編集2017年03月07日)
このエントリーをはてなブックマークに追加

Androidアプリ開発でRxJava2を使うと、UIの実装が便利になります。
この記事は、AndroidアプリでRxJava2のPublishProcessorの使い方を記載した記事です。

環境はAndroid 7.1 (API level 25) です。

環境

  • OS X El Capitan
  • android sdk 25
  • Oracle jdk version 1.8.0_72
  • Android Studio 2.3
  • Rxjava 2.0.1

難易度

中級者向け

サンプルコード

Android-Rxjava2-Demo

Rxjava2

Rxjavaは、Javaでリアクティブプログラミングをするためのライブラリです。
リアクティブプログラミングは、送られてきたデータを受け取るたびに反応して処理をするプログラミングです。

Rxjavaはデータを作成して通知する生産者(Publisher)と、データを受け取り処理を行う消費者(Subscriber)で構成されています。

Rxjava2ではObservableとほぼ同じ機能を提供するFlowableクラスが新たに導入されました。両者の違いは、Backpressure機能の有無です。

インストール

Rxjavaのインストールの設定をgradleにします。
Rxjavaのバージョンはbuild.gradleに次のように記述します。

{project_folder}/app/build.gradle
ext {
    buildToolsVersion = "25.0.2"
    supportLibVersion = "25.1.1"
    rxandroidVersion = "2.0.1"
    rxjavaVersion = "2.0.1"
    rxjavaOptional = "1.1.0"
    threetenabp = "1.0.5"
}
          

Rxjavaの情報は、app/build.gradleに次のように記述します。

{project_folder}/app/build.gradle
dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    androidTestCompile('com.android.support.test.espresso:espresso-core:2.2.2', {
        exclude group: 'com.android.support', module: 'support-annotations'
    })

    compile 'com.android.support:appcompat-v7:' + rootProject.supportLibVersion
    compile 'com.android.support:design:' + rootProject.supportLibVersion
    compile 'com.android.support:support-v4:' + rootProject.supportLibVersion

    compile 'io.reactivex.rxjava2:rxandroid:' + rootProject.rxandroidVersion
    // Because RxAndroid releases are few and far between, it is recommended you also
    // explicitly depend on RxJava's latest version for bug fixes and new features.
    compile 'io.reactivex.rxjava2:rxjava:' + rootProject.rxjavaVersion
    // https://github.com/JakeWharton/ThreeTenABP
    compile 'com.jakewharton.threetenabp:threetenabp:' + rootProject.threetenabp
    testCompile 'junit:junit:4.12'
}
          

ビルド後に、RxJava2の実装が可能になります。

アプリ設計

サンプルとして、以下のようなBMI計算アプリを作成します。

fig1. アプリ画面

Rxjava2のPublishProcessorを使ってBMI値を表示するだけのシンプルなアプリです。
身長と体重を変更すると、BMI値を計算して、値とメッセージが更新されます。

実装

画面のベースになるActivityを作成します。
クラス名はBmiCalculationActivityにします。

{project_folder}/BmiCalculationActivity.java
public class BmiCalculationActivity extends AppCompatActivity {

    private static final String TAG = "BmiCalculationActivity";

    // エラー入力
    // Error Input
    public final static float ERROR_INPUT_VALUE = -1;

    private EditText heightView;
    private EditText bodyWeightView;
    private TextView bmiView;
    private TextView msgView;

   +PublishProcessor<Float> height = PublishProcessor.create();
    PublishProcessor<Float> bodyWeight = PublishProcessor.create();
    CompositeDisposable compositeDisposable;

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

        heightView = (EditText) findViewById(R.id.height);
        heightView.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
               +height.onNext(convertFloat(s.toString()));
            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });

        bodyWeightView = (EditText) findViewById(body_weight);
        bodyWeightView.addTextChangedListener(new TextWatcher() {
            @Override
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {

            }

            @Override
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                +bodyWeight.onNext(convertFloat(s.toString()));
            }

            @Override
            public void afterTextChanged(Editable s) {

            }
        });

        bmiView = (TextView) findViewById(R.id.bmi);
        msgView = (TextView) findViewById(R.id.msg);

        // 身長通知(Producer)
        +Disposable disposableHeight = height.
                // 消費者のスレッド
                observeOn(AndroidSchedulers.mainThread()).
                // 購読(Consumer)
                subscribeWith(new DisposableSubscriber<Float>() {
                    @Override
                    public void onNext(Float height) {
                        updateBmiView(convertFloat(bodyWeightView.getText().toString()), height);
                    }

                    @Override
                    public void onError(Throwable t) {

                    }

                    @Override
                    public void onComplete() {

                    }
                });

        // 体重通知(Producer)
        Disposable disposableWeight = bodyWeight.
                observeOn(AndroidSchedulers.mainThread()).
                // 購読(Consumer)
                subscribeWith(new DisposableSubscriber<Float>() {
                    @Override
                    public void onNext(Float bodyWeight) {
                        updateBmiView(bodyWeight, convertFloat(heightView.getText().toString()));
                    }

                    @Override
                    public void onError(Throwable t) {

                    }

                    @Override
                    public void onComplete() {

                    }
                });

       +compositeDisposable = new CompositeDisposable(disposableHeight, disposableWeight);
    }

    /**
     * BMIを計算する
     * calculate BMI
     *
     * @param bodyWeight bodyWeight
     * @param height     height
     * @return bmi
     */
    private float calculateBmi(float bodyWeight, float height) {
        if (bodyWeight == ERROR_INPUT_VALUE || height == ERROR_INPUT_VALUE) {
            return ERROR_INPUT_VALUE;
        }
        return bodyWeight / (height * height);
    }

    /**
     * BMIView更新
     * update bmiTextView
     *
     * @param weight weight
     * @param height height
     */
    private void updateBmiView(float weight, float height) {
        float bmi = calculateBmi(weight, height);
        if (bmi == ERROR_INPUT_VALUE) {
            msgView.setText(getString(R.string.error));
            bmiView.setText(getString(R.string.error));
            return;
        }

        bmiView.setText(String.valueOf(bmi));

        String result = "wrong";
        @ColorRes int color = R.color.colorBlack;
        if (16 > bmi) {
            result = " probably sick ";
            color = R.color.colorRed;
        } else if (16 < bmi && bmi < 18.5) {
            result = " beautiful ";
        } else if (18.5 <= bmi && bmi < 22.0) {
            result = " standard ";
        } else if (22.0 <= bmi && bmi < 30.0) {
            result = " fat ";
        } else if (bmi > 30) {
            result = " sumo wrestler ";
            color = R.color.colorRed;
        }
       +msgView.setTextColor(ContextCompat.getColor(getApplicationContext(), color));
        msgView.setText(getString(R.string.msg, result));
    }

    /**
     * 文字列をFloatに変換する
     * 変換に失敗した場合はERROR_INPUT_VALUEを返す
     *
     * @param str 任意の文字列
     * @return result float value
     */
    private float convertFloat(@NonNull String str) {
        try {
            return Float.valueOf(str);
        } catch (NumberFormatException e) {
            return ERROR_INPUT_VALUE;
        }
    }

    @Override
    protected void onDestroy() {
        super.onDestroy();
        +if (!compositeDisposable.isDisposed()) {
            compositeDisposable.dispose();
        }
    }
}
        

■ 実装の解説

1. PublishProcessor

PublishProcessorはProcessorです。
消費者としてデータを受け取り、そのデータを生産者として通知するインターフェースです。

PublishProcessor<Float> height = PublishProcessor.create();
PublishProcessor<Float> bodyWeight = PublishProcessor.create();
        

上記は身長と体重の取得と通知をするPublishProcessorです。
消費者と生産者が同じなので、データの型は通知も取得もFloatです。

2. onNext

Subscriber(消費者)にデータを通知します。

height.onNext(convertFloat(s.toString()));
bodyWeight.onNext(convertFloat(s.toString()));
        

上記のheightとbodyWeightは、PublishProcessorオブジェクトなので、消費者としてデータを受け取って、このデータを生産者として通知しています。
onNextメソッドを呼び出すと、subscribeに通知します。

3. observeOn

observeOnメソッドは、データを受け取った側(消費者)の処理を、どのようなSchedule上で行うのかを設定するメソッドです。

observeOn(AndroidSchedulers.mainThread())
        

observeOnメソッドが指定したSchedulerが設定したスレッド上で、それ以降の処理を行うようになります。
上記の処理は、データを受け取る側(消費者)が、AndroidのUIの場合に利用します。このサンプルでは、データを受け取るのがUIのTextViewなので、AndroidSchedulers.mainThread()を使います。このメソッドを呼びださないと、Only the original thread that created a view hierarchy can touch its viewsが発生します。

4. subscribeWith

PublishProcessor(生産者)が通知したデータを購読します。

subscribeWith(new DisposableSubscriber<Float>() {
    @Override
    public void onNext(Float height) {
        updateBmiView(convertFloat(bodyWeightView.getText().toString()), height);
    }

    @Override
    public void onError(Throwable t) {

    }

    @Override
    public void onComplete() {

    }
});
        

subscribeWithはRxjava2で追加されたメソッドです。機能はsubscribeと同じで、生産者が通知したデータを購読します。
subscribeWithメソッドは、Subscriberを引数に取ります。多くの場合、DisposableSubscriberやResourceSubscriberを使います。onErrorメソッドとonCompleteのメソッドを使うことで、より細かい処理が実装できます。

5. Disposable

Disposableは購読を解除するためのメソッドを持つインターフェースです。

Disposable disposableHeight = height.observeOn(AndroidSchedulers.mainThread()).
        

subscribeの戻り値としてDisposableを取得することで、購読の解除が可能になります。
購読の解除をしないと、Androidアプリではメモリリークの原因になります。

6. CompositeDisposable

CompositeDisposableは、Disposableを格納する入れ物のクラスです。

compositeDisposable = new CompositeDisposable(disposableHeight, disposableWeight);
        

複数の購読を利用している場合、上記のようにdisposableをCompositeDisposableに格納しておくと良いでしょう。

多くのDisposableがある場合、全てを削除するのは大変です。また、削除漏れが発生する可能性もあります。CompositeDisposableに格納しておけば、一度に全ての購読を解除することができます。

if (!compositeDisposable.isDisposed()) {
    compositeDisposable.dispose();
}
        

7. ContextCompat.getColor

カラーの値を取得するのはContextCompat.getColorメソッドを使います。
以前は、getResources().getColorが使われていましたが、非推奨になりました。

×getResources().getColor(R.color.colorRed);
◯ContextCompat.getColor(getApplicationContext(), R.color.colorRed)
        

非推奨のメソッドはLintチェックが通りません。より優れたアプリを作成するために、Lint Baselineの機能を使って、地道にエラーや警告を潰してください。

ビルド

上記のコードビルドして動作を確認します。

fig2. アプリ画面

身長と体重を変更すると、BMIの計算結果とメッセージを表示するアプリが作成できました。

結論

PublishProcessorを使うと、消費者としてデータを受け取り、そのデータを生産者として通知することができます。とても便利な機能なので、使ってみてください。

関連記事

タグ検索で調べてみよう

Android7.1 RxJava RxJava2