Pepper SDK実践(1) Futureを使いこなそう!


今回はFutureについて少しだけ解説しておこうと思います。「Pepper SDK入門(8) アクションの連結」にある内容については、理解している前提で書いていきます。

Future自体は使わなくてもPepper向けのアプリは作れるけど、いろんな割込みとかを考慮して製品としてのアプリを作ろうとすると結局Futureのような機能を自前で作ることになるので、素直にFutureを使ってアプリを作るのがお勧めです。

1.とりあえず発話

「はろーペッパーえすでぃーけー」と発話させる方法です。

Say say = SayBuilder.with(qiContext).withText("はろーペッパーえすでぃーけー").build();
say.run();

同期実行なのでUIスレッドでやるとエラーが発生するので注意が必要なヤツです。これをFutureを使って非同期にしてみます。

Future<Void> futureSay = SayBuilder.with(qiContext).withText("はろーペッパーえすでぃーけー").buildAsync() // Sayアクションを非同期でビルド
        .andThenCompose(new Function<Say, Future<Void>>() {
            @Override
            public Future<Void> execute(Say say) throws Throwable {  // 引数としてビルドされたSayアクションのインスタンスが渡される
                return say.async().run();   // 非同期で実行
            }
        });

ぱっと見行数も増えて難しくなっちゃいましたね。でも、非同期であればUIスレッドから呼べますし、アクションを途中でキャンセルできますし、ものにもよりますがアクションを複数同時に実行できて非常に助かります。実際のアプリ開発のシーンで同期実行を使うケースはたぶんありません。あと、andThenで繋いでいるのは、Sayアクションのビルドが失敗、キャンセルされたら、Sayアクションの実行も当然できないので後続の処理を呼ばれなくするためです。

2.一応、アニメーションもやっとこう

今度はアニメーションさせる方法です。
デフォでついてる犬のモノマネをするアニメーション、dog_a001というqianimファイルを「R.raw.dog_a001」にインポートします。
で、イメージしやすいように一旦、同期実行でコードを書くとこんな感じ。

Animation animation = AnimationBuilder.with(qiContext).withResources(R.raw.dog_a001).build();
Animate animate = AnimateBuilder.with(qiContext).withAnimation(animation).build();
animate.run();

アニメーションはAnimationリソースをビルドして、Aniamteアクションをビルドして実行と3ステップになります。これをFutureを使って非同期にしてみます。

Future<Void> futureAnimate =  AnimationBuilder.with(qiContext).withResources(R.raw.dog_a001).buildAsync()
        .andThenCompose(new Function<Animation, Future<Animate>>() {
            @Override
            public Future<Animate> execute(Animation animation) throws Throwable {
                return AnimateBuilder.with(qiContext).withAnimation(animation).buildAsync();
            }
        }).andThenCompose(new Function<Animate, Future<Void>>() {
            @Override
            public Future<Void> execute(Animate animate) throws Throwable {
                return animate.async().run();
            }
        });

やっぱり同期のほうがシンプルに見えますね、、lambdaを使えば見栄えはだいぶ良くなりますので、ここだけlambdaでコードを綺麗にしたものも載せておきます。

Future<Void> futureAnimate =  AnimationBuilder.with(qiContext).withResources(R.raw.dog_a001).buildAsync()
        .andThenCompose(animation1 -> AnimateBuilder.with(qiContext).withAnimation(animation1).buildAsync())
        .andThenCompose(animate1 -> animate1.async().run());

一気に見通しが良くなりました。見やすさとバグの少なさは相関がある気もするので、こういったコードの見栄えは割と重要だと考えています。

3.そろそろ本題、Futureの待ち合わせ

Pepper向けのアプリを作っていると発話とアニメーションが両方終わったら次の何かをさせたいようなシーンが良くあります。
そんな時はFuture.waitAllを使うとことが簡単です。

Future<Void> futureBoth = Future.waitAll(futureSay, futureAnimate);
futureBoth.thenConsume(new Consumer<Future<Void>>() {
    @Override
    public void consume(Future<Void> voidFuture) throws Throwable {
        runOnUiThread(new Runnable() {
            @Override
            public void run() {
                getSupportFragmentManager().beginTransaction()
                        .replace(R.id.main_content, NankanoFragment.newInstance())
                        .commit();
            }
        });
    }
});

前の説明で作っておいたfutureSayとfutureAnimateをFuture.waitAllを使って連結してfutureBothを作っています。さらにfutureBothに対して、thenConsumeを使ってFragmentを操作するような処理を入れてみました。Choregraphe的なイメージだとこんな感じですね。

これで、発話とアニメーションが両方終了した後に、thenConsumeで渡したConsumerインスタンスのconsumeメソッドが呼ばれ、Fragmentの変更などが行えます。途中でボタン操作などがあり発話やアニメーションをキャンセルしたい場合は、futureBothのrequestCancellation()でキャンセルすれば発話とアニメーションの両方がキャンセルされます。今回は、Fragmentの変更処理をthenConsumeで繋いでいるので、キャンセルされた場合でもconsumeメソッドが呼ばれ、Fragmentの変更処理は実行されます。

ほとんど余談になりますが、future内はすべてバックグラウンドスレッドで呼ばれますので、画面操作を行いたい場合などにはUIスレッド上で実行しなければならないことを忘れないでください。ここではrunOnUiThreadにRunnableインスタンを渡して実現していますが、Qi.onUiThreadを挟むことでUIスレッド上でコールバックさせることもできます。

Future<Void> futureBoth = Future.waitAll(futureSay, futureAnimate);
futureBoth.thenConsume(Qi.onUiThread(new Consumer<Future<Void>>() {
    @Override
    public void consume(Future<Void> voidFuture) throws Throwable {
        getSupportFragmentManager().beginTransaction()
                .replace(R.id.main_content, NankanoFragment.newInstance())
                .commit();
    }
}));

コードはきれいになりますが、consumeメソッドは必ずバックグラウンドスレッドで呼ばれるというシンプルさを失うので注意も必要です。

以上、Futureの使い方についてざざざーーっとまとめてみました。

1マイクロでも役に立った方は、ぜひコメントやいいね!をよろしくお願いします!
あと、面白いお仕事にお声がけくださーい笑