20200323のAndroidに関する記事は8件です。

どこでもAndroidのリソースファイルを参照する方法

Androidのリソースファイルが参照できない

Androidのアプリ開発をしていて、変数に文字列を入れる時、なるべくリソースファイルを参照した方が良いと思うけれどもgetStringメソッドが使えたり使えなかったり、

リソースファイルのように@string/textなど色々試してみるけれども、参照できないと思ったことが多かったのでメモ。

いつリソースがファイルが参照できていないのか

「getStringが使えたり使えなかったりするけれども、いつ使えていつ使えないのか?」と思い試してみたところ、

  • ActivityやFragmentではgetStringメソッドを使える。

  • ViewModelファイルやActivityやFragment内でもcompanion object内だと参照出来ない

ということが分かりました。Fragmentファイルでも大体companion object内こそリソース参照したいことが多いのに困りました。

解決法

解決策として、これで良いのかは分かりませんが、

  • ViewModelの場合

ViewModel内からだと、以下を使って参照しました。

context.resources.getString(R.string.sample)

contextはViewModelのメソッドを呼び出すfragment側から、contextを渡しました。

  • companion object内の場合

普段、Koin設定したりしているSampleApplicationファイル内で、

companion object {
        lateinit var context: Context
    }

    override fun onCreate() {
        super.onCreate()
        context = this
    }

とした上で、ActivityやFragmentのcompanion object内で

SampleApplication.context.getString(R.string.sample)

と、しました。

特にViewModelの方はcontextをメソッドで渡すという点が、スッキリしない気がします。

追記

場合にもよると思いますが、activityやFragementのcompanion object内でリソースファイルを参照している場合、そもそもcompanion objectにする必要があるかどうか疑った方が良いかもしれません。

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kotlinで他のところタップでキーボードを引っ込める

キーボードがいい感じに引っ込んでくれない

image.png
image.png

これだとログインボタンを押すことができません...

他の領域にクリックイベントをつけることで解決する

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
    android:orientation="vertical" android:layout_width="match_parent"
    android:layout_height="match_parent">

    <View
        android:id="@+id/upper_space"
        android:layout_width="match_parent"
        android:layout_height="200dp" />

    <EditText
        android:id="@+id/id"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="ID"/>

    <View
        android:id="@+id/top_space"
        android:layout_width="match_parent"
        android:layout_height="30dp" />

    <EditText
        android:id="@+id/password"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:hint="パスワード"/>

    <View
        android:id="@+id/middle_space"
        android:layout_width="match_parent"
        android:layout_height="30dp" />

    <!-- 本当はこの部分にもEditTextがひとつあったのですが今は表示していません -->

    <View
        android:id="@+id/bottom_space"
        android:layout_width="match_parent"
        android:layout_height="130dp" />

    <Button
        android:id="@+id/login_button"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:text="ログイン" />

</LinearLayout>

では,プログラムを書いていきます。

プログラム

他のEditTextがクリックされたときはキーボードを引っ込める必要はないと思ったので,そのときはキーボードを引っ込める処理は行いません。

import android.content.Context
import android.content.Intent
import android.hardware.input.InputManager
import android.os.Bundle
import android.view.inputmethod.InputMethodManager
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.getSystemService
import kotlinx.android.synthetic.main.activity_login.*

class LoginActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContentView(R.layout.activity_login)

        upper_space.setOnClickListener {
            this.hideKeyboard()
        }

        top_space.setOnClickListener {
            this.hideKeyboard()
        }

        middle_space.setOnClickListener {
            this.hideKeyboard()
        }

        bottom_space.setOnClickListener {
            this.hideKeyboard()
        }
    }

    private fun hideKeyboard() {
        val view = this@LoginActivity.currentFocus
        if (view != null) {
            val manager = getSystemService(Context.INPUT_METHOD_SERVICE) as InputMethodManager
            manager.hideSoftInputFromWindow(view.windowToken, 0)
        }
    }
}

スペース部分のViewにクリックイベントをつけて,キーボードを引っ込める関数のhideKeyboard()を呼び出しています!

これでキーボードが引っ込むようになりました。

参考リンク

https://qiita.com/bassaer/items/0e412d9f36b2113ee8d0

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flutter:タブの内容に合わせてAppBarを切り替える

はじめに

最近Flutterを始めました。
その際、tab(見ているページ)に合わせてAppBarを切り替えたくなったので、実装してみました。間違い等ありましたら、ご指摘お願いします。

開発環境 (flutter version)

Flutter (Channel master, v1.15.19-pre.8, on Microsoft Windows [Version 10.0.19041.153], locale en-US)

ソースコード

コピペでも動きます。
tabcontrollerを使用して今どのタブを開いているかを確認し、どのAppBarを出すか決めています。

main.dart
import 'package:flutter/material.dart';

void main() => runApp(MyApp());

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Flutter AppBar Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: DefaultTabController(
        child: MyHomePage(),
        length: 2,
      ),
    );
  }
}

class MyHomePage extends StatefulWidget {
  @override
  _MyHomePageState createState() => _MyHomePageState();
}

class _MyHomePageState extends State<MyHomePage>
    with SingleTickerProviderStateMixin {
  TabController tabController;
  final List<Widget> _tabs = [
    AppBarA(),
    AppBarB(),
  ];
  Widget _myHandler;

  void initState() {
    super.initState();
    tabController = new TabController(vsync: this, length: 2);
    _myHandler = _tabs[0];
    tabController.addListener(_handleSelected);
  }

  @override
  void dispose() {
    tabController.dispose();
    super.dispose();
  }

  void _handleSelected() {
    setState(() {
      _myHandler = _tabs[tabController.index];
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: _tabs[tabController.index],
      body: TabBarView(
        controller: tabController,
        children: [
          Container(
            color: Colors.red,
            child: Center(
              child: Icon(
                Icons.adb,
                color: Colors.green,
                size: 150.0,
              ),
            ),
          ),
          Container(
            color: Colors.green,
            child: Center(
              child: Icon(
                Icons.loyalty,
                color: Colors.pink,
                size: 150.0,
              ),
            ),
          ),
        ],
      ),
      bottomNavigationBar: SafeArea(
        child: Material(
          child: TabBar(
            controller: tabController,
            unselectedLabelColor: Colors.black.withOpacity(0.3),
            unselectedLabelStyle: TextStyle(fontSize: 12.0),
            labelColor: Colors.pink[400],
            labelStyle: TextStyle(fontSize: 16.0),
            indicatorColor: Colors.pink,
            indicatorWeight: 2.0,
            tabs: [
              Tab(
                child: Icon(
                  Icons.favorite,
                ),
              ),
              Tab(
                child: Icon(
                  Icons.explore,
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

class AppBarA extends StatefulWidget with PreferredSizeWidget {
  @override
  _AppBarAState createState() => _AppBarAState();
  @override
  Size get preferredSize => Size.fromHeight(kToolbarHeight);
}

class _AppBarAState extends State<AppBarA> {
  @override
  Widget build(BuildContext context) {
    return AppBar(
      title: Text(
        'A',
        style: TextStyle(
          fontSize: 30.0,
        ),
      ),
      elevation: 1.0,
    );
  }
}

class AppBarB extends StatefulWidget with PreferredSizeWidget {
  @override
  _AppBarBState createState() => _AppBarBState();
  @override
  Size get preferredSize => Size.fromHeight(kToolbarHeight);
}

class _AppBarBState extends State<AppBarB> {
  @override
  Widget build(BuildContext context) {
    return AppBar(
      title: Text(
        'B',
        style: TextStyle(
          fontSize: 30.0,
        ),
      ),
      elevation: 1.0,
    );
  }
}

結果

このように動作します。
タブを移動することでAppBarが切り替わっています。
ezgif.com-video-to-gif.gif

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

【エラー】D8: Program type already present: android.support.v4.os.ResultReceiverの解決方法

はじめに

いつも通りionic5で開発したアプリをionic cordova run androidでビルドしてAndroid端末にインストールしようとしたのですが、D8: Program type already present: android.support.v4.os.ResultReceiverというエラーが発生しました。
解決方法を記載します。

解決方法

  1. ionic cordova plugin add cordova-plugin-androidxを実行
  2. ionic cordova plugin add cordova-plugin-androidx-adapterを実行

cordova-plugin-androidxとは

CordovaプロジェクトでAndroidサポートライブラリの後継であるAndroidXを有効にします。
https://github.com/dpa99c/cordova-plugin-androidx

cordova-plugin-androidx-adapterとは

CordovaプロジェクトにAndroidサポートライブラリとAndroidXの両方を参照するプラグイン/ライブラリが含まれている場合に必要。
Androidサポートライブラリへの参照をAndroidXにマッピングしてくれる。
https://github.com/dpa99c/cordova-plugin-androidx-adapter

おわりに

開発中にインストールしたプラグインでAndroidXが必要だったため、エラーが起きていたようです。

参考

https://forum.ionicframework.com/t/d8-program-type-already-present/166812/4

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flutter gallery_saver Androidのみ保存でエラーになる問題の解決

gallery_saver は、写真・画像をカメラロール、ギャラリーに保存するライブラリ。
※ 使用バージョン gallery_saver 1.0.7

Androidのみ、同一ファイル名で保存をかけた場合、エラーとなる問題がある。

発生するエラー。

E/AndroidRuntime(21661): FATAL EXCEPTION: DefaultDispatcher-worker-1
E/AndroidRuntime(21661): Process: com.example.myapp, PID: 21661
E/AndroidRuntime(21661): java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String android.net.Uri.getLastPathSegment()' on a null object reference
E/AndroidRuntime(21661):    at android.content.ContentUris.parseId(ContentUris.java:85)
E/AndroidRuntime(21661):    at carnegietechnologies.gallery_saver.FileUtils.insertImage(FileUtils.kt:79)
E/AndroidRuntime(21661):    at carnegietechnologies.gallery_saver.GallerySaver$saveMediaFile$1$success$1.invokeSuspend(GallerySaver.kt:69)
E/AndroidRuntime(21661):    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
E/AndroidRuntime(21661):    at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
E/AndroidRuntime(21661):    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
E/AndroidRuntime(21661):    at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
E/AndroidRuntime(21661):    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
D/FlutterView(21661): Detaching from a FlutterEngine: io.flutter.embedding.engine.FlutterEngine@97f0662

問題は確認されているがリリースまで進んでいない模様。
fix error if image was selected twice #41

関連issue

ひとまずの解決として、保存をかけたい一時保存しているファイルのファイル名を、ランダムにするなどして対応することで、エラーが発生せず保存ができた。

/// ランダムな文字列を生成するメソッド
static String randomString(int length) {
  const _randomChars =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  const _charsLength = _randomChars.length;

  final rand = Random();
  final codeUnits = new List.generate(
    length,
    (index) {
      final n = rand.nextInt(_charsLength);
      return _randomChars.codeUnitAt(n);
    },
  );
  return new String.fromCharCodes(codeUnits);
}

// GallerySaver.saveImageに渡すため一時保存するファイル名をランダムにする
const tmpFileName = randomString(10) + '.png';

参考URL

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Flutter gallery_saver Androidのみ保存でエラーになる問題を解決

gallery_saver は、写真・画像をカメラロール、ギャラリーに保存するライブラリ。
※ 使用バージョン gallery_saver 1.0.7

Androidのみ、同一ファイル名で保存をかけた場合、エラーとなる問題がある。
エラーが発生しクラッシュしてしまう。

E/AndroidRuntime(21661): FATAL EXCEPTION: DefaultDispatcher-worker-1
E/AndroidRuntime(21661): Process: com.example.myapp, PID: 21661
E/AndroidRuntime(21661): java.lang.NullPointerException: Attempt to invoke virtual method 'java.lang.String android.net.Uri.getLastPathSegment()' on a null object reference
E/AndroidRuntime(21661):    at android.content.ContentUris.parseId(ContentUris.java:85)
E/AndroidRuntime(21661):    at carnegietechnologies.gallery_saver.FileUtils.insertImage(FileUtils.kt:79)
E/AndroidRuntime(21661):    at carnegietechnologies.gallery_saver.GallerySaver$saveMediaFile$1$success$1.invokeSuspend(GallerySaver.kt:69)
E/AndroidRuntime(21661):    at kotlin.coroutines.jvm.internal.BaseContinuationImpl.resumeWith(ContinuationImpl.kt:33)
E/AndroidRuntime(21661):    at kotlinx.coroutines.DispatchedTask.run(Dispatched.kt:238)
E/AndroidRuntime(21661):    at kotlinx.coroutines.scheduling.CoroutineScheduler.runSafely(CoroutineScheduler.kt:594)
E/AndroidRuntime(21661):    at kotlinx.coroutines.scheduling.CoroutineScheduler.access$runSafely(CoroutineScheduler.kt:60)
E/AndroidRuntime(21661):    at kotlinx.coroutines.scheduling.CoroutineScheduler$Worker.run(CoroutineScheduler.kt:742)
D/FlutterView(21661): Detaching from a FlutterEngine: io.flutter.embedding.engine.FlutterEngine@97f0662

問題は確認されているがリリースまで進んでいない模様。
fix error if image was selected twice #41

関連issue

ひとまずの解決として、保存をかけたい一時保存しているファイルのファイル名を、ランダムにするなどして対応することで、エラーが発生せず保存ができた。

/// ランダムな文字列を生成するメソッド
static String randomString(int length) {
  const _randomChars =
      "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
  const _charsLength = _randomChars.length;

  final rand = Random();
  final codeUnits = new List.generate(
    length,
    (index) {
      final n = rand.nextInt(_charsLength);
      return _randomChars.codeUnitAt(n);
    },
  );
  return new String.fromCharCodes(codeUnits);
}

// GallerySaver.saveImageに渡すため一時保存するファイル名をランダムにする
const tmpFileName = randomString(10) + '.png';

参考URL

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Kotlin Coroutine 入門2: 並列実行と Structured Concurrency と例外

前回は Kotlin の coroutine の基本として、起動と suspend 関数の解説をしました。今回は coroutine を並列で起動する場合に必要になってくる概念を解説していきます。

シナリオ: 最安値を見つけろ!

今回は「二つの販売店の API を使って商品の価格を比較し、最安値を取得する」というシナリオを考えます。動作を見やすくするために Store という抽象クラスを用意しました。

/** お店の商品情報を提供する抽象クラス */
abstract class Store(private val name: String) {
    /** 価格取得の実装。サブクラスが実装する。 */
    protected abstract suspend fun doGetPrice(itemCode: String): Int

    /** doGetPrice を呼び出し、取得開始と終了、エラーが出た時にログを出力します。 */
    suspend fun itemPrice(itemCode: String): Int {  }
}

さらにサンプルのお店を二つ用意しました。

/** 普通の店。処理に2秒かかる。 */
object AStore : Store("AStore") {
    override suspend fun doGetPrice(itemCode: String): Int {
        delay(2000)
        return 49998
    }
}

/** ちょっと安い店。AStore と同じく処理に2秒かかる。 */
object BStore : Store("BStore") {
    override suspend fun doGetPrice(itemCode: String): Int {
        delay(2000)
        return 49800
    }
}

実際に二つのお店の商品価格を取得して表示してみます。

fun runMain(): Job = scope.launch {
    val price1 = AStore.itemPrice("4901170017583")
    val price2 = BStore.itemPrice("4901170017583")
    println("⭐⭐AStore: $price1, BStore: $price2⭐⭐")
}

▶️ 実行してみる

実行結果

 0.621: AStore: 「4901170017583」の価格を取得します
 2.723: AStore: 「4901170017583」の価格は 49998 でした
 2.724: BStore: 「4901170017583」の価格を取得します
 4.725: BStore: 「4901170017583」の価格は 49800 でした
⭐⭐AStore: 49998, BStore: 49800⭐⭐

各ストアの itemPrice() が呼び出されると、価格取得処理の実行前後にログが出力されます。ログは処理全体の開始何秒のイベントかが記録されています。例外が発生した時もログが出力されます。

さて、準備ができたので本題に入りましょう。

並列実行と Structured Concurrency

async と await

実際に AStoreBStore の価格を比較し、最安値を計算してみましょう。

fun runMain(): Job = scope.launch {
    val price1 = AStore.itemPrice("4901170017583")
    val price2 = BStore.itemPrice("4901170017583")
    val bestPrice = min(price1, price2)
    println("⭐⭐最安価格: $bestPrice⭐⭐")
}

▶️ 実行してみる

これを実行すると、以下のように出力されます。

 0.105: AStore: 「4901170017583」の価格を取得します
 2.129: AStore: 「4901170017583」の価格は 49998 でした
 2.130: BStore: 「4901170017583」の価格を取得します
 4.131: BStore: 「4901170017583」の価格は 49800 でした
⭐⭐最安価格: 49800⭐⭐

まず AStore の価格の取得のために2秒中断し、その後 BStore の価格の取得に2秒中断、合計処理に4秒かかっていて、少々効率的ではありません。同時に itemPrice() を呼び出せば2秒で両方取得する事ができるはずです。

asyncawait を使う事で並列実行が可能になります。

fun runMain(): Job = scope.launch {
    val price1 = async { AStore.itemPrice("4901170017583") }
    val price2 = async { BStore.itemPrice("4901170017583") }
    val bestPrice = min(price1.await(), price2.await())
    println("⭐⭐最安価格: $bestPrice⭐⭐")
}

▶️ 実行してみる

itemPrice()async { … } で囲む事で、処理を裏で実行させながら次の処理が実行できるようになります。async の結果に対して await() を呼ぶと、処理が終了するまで中断してからその結果を受け取ります。

実行結果

 0.140: AStore: 「4901170017583」の価格を取得します
 0.148: BStore: 「4901170017583」の価格を取得します
 2.165: AStore: 「4901170017583」の価格は 49998 でした
 2.166: BStore: 「4901170017583」の価格は 49800 でした
⭐⭐最安価格: 49800⭐⭐

最初に二つの処理がほぼ同時に開始され、それぞれ2秒後に処理が終了している事がわかると思います。これで2秒後に「最安価格: 49800」が出力されました。便利!!??


async/await じゃんって思った?
async で返している値は Deferred<T> と呼ばれる、非同期の計算結果を返すオブジェクトです。これは JavaScript の Promise や RxJava の Single と非常に似ています。これを見て他のプラットフォームに慣れている人は「Deferred を返す関数」を作ろうと考えるかもしれませんが、 これは Kotlin の coroutine の良さを生かせないやり方 になります1。async と await は複数の処理を並列実行をしたい場合にのみ、呼び出し側が使う事が推奨されます。

Coroutine Builder と CoroutineScope

launchasync といった、coroutine を起動する関数を「coroutine builder」と呼びます。ほとんど2の coroutine builder は CoroutineScope の拡張関数で定義されています。そして先ほどの例の async はトップレベル関数のように見えて 実は CoroutineScope の拡張関数です。ややこしいですね。

このカラクリを理解するには、launch メソッドの定義を見ると良いです。公式ドキュメントを見ると launch メソッドは以下のように書かれてあります。

fun CoroutineScope.launch(
    context: CoroutineContext = EmptyCoroutineContext,
    start: CoroutineStart = CoroutineStart.DEFAULT,
    block: suspend CoroutineScope.() -> Unit
): Job

(公式ドキュメント: launch - kotlinx-coroutines-coreより転載)

最初の二つは省略可能で、普段指定しているのは三つ目の block なので、その型に注目してみましょう。CoroutineScope.() -> Unit と書かれてあります。もしかしたら見慣れない人もいるかも知れませんが、これは「CoroutineScope を this に指定したレシーバ付き関数リテラル」です。apply { … } 関数を CoroutineScope に対して実行していると考えると良いと思います。suspend も指定されているので suspend 関数でもあります。

launch に限らず全ての coroutine builder のブロック引数は this に CoroutineScope が指定されます。そのためレシーバーを指定しないで launchasync を呼び出すと現在のスコープ (=this) に coroutine を生成させる事になります。特に async メソッドはレシーバーを指定しない方が良い事が多いでしょう。

後述しますが、この仕組みは Kotlin の coroutine の設計思想の重要な部分となっています。

Structured Concurrecy

coroutine builder によって起動された coroutine と、そのブロックで this として渡されたスコープは表裏一体の関係にあります。具体的にいうと、coroutine をキャンセルすればそのブロックのスコープにもキャンセルが発行されます。

これは何を指すのでしょうか。

試しに二つの処理を同時に実行してみます。この時片方は現在のスコープに対して async を、もう片方は scope.async を呼び出します。

fun runMain(): Job = scope.launch {
    try {
        // 片方は scope から呼び出す。
        val price1 = async { AStore.itemPrice("4901170017583") }
        val price2 = scope.async { BStore.itemPrice("4901170017583") }
        val bestPrice = min(price1.await(), price2.await())
        println("⭐️⭐️最安価格: $bestPrice⭐️⭐️")
    } catch (e: Exception) {
        println("??取得に失敗しました: $e??")
    }
}

この時に runMain() が終了する前にキャンセルするとどうなるでしょうか?

// 実行して即キャンセルする。
val job: Job = runMain()

println("キャンセル呼び出し")
job.cancel()

▶️ 実行してみる

実行結果

 0.177: BStore: 「4901170017583」の価格を取得します
 0.169: AStore: 「4901170017583」の価格を取得します
キャンセル呼び出し
 0.724: AStore: 「4901170017583」の価格の取得がキャンセルされました
??取得に失敗しました: kotlinx.coroutines.JobCancellationException: Job was cancelled; job="coroutine#2":StandaloneCoroutine{Cancelling}@6ac1c3f0??
 2.181: BStore: 「4901170017583」の価格は 49800 でした

runMain() の処理をキャンセルすると、現在のスコープによって起動した AStore への async 処理もキャンセルされました。一方で scope.async によって起動した BStore への処理はキャンセルされず、結果 BStore の処理は正常終了します。

つまりキャンセルを発行すると、ブロックのスコープにもキャンセルが発行され、そのスコープによって起動した coroutine もキャンセルされる…という伝播が起こります。

この仕組みを使えば、入れ子で呼び出した coroutine は外側の coroutine に連動して自動でキャンセルさせる事ができます。

この CoroutineScope の階層化の考えを structured concurrency と呼んでいます。structured concurrency における各スコープは以下のような関係を持っています。

  • 内側のスコープ(の coroutine) が全て終了するまで外側のスコープは終了しない
  • 外側のスコープに対してキャンセルが要求されれば、内側のスコープに対してもキャンセルが伝播する

逆に言えば、「内側のスコープが動いているのに外側の処理が終了している」といった事は起こらないようになっています。

実際に使用する際は、あえてこの階層化を超えて実行したい処理(ログや通知など)は viewModelScopeGlobalScope のような階層外にある CoroutineScope を使い、それ以外の場合は現在のスコープを使うのが一つの良い指針と言えると思います。

structured concurrecy に関するさらに詳しい話に興味がある方は、以下が参考になると思います。

Structured Concurrency と suspend 関数

さて、suspend 関数から coroutine builder を呼び出す事を考えてみます。AStoreBStore の安い方の価格で商品を扱う CheapestStore を作ってみます。

/** 最安価格で取り扱うストア */
object CheapestStore : Store("CheapestStore") {
    override suspend fun doGetPrice(itemCode: String): Int {
        val price1 = async { AStore.itemPrice(itemCode) }
        val price2 = async { BStore.itemPrice(itemCode) }
        return min(price1.await(), price2.await())
    }
}

// メイン処理
fun runMain(): Job = viewModelScope.launch {
    println("⭐️⭐️最安価格: ${CheapestStore.itemPrice("4901170017583")}⭐️⭐️")
}

なんと、コンパイルエラーになります。というのも、suspend 関数は launch { … } の中のブロックとは違い現在のスコープが指定されてないため、coroutine builder を呼び出す事ができないのです。

suspend 関数から coroutine builder を呼び出すためには、coroutineScope { … } (最初の c は小文字)関数を使います。

/** 最安価格で取り扱うストア */
object CheapestStore : Store("CheapestStore") {
    override suspend fun doGetPrice(itemCode: String): Int = coroutineScope {
        val price1 = async { AStore.itemPrice(itemCode) }
        val price2 = async { BStore.itemPrice(itemCode) }
        min(price1.await(), price2.await())
    }
}

▶️ 実行してみる

coroutineScope { … } のブロックにスコープが割り当てられ、coroutine builder を呼び出す事ができるようになります。

coroutineScope { … }launchasync と違い、新しく coroutine を起動せずに同期的に実行します。つまり suspend 関数の呼び出しが終わって値を返すタイミングで、その中で起動した(階層外の CoroutineScope で起動したもの以外の) coroutine は全て終了している事が保証されます。

Structured Concurrency と例外

Kotlin の structured concurrency を考える際、例外の扱いに関する理解は欠かせません。直感的でない面もあるのでここで解説します。

実際に例外を投げてみます。今回は商品情報を取得しようとするとエラーが起きる XStore というのを用意しました。

/** 例外を投げてしまうストア */
object XStore : Store("XStore") {
    override suspend fun doGetPrice(itemCode: String): Int {
        error("サーバーダウン")
    }
}

今まで使っていた AStore と今回の XStore に対して非同期で itemPrice を投げてみます。動作の詳細が分かるように、今回は各行にログを仕込んでおきます。

// メイン処理。片方のストアは例外を投げる。
fun runMain(): Job = scope.launch {
    try {
        println("フェーズ1: AStore にアクセス")
        val price1 = async { AStore.itemPrice("4901170017583") }
        println("フェーズ2: XStore にアクセス")
        val price2 = async { XStore.itemPrice("4901170017583") }
        println("フェーズ3: AStore の結果を取得")
        val p1 = price1.await()
        println("フェーズ4: XStore の結果を取得")
        val p2 = price2.await()
        println("フェーズ5: 最安値を計算")
        val bestPrice = min(p1, p2)
        println("⭐️⭐️最安価格: $bestPrice⭐️⭐️")
    } catch (e: Exception) {
        println("??取得に失敗しました: $e??")
    }
}

さあ、実行すると起動した各 coroutine に何が起きるでしょうか?特に、メインの処理はどのフェーズで落ちる(あるいは落ちないで完了する)でしょうか?一度考えてみましょう!

▶️ 実行してみる

実行結果

フェーズ1: AStore にアクセス
フェーズ2: XStore にアクセス
フェーズ3: AStore の結果を取得
 0.131: AStore: 「4901170017583」の価格を取得します
 0.132: XStore: 「4901170017583」の価格を取得します
 0.142: XStore: 「4901170017583」の価格の取得が失敗しました: (java.lang.IllegalStateException: サーバーダウン)
??取得に失敗しました: kotlinx.coroutines.JobCancellationException: Parent job is Cancelling; job="coroutine#2":StandaloneCoroutine{Cancelling}@5cf18424??
 0.247: AStore: 「4901170017583」の価格の取得がキャンセルされました

実行結果をみると以下の事がわかります。

  • メイン処理だけでなく、無関係の AStore の処理にもキャンセルが発行される
  • メイン処理はフェーズ 3 の無関係の AStore の処理に対する await() でキャンセルが発行される

なんと、XStore の処理や await() だけでなく、全く無関係の AStore に対する挙動にも影響を与えています!この挙動は JavaScript などの言語における async/await と全く異なります。

coroutine で発生した例外(CancellationException 以外)をキャッチしなかった場合は、後述する一部の例外を除き一番外側の coroutine (およびその内側にある coroutine)に対してキャンセルが発行されます。

このような挙動になっている理由は、失敗した一連の処理を速やかに終了させるためのようです3。実際今回の例で言えば、XStore が失敗している事がわかっているのに AStore の処理を待つのは時間の無駄なので、全てキャンセルしてしまった方が都合が良いです。

coroutine の例外をハンドリングする方法

しかし例外を全くハンドリングできないというのでは困ってしまいます。代表的な対応策はいくつかあります。

  1. 入れ子で coroutine を使わない
  2. coroutineScope { … } を使う
  3. CoroutineExceptionHandlerSupervisorJob を使う

一つ目は特に特別な仕組みを知らずともできる方法です。並列実行して高速化したい理由が特にない場合に推奨される方法と言えるでしょう。

今回は二つ目の方法を紹介します。三つ目の方法は今回は説明しません。

coroutineScope { … } と例外

coroutineScope { … } は先ほど suspend 関数で coroutine builder を使うための方法として紹介しました。ではこの状態で例外が発生したらどうなるでしょうか?

/** 最安価格で取り扱うストア。扱うストアの片方が例外を投げる。 */
object CheapestStore : Store("CheapestStore") {
    override suspend fun doGetPrice(itemCode: String): Int = coroutineScope {
        val price1 = async { AStore.itemPrice(itemCode) }
        val price2 = async { XStore.itemPrice(itemCode) }
        min(price1.await(), price2.await())
    }
}

// メイン処理
fun runMain(): Job = scope.launch {
    try {
        val bestPrice = CheapestStore.itemPrice("4901170017583")
        println("⭐️⭐️最安価格: $bestPrice⭐️⭐️")
    } catch (e: Exception) {
        println("??取得に失敗しました: $e??")
    }
}

▶️ 実行してみる

実行結果

 0.213: CheapestStore: 「4901170017583」の価格を取得します
 0.236: XStore: 「4901170017583」の価格を取得します
 0.236: AStore: 「4901170017583」の価格を取得します
 0.236: XStore: 「4901170017583」の価格の取得が失敗しました: (java.lang.IllegalStateException: サーバーダウン)
 0.299: AStore: 「4901170017583」の価格の取得がキャンセルされました
 0.302: CheapestStore: 「4901170017583」の価格の取得が失敗しました: (java.lang.IllegalStateException: サーバーダウン)
??取得に失敗しました: java.lang.IllegalStateException: サーバーダウン??

前回と変わらず無関係な AStore にキャンセルが発行されますが、runMain() 自体に CancellationException が発生する事はなく、CheapestStore.itemPrice()XStore が投げた例外になります。

coroutineScope のスコープの中にある coroutine でキャッチされない例外が発生した場合、coroutineScope はその例外を投げます。この際、先ほどと同様にスコープ内の他の処理をキャンセルし、完了するのを待ちます。

coroutineScope のこの挙動のため、例外によるキャンセル伝播は suspend 関数のスコープがキャンセルされるだけで、呼び出し元の coroutine までキャンセルされたりはしません

なお coroutineScope はただの suspend 関数なので、launch { … } の中でも呼び出す事が可能です。例外が発生した時のキャンセル伝播を途中で止めてリカバリしたい時などは coroutineScope を使うと良いでしょう。

まとめ

ここまで Kotlin の coroutine で並列実行をする際に重要となる structured concurrency と、例外が起きた時の動作を解説しました。structured concurrency は Kotlin の coroutine において重要な考えであり、また個人的には非常に面白い機能だと思います。しかしそれなりに複雑ではあるので、慣れるまでは入れ子の async などを使わないというのも一つの手だと個人的には思います。

次回は今まであまり話して来なかった、viewModelScopeGlobalScope のようなトップレベルの CoroutineScope を作る方法について解説しようと思っています。


  1. 公式ドキュメントにも非推奨と書かれてあります。 

  2. runBlocking { … } といった、呼び出し元をブロックする coroutine builder は CoroutineScope が不要です。 

  3. Exception handling with structured concurrency (rx & async) · Issue #691 · Kotlin/kotlinx.coroutines 

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む

Androidのカスタムビューに2way-DataBindingを設定する

BindingAdapterのソースコード

@BindingAdapter("frame")
fun Ocha.setFrame(frame: Int) {
    setFrame(frame)
}

@InverseBindingAdapter(attribute = "frame")
fun Ocha.getFrame() = getFrame()

@BindingAdapter("frameAttrChanged")
fun Ocha.setListener(listener: InverseBindingListener?) {
    findViewById<SeekBar>(R.id.seekBar).setOnSeekBarChangeListener(object : SeekBar.OnSeekBarChangeListener {
        override fun onProgressChanged(seekBar: SeekBar?, progress: Int, fromUser: Boolean) {
            findViewById<LottieAnimationView>(R.id.animationView).frame = progress
            listener?.onChange()
        }
        override fun onStartTrackingTouch(seekBar: SeekBar?) {}
        override fun onStopTrackingTouch(seekBar: SeekBar?) {}
    })

}
  • @BindingAdapterはViewModel->ロジックに値を渡すもの
  • @InverseBindingAdapterはロジック->ViewModelに値を渡すもの
  • @BindingAdapter("frameAttrChanged")は公式を見てください(動いただけで、確認してない)
    • InverseBindingAdapterのattribute名+AttrChangedの名前にします。(eventを設定してるならそっちのはず)

LayoutXml

app:frame="@={viewModel.frame}"

@=で双方向データバインディング

  • このエントリーをはてなブックマークに追加
  • Qiitaで続きを読む