- 投稿日:2020-03-23T20:42:25+09:00
どこでも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にする必要があるかどうか疑った方が良いかもしれません。
- 投稿日:2020-03-23T20:01:08+09:00
Kotlinで他のところタップでキーボードを引っ込める
キーボードがいい感じに引っ込んでくれない
これだとログインボタンを押すことができません...
他の領域にクリックイベントをつけることで解決する
<?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()
を呼び出しています!これでキーボードが引っ込むようになりました。
参考リンク
- 投稿日:2020-03-23T16:38:01+09:00
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.dartimport '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, ); } }結果
- 投稿日:2020-03-23T16:06:11+09:00
【エラー】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
というエラーが発生しました。
解決方法を記載します。解決方法
ionic cordova plugin add cordova-plugin-androidx
を実行ionic cordova plugin add cordova-plugin-androidx-adapter
を実行cordova-plugin-androidxとは
CordovaプロジェクトでAndroidサポートライブラリの後継であるAndroidXを有効にします。
https://github.com/dpa99c/cordova-plugin-androidxcordova-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
- 投稿日:2020-03-23T10:53:04+09:00
Flutter gallery_saver Androidのみ保存でエラーになる問題の解決
gallery_saver は、写真・画像をカメラロール、ギャラリーに保存するライブラリ。
※ 使用バージョン gallery_saver 1.0.7Androidのみ、同一ファイル名で保存をかけた場合、エラーとなる問題がある。
発生するエラー。
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
- 投稿日:2020-03-23T10:53:04+09:00
Flutter gallery_saver Androidのみ保存でエラーになる問題を解決
gallery_saver は、写真・画像をカメラロール、ギャラリーに保存するライブラリ。
※ 使用バージョン gallery_saver 1.0.7Androidのみ、同一ファイル名で保存をかけた場合、エラーとなる問題がある。
エラーが発生しクラッシュしてしまう。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
- 投稿日:2020-03-23T09:32:11+09:00
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
実際に
AStore
とBStore
の価格を比較し、最安値を計算してみましょう。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秒で両方取得する事ができるはずです。
async
とawait
を使う事で並列実行が可能になります。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
launch
やasync
といった、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 が指定されます。そのためレシーバーを指定しないでlaunch
やasync
を呼び出すと現在のスコープ (=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) が全て終了するまで外側のスコープは終了しない
- 外側のスコープに対してキャンセルが要求されれば、内側のスコープに対してもキャンセルが伝播する
逆に言えば、「内側のスコープが動いているのに外側の処理が終了している」といった事は起こらないようになっています。
実際に使用する際は、あえてこの階層化を超えて実行したい処理(ログや通知など)は
viewModelScope
やGlobalScope
のような階層外にある CoroutineScope を使い、それ以外の場合は現在のスコープを使うのが一つの良い指針と言えると思います。structured concurrecy に関するさらに詳しい話に興味がある方は、以下が参考になると思います。
- Structured concurrency - Roman Elizarov - Medium
- Notes on structured concurrency, or: Go statement considered harmful
Structured Concurrency と suspend 関数
さて、suspend 関数から coroutine builder を呼び出す事を考えてみます。
AStore
とBStore
の安い方の価格で商品を扱う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 { … }
はlaunch
やasync
と違い、新しく 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 の例外をハンドリングする方法
しかし例外を全くハンドリングできないというのでは困ってしまいます。代表的な対応策はいくつかあります。
- 入れ子で coroutine を使わない
coroutineScope { … }
を使うCoroutineExceptionHandler
やSupervisorJob
を使う一つ目は特に特別な仕組みを知らずともできる方法です。並列実行して高速化したい理由が特にない場合に推奨される方法と言えるでしょう。
今回は二つ目の方法を紹介します。三つ目の方法は今回は説明しません。
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
などを使わないというのも一つの手だと個人的には思います。次回は今まであまり話して来なかった、
viewModelScope
やGlobalScope
のようなトップレベルの CoroutineScope を作る方法について解説しようと思っています。
runBlocking { … }
といった、呼び出し元をブロックする coroutine builder は CoroutineScope が不要です。 ↩Exception handling with structured concurrency (rx & async) · Issue #691 · Kotlin/kotlinx.coroutines ↩
- 投稿日:2020-03-23T07:45:25+09:00
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}"@=で双方向データバインディング