Kotlin Coroutinesパターン&アンチパターン
Kotlin Coroutinesパターン&アンチパターン
Coroutineを理解するため、以下のサイトを日本語訳しました。 https://proandroiddev.com/kotlin-coroutines-patterns-anti-patterns-f9d12984c68e
目次
- 非同期呼び出しをcoroutineScopeでラップするか、SupervisorJobを使用して例外を処理します。
- ルートコルーチンのメインディスパッチャを優先する
- 不要なasync / awaitの使用を避ける
- スコープジョブをキャンセルしない
- 暗黙のディスパッチャを使って中断関数を書かないようにする
- グローバルスコープの使用を避ける
非同期呼び出しをcoroutineScopeでラップするか、SupervisorJobを使用して例外を処理します
非同期ブロックが例外をスローする可能性がある場合は、try / catchブロックでラップすることに頼らないでください。
val job: Job = Job() val scope = CoroutineScope(Dispatchers.Default + job) // may throw Exception fun doWork(): Deferred<String> = scope.async { ... } // (1) fun loadData() = scope.launch { try { doWork().await() // (2) } catch (e: Exception) { ... } }
記の例では、doWork関数はnew coroutine(1)を起動し、これが未処理の例外を投げます。 doWorkをtry / catchブロック(2)でラップしようとしても、まだクラッシュします。
クラッシュを回避する方法の1つは、SupervisorJob(1)を使用することです。
子の失敗またはキャンセルは、SupervisorJの仕事を失敗させることはなく、他の子にも影響しません。
val job = SupervisorJob() val scope = CoroutineScope(Dispatchders.Default + job) // 例外をスローする可能性があります fun doWork(): Defferd<String> = scope.async { ... } fun loadData() = scope.launch { try { doWork().await() } catch (e: Exception){ ... } }
ノート:SupervisorJobを使用してCoroutineScopeで非同期を明示的に実行した場合にのみ機能します。 そのため、以下のコードはasyncがparent coroutine(1) の範囲内で起動されるため、アプリケーションをクラッシュさせます。
val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default job) fun loadData() = scope.launch { try { async { // (1) // 例外をスローする可能性があります } } catch (e: Exception) { ... } }
クラッシュを避けるためのもう1つの方法は、coroutineScope(1)を使ってasyncをラップすることです。 async内部で例外が発生すると、外部スコープに触れることなく、このスコープ内に作成された他のすべてのコルーチンをキャンセルします。 (2)
val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) // 例外をスローする可能性あり fun doWork(): Deferred<String> = coroutineScope { // (1) async { .... } } fun loadData() = scope.launch { // (2) try { doWork().await() } catch (e: Exception) { ... } }
あるいは、非同期ブロック内で例外を処理することもできます。
ルートコルーチンのメインディスパッチャを優先する
バックグラウンドで作業を行い、ルートコルーチン内でUIを更新する必要がある場合は、メイン以外のディスパッチャを使用して起動しないでください。
val scope = CoroutineScope(Dispatchers.Default) // (1) fun login() = scope.launch { withContext(Dispatcher.Main) { view.showLoading() } // (2) networkClient.login(...) withContext(Dispatcher.Main) { view.hideLoading() } // (2) }
記の例では、Default dispatcher(1)のスコープを使用してルートコルーチンを起動します。 このアプローチでは、ユーザーインターフェイスに触れる必要があるたびに、コンテキストを切り替える必要があります(2)。
ほとんどの場合、メインディスパッチャを使用してスコープを作成することをお勧めします。これにより、コードが単純になり、コンテキストの切り替えが明確になります。
val scope = CoroutineScope(Dispatchers.Main) fun login() = scope.launch { view.showLoading() withContext(Dispatcher.IO) { networkClient.login(...) } view.hideLoading() }
不要なasync / awaitの使用を避ける
async機能を使用していてすぐに待つのであれば、これをやめるべきです
launch { val data = async(Dispatchers.Default) { /* code */ }.await() }
コルーチンコンテキストを切り替えてすぐに親コルーチンを中断したい場合はwithContextを使うのが望ましい方法です。
launch { val data = withContext(Dispatchers.Default) { /* code */ } }
パフォーマンス面では大きな問題ではありませんが(asyncが新しいコルーチンを作成して作業を行うと考えられていても)、意味的にasyncはバックグラウンドでいくつかのコルーチンを起動して待機することを意味します。
スコープジョブをキャンセルしない
コルーチンをキャンセルする必要がある場合は、最初にスコープジョブをキャンセルしないでください。
class WorkManager { val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun doWork1() { scope.launch { /* do work */ } } fun doWork2() { scope.launch { /* do work */ } } fun cancelAllWork() { job.cancel() } } fun main() { val workManager = WorkManager() workManager.doWork1() workManager.doWork2() workManager.cancelAllWork() workManager.doWork1() // (1) }
上記のコードの問題は、jobをキャンセルすると、完了状態になることです。完了したジョブの範囲内で起動されたコルーチンは実行されません(1)。
特定のスコープのすべてのコルーチンをキャンセルしたい場合は、cancelChildren関数を使用できます。 また、個々の仕事をキャンセルする可能性を提供するのは良い習慣です(2)。
class WorkManager { val job = SupervisorJob() val scope = CoroutineScope(Dispatchers.Default + job) fun doWork1(): Job = scope.launch { /* do work */ } // (2) fun doWork2(): Job = scope.launch { /* do work */ } // (2) fun cancelAllWork() { scope.coroutineContext.cancelChildren() // (1) } } fun main() { val workManager = WorkManager() workManager.doWork1() workManager.doWork2() workManager.cancelAllWork() workManager.doWork1() }
暗黙のディスパッチャを使って中断関数を書かないようにする
特定のコルーチンディスパッチャからの実行に依存するサスペンド機能を書かないでください。
suspend fun login(): { view.showLoading() val result = withContext(Dispacher.IO) { someBlocikngCall() } view.hideLoading() return result }
上記の例では、login関数はサスペンド関数で、メイン以外のディスパッチャを使用するコルーチンから実行するとクラッシュします。
launch(Dispatcher.Main) { // (1) no crash val loginResult = login() ... } launch(Dispatcher.Default) { // (2) cause crash val loginResult = login() ... }
CalledFromWrongThreadException:そのビューに触れることができるのは、ビュー階層を作成した元のスレッドだけです。
任意のコルーチンディスパッチャから実行できるように中断関数を設計します。
suspend fun login(): Result = withContext(Dispatcher.Main) { view.showLoading() val result = withContext(Dispatcher.IO) { someBlockingCall() } view.hideLoading() return result }
これで、どのディスパッチャからもログイン関数を呼び出すことができます。
launch(Dispatcher.Main) { // (1) no crash val loginResult = login() ... } launch(Dispatcher.Default) { // (2) no crash ether val loginResult = login() ... }
グローバルスコープの使用を避ける
Androidアプリのいたる所でGlobalScopeを使っているのなら、これをやめるべきです。
GlobalScope.launch {
// code
}
グローバルスコープは、アプリケーションの有効期間全体にわたって動作し、時期尚早にキャンセルされない最上位レベルのコルーチンを起動するために使用されます。
アプリケーションコードは通常、アプリケーション定義のCoroutineScopeを使用する必要があります。GlobalScopeのインスタンスで非同期または起動を使用することは強くお勧めできません。
Androidでは、コルーチンはActivity、Fragment、ViewまたはViewModelライフサイクルに簡単にスコープ設定できます。
class MainActivity : AppCompatActivity(), CoroutineScope { private val job = SupervisorJob() override val coroutineContext: CoroutineContext get() = Dispatchers.Main + job override fun onDestroy() { super.onDestroy() coroutineContext.cancelChildren() } fun loadData() = launch { // code } }
オランダは残業をしない
会社の同僚のオランダ人からこの動画を紹介された。
52秒ほどの短い動画だが、オランダの労働環境の良さについて紹介されている。
その中で出てくる言葉を書き出して日本語訳してみた。
Only 1% of Dutch men work more than 50 hours a week 週50時間以上仕事をしているオランダ人男性はわずか1%です And the number of women working very long hours is too small to measure そして、非常に長い時間働く女性の数は測定できないほど少ない The Dutch spend almost 16 hours a week on leisure and personal care オランダ人は、レジャーとパーソナルケア(体の手入れ)に週に16時間を費やしています。 Just 0.5% of Dutch people spend longer at work than they should オランダの人々のうち、定期的に非常に長い時間を働くのはわずか0.5% The average in OECD countries is 13% OECD諸国の長い時間働く平均は13%です ※OECD:経済協力開発機構、先進国35か国が参加(日本も加盟) Working long hours is damaging to health and wellbeing 長時間労働は健康と幸福に害を与えます More leisure time can improve physical and mental health 余暇時間を長くすると、肉体的および精神的健康が向上する The fact the Dutch spend time looking after themselves may account for the longer life expectancy オランダ人が自分たちの世話をするのに時間を費やすという事実は、より長い平均余命を説明するかもしれない The average Dutch person lives 82 years オランダの平均寿命は82歳 That's years longer than the OECD average of 80 OECD諸国の平均は80歳です Could you be healthier by going Dutch? あなたはオランダ語に行くことによって健康になりますか?
すごいな、オランダ
日本とはえらい違いだ。
いったいどうやったらこんな事が可能なんだろうか。
ちょっと調べてみるか
balena DashとRaspberryPi3+モニタでWebフレームを作った
balena Dashとは
完成物
作成手順
注意点
初回起動時にbalenaDashにエラーログが延々と流れていてディスプレイに再生が始まらなかった。
原因はGPUメモリの割り当てが初期値だと足りなかった。
Define device GPU memory in megabytes.
を396
に指定すると、エラーログも出なくなりディスプレイへの再生が始まった。
備考
Youtubeを流す場合、https://www.yout-ube.com/ を使う
CO2を測定できる機器orセンサーを調査
やりたい事
室内の室温、湿度、二酸化炭素濃度を計ってグラフ化したい
できればセンサー単体で値をpostしてくれるモノがベスト
そういうデバイスがないか調べてみた
NETATMO(ネタトモ) ウェザーステーション
- 価格: ¥ 18,315
- 気温、湿度、気圧、騒音、二酸化炭素濃度の測定が可能
- Wi-Fi:802.11 b/g/n互換
- よく記事を見かける
- 追加モジュールがあるらしい http://amzn.asia/d/jjY3tSh
最有力候補だが、値段が高い
カスタム (CUSTOM) CO2モニター CO2-mini
- 価格: ¥ 9,720
- 測定項目:CO2、温度(湿度がない)
- 電源:USB(ケーブル付属。要USBポート)
- Wifi接続できず
ラズパイ等に接続してpythonでセンサーを送ってた、wifiが無いのが惜しい
SMARTMIEW スマートミュー
二酸化炭素は計れないので惜しい
番外編:センサー
空気品質を測定し、記録する - AmbientでIoTをはじめよう http://pages.switch-science.com/letsiot/airquality/
一番安くてカスタマイズ性も最強だが、今回はこういう電子工作っぽいのはあまりやりたくない。。。
Android開発の最新バージョンリンク集 (Android Studio, Support Library, buildToolsVersion, etc...)
開発に必要な最新バージョンを知るための情報リンクをメモとして載せておく。
Android Studio
Android Studio Release Updatesfds https://androidstudio.googleblog.com/
Support Library
Recent Support Library Revisions | Android Developers https://developer.android.com/topic/libraries/support-library/revisions
AndroidX
AndroidX release notes | Android Developers https://developer.android.com/topic/libraries/support-library/androidx-rn?hl=ja
Play Service
Release Notes | Google APIs for Android | Google Developers https://developers.google.com/android/guides/releases
Build Tools Version
SDK Build Tools release notes | Android Developers https://developer.android.com/studio/releases/build-tools
app/build.gradleで使うやつ
android { compileSdkVersion 27 buildToolsVersion '27.0.3' // <-これ
Android Gradle plugin
Android Gradle plugin release notes | Android Developers https://developer.android.com/studio/releases/gradle-plugin
build.gradleで使うやつ
buildscript { ... dependencies { classpath 'com.android.tools.build:gradle:3.2.0' // <-これ
Android Studioで保存すると自動でコード整形したい
コードを書いて「コード整形+インポート最適化+ファイル保存」 ←この作業を1アクションで済ませたい。
やり方は2パターンある
- マクロを使う方法
- プラグインを使う方法
マクロを使う方法
以下の動作をマクロとして記憶させて⌘S(もしくはctrl+s)に割り当てる。 - Code => Optimization Imports - Code => Reformat Code - File => Save All
以下の記事に詳しく紹介されている。
Android Studioでファイル保存時にコードを最適化したい - Qiita https://qiita.com/chocomelon/items/46810763a5be0a433158
マクロのメリット
- やる事がシンプル
- カスタマイズ可能
- マクロの有効/無効が簡単、Android Studioを再起動せずに済む
- Android Studio起動時にPlugin読み込み数が減って起動が早くなるかも?
デメリット
- 機能はプラグインに及ばない
- 新たにAndroid Studioをインストールして設定するとき、自動整形マクロを作成/設定方法を毎回忘れる
- (マクロファイルをインポートすればいいだけだが、それすらも面倒だと思う時がある)
プラグインを使う方法
Save Actions というプラグインをインストールする
Save Actions - Plugins | JetBrains https://plugins.jetbrains.com/plugin/7642-save-actions
いい紹介記事が見当たらなかったのでスクショを貼っておこう
インストール
Preference => Plugins => Install JetBrains Plugin...
からSave Actionsを検索してインストールする。
インストール完了したらAndroid StudioをRestartさせる。
Save Actionsを有効にするには以下の画像のような設定にする。
これで保存時に自動でコード整形+インポート整理されるはず。
プラグインのメリット
- インストールが簡単で楽
- マクロにはできない事が多々ある(使ってないけど)
- 変更した箇所だけコード整形とか
- finalを自動で変数に追加
デメリット
- プラグインだらけにするとAndroid Studioが動作が遅くなるかも
- プラグインをUpdateするとAndroid Studioの再起動を求められる
まとめ
両方試してみて自分に合う方を使用すればよい
ちなみに、自分はマクロ派
TransitionDrawableでFragmentをクロスフェード遷移させる
TransitionDrawableでFragmentをクロスフェードさせながら切り替えてみた。
色合いはさておき、なかなかカッコいい
使い所としては、アプリの紹介やチュートリアル画面あたりだろうか。
実装方法
まずdrawable内にtransition
タグのリソースxmlを作成する
<?xml version="1.0" encoding="utf-8"?> <transition xmlns:android="http://schemas.android.com/apk/res/android"> <item android:drawable="@color/first" /> <item android:drawable="@color/second" /> </transition>
このtrans.xmlを置き換えるレイアウトの背景に指定する。
<FrameLayout android:id="@+id/main_container" android:layout_width="match_parent" android:layout_height="match_parent" + android:background="@drawable/trans" />
これで準備OK
あとはstartTransition
にクロスフェードする時間を引数に渡して呼び出すだけ
class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { ... fab.setOnClickListener { + val transition = main_container.background as TransitionDrawable + transition.startTransition(1000) //1秒かけてクロスフェード開始 + supportFragmentManager + .beginTransaction() + .replace(R.id.main_container, SecondFragment()) + .commit() } }
これで@color/first
から@color/second
のクロスフェードが始まるので、その間にFirstFragment
からSecondFragment
へ切り替える。
切り替え後、戻す時はreverseTransition
を使えば逆のクロスフェードになる。
transition.reverseTransition(1000)
override fun onBackPressed() { + val transition = main_container.background as TransitionDrawable + transition.reverseTransition(1000) + supportFragmentManager + .beginTransaction() + .replace(R.id.main_container, FirstFragment()) + .commit() }
たったこれだけ、とても簡単
公式ドキュメント
TransitionDrawable | Android Developers https://developer.android.com/reference/android/graphics/drawable/TransitionDrawable
実装コード
今回のコードは以下にありまっせ
banbara23/Android-Kotlin-Lab at TransitionDrawable https://github.com/banbara23/Android-Kotlin-Lab/tree/TransitionDrawable