Kotlin で 非同期処理 Coroutine #1 ~ launch(), async()

ネット上を調べてみてもよくわかりません。

難しい言葉や experimental な仕様の変更などあったりして。

少しづつ試してみながらマスターしていきましょう。

// build.gradle

implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:0.22.2"

//gradle.properties

kotlin.coroutines=enable

kotlinx.coroutines/coroutines-guide-ui.md at master · Kotlin/kotlinx.coroutines

まず、これ。


for (i in 1..10) {
  Timber.d("$i")
  Thread.sleep(1000)
}

非同期にしたいですよね。
launch から始めます。


launch { // @
  for (i in 1..10) {
    Timber.d("$i")
    Thread.sleep(1000)
  }
}


launch {
  for (i in 1..10) {
    Timber.d("$i")
    delay(1000)  // @
  }
}

引数をつけて渡す。

UI :
UIスレッドで実行。

CommonPool :
バックグランドスレッドで実行。


launch(UI) {  // @
  for (i in 1..10) {
    Timber.d("$i")
    delay(1000)
  }
}


launch(CommonPool) {  // @
  for (i in 1..10) {
    Timber.d("$i")
    delay(1000)
  }
}


launch(UI + CommonPool) {  // @
  for (i in 1..10) {
    Timber.d("$i")
    delay(1000)
  }
}

launch() の戻りからキャンセルできます。


val job = launch(UI) {  // @
  for (i in 1..10) {
    Timber.d("$i")
    delay(1000)
  }
}

fab.setOnClickListener {
  job.cancel() // @
}

 

まとめ

UIスレッドに限定されたコルーチンは、UIスレッドをブロックすることなく、UI内の何かを自由に更新して中断することができます。

delay が待っている間UIスレッドはブロックされないのでUIはフリーズしません。ただ単にコルーチンを中断します。

Job.cancelは完全にスレッドセーフでノンブロッキングです。
実際に終了するのを待つことなく、コルーチンがそのジョブをキャンセルするように通知するだけです。 どこからでも呼び出すことができます。

基本的な非同期呼び出しは、launch() と async() の2つ。似ているが戻りが異なる。


E を出力させない Double.toPlainString()

こういうの。助かります。


double d1 = 0.00000000000000000000000000000000000000000123456789012345678901234567890d;
double d2 = 1234567890123456789012345678900000000000000000000000000000000000000000000d;
double d3 = 1234567890.1234567890123456789d;

System.out.println(BigDecimal.valueOf(d1).toPlainString());
// 0.0000000000000000000000000000000000000000012345678901234568
System.out.println(BigDecimal.valueOf(d2).toPlainString());
// 1234567890123456800000000000000000000000000000000000000000000000000000000
System.out.println(BigDecimal.valueOf(d3).toPlainString());
// 1234567890.1234567

Java の double を素直な String にしたい、E 要らない - Qiita

直感的には表示できなく、ネット上を放浪していました。

Kotlin では、Extention Functions に追加しておきますわ。


fun Double.toPlainString(): String
    = BigDecimal.valueOf(this).toPlainString()


val d1 = 0.00000000000000000000000000000000000000000123456789012345678901234567890
val d2 = 1234567890123456789012345678900000000000000000000000000000000000000000000.0
val d3 = 1234567890.1234567890123456789
val d4 = 4.34e-05
val d5 = 5.6e-07

println(d1.toPlainString())
println(d2.toPlainString())
println(d3.toPlainString())
println(d4.toPlainString())
println(d5.toPlainString())


I/System.out: 0.0000000000000000000000000000000000000000012345678901234568
I/System.out: 1234567890123456800000000000000000000000000000000000000000000000000000000
I/System.out: 1234567890.1234567
I/System.out: 0.0000434
I/System.out: 0.00000056

Java Double に toPlainString() がないのは何か理由があるのかも。とは思うが。


イベントハンドラとラムダ

おさらい


val printHelloWorld = {
   println("Hello, world!")
}

printHelloWorld()
//printHelloWorld.invoke()

これで以下が出力される。

Hello, world!

同様に引数を渡す。


val printGreeting = { word: String, name: String ->
   println("$word, $name")
}

printGreeting("おはよう", "太郎")
printGreeting.invoke("こんにちは", "次郎")

おはよう, 太郎
こんにちは, 次郎

これまでのコールバック

inferface を使ったコールバックは kotlin では以下のように書けます。


interface CallBackListener {
    fun onHoge(foo: String, bar: Int)
}

// caller
var callback: CallBackListener? = null
callback?.onHoge("foo", 100)

// callee
val callback = object : CallBackListener {
    override fun onHoge(foo: String, bar: Int) {
        print("$foo : $bar")
    }
}

これを typealias で。


typealias CallBackListener = (foo: String, bar: Int) -> Unit

// caller
var callback: CallBackListener? = null
callback?.invoke("foo", 100)

// callee
val callback = { foo, bar ->
    print("$foo : $bar")
}

【Kotlin】 TypeAliasで関数リテラルに名前を付ける - Qiita

イベントハンドラ

例えば、RecyclerView でそれぞれのアイテムのViewHolder内のある要素のクリックイベントを拾いたい時、Activity(View)までが遠いですよね。

Activity(View)
↓↑
RecyclerViewAdapter
↓↑
ViewHolder

少しでも、コールバック記述をシンプルにして見通し良くしておきたいものです。

ここで Type Alias を使います。

typealias EventHandler = (T) -> Unit

java - Kotlin: Use a lambda in place of a functional interface? - Stack Overflow

1. ViewHolder にハンドラー記述。クラスの外。

ItemViewHolder:


internal typealias ItemHandler = (Item) -> Unit

それを拡張して、ハンドラークラスを作ります。
invoke を override します。

ShareItemHandler:


internal class ShareItemHandler(
  private val activity: Activity
) : ItemHandler {

  override fun invoke(item: Item) {
    IntentBuilder.from(activity)
        .setType("text/plain")
        .setChooserTitle("Share ${item.name} to ?")
        .setText("${$item.name} ${item.address}")
        .startChooser()
  }
}

ClipboardCopyItemHandler:


internal class ClipboardCopyItemHandler(
  private val context: Context
) : ItemHandler {

  override fun invoke(item: Item) {
    val clipboard = context.systemService<ClipboardManager>()
    clipboard.primaryClip = ClipData.newPlainText(item.name, item.address)
  }
}

2. Activity で そのインスタンスを生成して
RecyclerAdapter経由でViewHolder内にセットしていきます。

以下関係部分のみ抜粋。

MainActivity:


val onCopy = ClipboardCopyItemHandler(this)
val onShare = ShareItemHandler(this)

// ...

val layoutManager = LinearLayoutManager(this)
recyclerView.layoutManager = layoutManager
val adapter = ItemAdapter(layoutInflater, onCopy, onShare)
recyclerView.adapter = adapter

ItemAdapter:


internal class ItemAdapter(
  private val inflater: LayoutInflater,
  private val onCopy: (Item) -> Unit,
  private val onShare: (Item) -> Unit
) : RecyclerView.Adapter<ItemViewHolder>() {

  // ...

  override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ItemViewHolder {
    val view = inflater.inflate(R.layout.item, parent, false)
      return ItemViewHolder(view, onCopy, onShare)
    }

ItemViewHolder:



internal typealias ItemHandler = (Item) -> Unit // ***

internal class ItemViewHolder(
  private val root: View,
  private val onCopy: ItemHandler
  private val onShare: ItemHandler,
) : ViewHolder(root), OnClickListener, OnMenuItemClickListener {

  // ...  

  override fun onClick(view: View) = when(view.id) {
    R.id.copy -> onCopy(item!!)
    R.id.share -> onShare(item!!)
    else -> throw IllegalArgumentException("Unknown menu item: $menuItem")
  }

なんだか素晴らしいですね。

RecyclerView利用時のテンプレートとして。