RxAndroid でスクリーンセーバー的な機能を作る

 これは Android Advent Calendar 2014 8日目 の記事です。

 例えば◯秒間操作がなかったらパスキーロック画面を表示する、とかそういうの。普通に作るとタイマーを使って面倒な感じになっちゃいますが、RxJavaRxAndroid を使うととても簡単にできます。

RxJava + RxAnroid の場合

 例えば、画面に EditBoxButton があって、「文字列の入力」と「ボタンが押されたか」を監視、◯秒間操作がなかったら××する、という処理をしたい時、RxJava+RxAndroid では以下のように書けます。

//MyActivity.java
public class MyActivity extends Activity {
    private static final String TAG = "MyActivity";
    private Subscription _subscription;

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

        final EditText editName = (EditText)findViewById(R.id.editName);
        final View buttonOk = findViewById(R.id.buttonOk);

        // OnTextChangeEvent や OnClickEvent をただの Void シグナルに変換
        final Func1<Object, Void> signalizer = new Func1<Object, Void>() {
            @Override
            public Void call(Object onClickEvent) {
                return null;
            }
        };

        // 文字入力イベントのストリームと…
        _subscription = ViewObservable.text(editName).map(signalizer)
                // ボタン押されたのストリームを合体
                .mergeWith(ViewObservable.clicks(buttonOk).map(signalizer))
                // 3秒間なんもなかったらエラーにする
                .timeout(3, TimeUnit.SECONDS)
                .subscribe(new Action1<Void>() {
                    @Override
                    public void call(Void dummy) {
                        // 何かアクションがあったらこっち
                        Log.d(TAG, "文字が入力されたか、ボタンが押されたよ");
                    }
                }, new Action1<Throwable>() {
                    @Override
                    public void call(Throwable throwable) {
                        // 3秒間何もなかったらこっち
                        runOnUiThread(new Runnable() {
                            @Override
                            public void run() {
                                Toast.makeText(MyActivity.this, 
                                    "3秒間何も操作がありませんでした", Toast.LENGTH_SHORT)
                                    .show();
                            }
                        });
                    }
                });
    }

    @Override
    protected void onDestroy() {
        // イベント系は無限ストリームだから開放してやらないとリークするはず
        _subscription.unsubscribe();
        super.onDestroy();
    }
}

 ViewObservable.text(editName) がテキストが入力される度にシグナルを発するストリーム、ViewObservable.clicks(buttonOk)がボタンが押される度にシグナルを発するストリームです。これらを mergeWith で合体させます。

 あとは timeout につなげるだけ。3秒以内にシグナルがあったら onNext→new Action<Void>()のとこ、3秒以上何も操作がなかったらタイムアウトして onError→new Action<Throwable>() のとこに飛びます。あとはご自由に、ここでは Toast を表示してるだけです。

 注意点は、イベントから生成されたストリームは無限、つまり onComplete は来ない。こういう Observable は自力での購読解除(unsubscribe)が必須です。

これを動かすとこんな感じになります

Xamarin.Android + Rx本家の場合

 さて Xamarin です。Xamarin では本家の Reactive Extensions が使用できます。RxAndroid と同じことをやると下のように書けます、スマート。

//MainActivity.cs
[Activity(Label = "RxJavaSample", MainLauncher = true, Icon = "@drawable/icon")]
public class MainActivity : Activity
{
    protected override void OnCreate(Bundle bundle)
    {
        base.OnCreate(bundle);

        // Set our view from the "main" layout resource
        SetContentView(Resource.Layout.Main);

        var editName = FindViewById<EditText>(Resource.Id.editName);
        var buttonOk = FindViewById<Button>(Resource.Id.buttonOk);

        Observable.FromEventPattern<TextChangedEventArgs>(editName, "TextChanged").Select(_=>true)
            .Merge(Observable.FromEventPattern(buttonOk, "Click").Select(_=>true))
            .Timeout(TimeSpan.FromSeconds(3))
            .Subscribe(_ => {} , 
            e => RunOnUiThread(() => Toast.MakeText(this, 
                "3秒間何も操作がありませんでした", ToastLength.Short).Show()));
    }
}

まとめ

 Reactive Extensions を使うと、UIイベントをストリームに変換でき、合成・加工・フィルタなどして様々な応用ができます。しかしこれは Rx のパワーのまだ半分。もう半分は、WebAPI とか DB とか、Model 由来のレスポンスもストリーム化できること。どちらも Observable にしたら、あとはそれをつなぐだけでアプリ完成!  さあみんなで Rx にロックインされましょう!