OE_uia Tech Blog

ScalaMatsuri / Scala / Android / Bioinfomatics

Scala標準のPromiseがAndroidで便利だという話

| Comments

この記事は、Scala Advent Calendar 13日目です。

今日はscala.concurrent.Promiseの話をします。

Promise - Scala Standard Library 2.11.7 - scala.concurrent.Promise

そもそもPromiseって使いどころがわかりにくいですよね。他人のコードで使ってるの、ほとんど見たことがありません。

公式ドキュメントではProducer-Consumerパターンでの使い方を解説していますが、現実にこの使い方が必要になるケースってあまり遭遇せず、たいていの場合はFuture同士のflatMapによる合成で事足りてしまうと思います。

Future と Promise - Scala Documentation

ところが、実はAndroidアプリ開発では頻繁に遭遇するあのパターンが、Promiseを使うと非常に取り回しが良くなりますので、紹介したいと思います。

それは、Intentで外部Activityから何かしらの値を取得し、onActivityResultでその値を受け取るパターンです。

Intent | Android Developers

Activity#onActivityResult | Android Developers

Androidでは、アプリ外部との連携をIntentという仕組みを使って制御しています。

取得したい情報や実行したい処理(Action)をIntentにセットし、startActivityForResultメソッドに渡すことで、外部アプリなどを起動し必要なデータを取得させたうえで、自分のアプリに返ってくる(onActivityResultメソッドが呼ばれ、引数にデータが渡される)ことが出来ます。

このIntentは大変便利な仕組みですが、その一方で一連のパイプラインの記述がstartActivityForResultonActivityResultの間で分断されてしまい、データの流れが追いにくいコードになっています。

例えば以下のような、ボタンをクリックすると外部アプリで写真を選択させて、自アプリに表示するだけのアプリについて考えてみましょう。

~startActivityForResult onActivityResult ~
Promise無しの例(パイプラインが分断される)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
class MyActivity extends Activity with TypedFindView{
lazy val IMAGE_FETCH_ID = 12345

override def onCreate(bundle: Bundle) {
   //Activity初期化処理など
   //...
   findView(TR.button_image).onClick{
     _ =>
      val intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
      intent.setType("image/*")
      startActivityForResult(intent, IMAGE_FETCH_ID)
      //データパイプラインがここで分断
    }
 }

 override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = {
  requestCode match {
    case IMAGE_FETCH_ID =>
    //データパイプラインがここから継続
    if (resultCode == Activity.RESULT_OK)
      findView(TR.image).setImageURI(data.getData)
     else{
       //failed. Do something if needed.  
     }
    case _ => //do nothing
  }
}
}

これをPromiseを使って書き換えてみましょう。

Promiseによって、startActivityForResultから onActivityResultまでの流れを、単一Futureインスタンスの中に閉じ込めたかのように扱うことが出来ます。

これでパイプラインの記述をスッキリ書くことが出来るようになりました。

Promise有りの例
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class MainActivity extends Activity with TypedFindView with ImageLoadable{
lazy val uiContext = UIContext(this)
 override def onCreate(bundle: Bundle) {
   //Activity初期化処理など
   //...

   //データパイプラインを分断させず、そのまま記述できる
   findView(TR.button_image).onClick{
    _ => chooseImageUri().foreach{
      bmp => findView(TR.image).setImageURI(bmp)
    }(uiContext)
   }
 }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
trait ImageLoadable extends Activity {
  lazy val IMAGE_FETCH_ID = 12345
  private var promise: Option[Promise[Uri]] = None

  def chooseImageUri(): Future[Uri] = {
    promise.filterNot(_.isCompleted).foreach(_.failure(new InterruptedException("Asked to load another image. Aborted.")))
    val p = Promise[Uri]()
    promise = Some(p)
    val intent = new Intent(Intent.ACTION_PICK, MediaStore.Images.Media.EXTERNAL_CONTENT_URI)
    intent.setType("image/*")
    startActivityForResult(intent, IMAGE_FETCH_ID)
    p.future
  }

  override def onActivityResult(requestCode: Int, resultCode: Int, data: Intent): Unit = {
    super.onActivityResult(requestCode,resultCode,data)
    requestCode match {
      case IMAGE_FETCH_ID => if (resultCode == Activity.RESULT_OK)
        promise.foreach(_.success(data.getData))
      else
        promise.foreach(_.failure(ImageNotAvailableException("Failed to fetch image.")))
      case _ => //do nothing
    }
  }
}

ビルド可能なサンプルソースコードはこちらです。

taisukeoe/ScalaPromiseDemo

この例と似た、より一般的な例が以下のPromiseを使ったCallback APIのFuture化です。 しかしCallback APIのFuture化はscalaz.concurrent.Task.asyncでも実現できますが、上記の例は実現できません(onActivityResultのせい)。

ScalaFPEvent - ScalaStdFutureExmaple

Promiseって意外と使えるジャン、と思ってもらえれば幸いです。

それでは。

Comments