- 投稿日:2019-07-07T18:18:31+09:00
StringからDateに変換するときにException吐いてたのをどうにかした話
はじめに
Android Studioでテストコード走らせているときは問題なかったのに、端末にインストールして動かしたら動かなかった。
対応(修正前)
タイムゾーンも入れていて、Patternを指定していたのが以下。
"yyyy/MM/dd'T'HH:mm:ss.SSSX"
もともと"yyyy/MM/dd'T'HH:mm:ss.SSSZ"って指定していたのだけど、Zがダメっぽくて上記のように対応していた。
これはテストコードでは動いていた。対応(修正後)
最終形が以下。
"yyyy/MM/dd'T'MM:dd:ss.SSSZZZZZ"終わりに
増やしたらいけるとかよくわからないが、Androidのソース依存ぽい。
- 投稿日:2019-07-07T18:08:50+09:00
Android で CloudVision API を使ってウェブ検出する
Android で CloudVision API を使ってラベル検出する
の続きです。前回の記事では、ラベル検出を行った。
今回は、ウェブ検出を行う。下記を参考にした。
参考 : Android : Google Cloud Vision APIを用いてWEB_DETECTIONする
リクエストを作成する
基本的な処理は、ラベル検出と同じです。
違いは、機能のタイプにウェブ検出を指定すること。
Feature feature = new Feature(); // ウェブ検出を指定する feature.setType("WEB_DETECTION");ウェブ検出のレスポンス
ラベル検出では、取得できるアノテーションはラベルだけでした。
ウェブ検出では、下記のアノテーションが取得できる。
- ウェブから取得したラベル
- 一致する画像のあるサイト URL
- リクエスト中の画像に部分一致または完全一致するウェブ画像を参照する URL
- 視覚的に類似している画像を参照する URL
WebDetection
ウェブ検出のレスポンスのクラス
AnnotateImageResponse response WebDetection webDetection = response.getWebDetection(); // 最も適切に推測したラベル List<WebLabel> bestGuessLabels = webDetection.getBestGuessLabels(); // ウェブから取得したラベル List<WebEntity> webEntities = webDetection.getWebEntities(); // 一致する画像のあるサイト List<WebPage> pagesWithMatchingImages = webDetection.getPagesWithMatchingImages(); // 完全一致する画像 List<WebImage> fullMatchingImages = webDetection.getFullMatchingImages(); // 部分一致する画像 List<WebImage> partialMatchingImages = webDetection.getPartialMatchingImages(); // 類似している画像 List<WebImage> visuallySimilarImages = webDetection.getVisuallySimilarImages();WebLabel
ウェブラベルのメタデータ
ラベルが取得できるWebEntity
ウェブエンティティのメタデータ
ラベルとスコアが取得できるWebPage
ウェブページのメタデータ
- 一致する画像のあるサイトのURLと、
- 一致する画像のURLが取得できるWebImage
オンライン画像のメタデータ
- 一致する画像のURLが取得できるレスポンスの処理
- 一致する画像のあるサイトのURLから ウェブブラウザアプリでそのサイトを表示する。
- 一致する画像のURLから その画像を表示する。
実行例
テキスト
I found these things:
bestGuessLabels:
google cloud vision api label direction
webEntities:
Google Cloud Platform: 0.825
Application programming interface: 0.703
American Cocker Spaniel: 0.569
Cloud computing: 0.567
Google: 0.526
Application software: 0.460
Google APIs: 0.391
Library: 0.378
pagesWithMatchingImages: 10
fullMatchingImages: 4
partialMatchingImages: 3
visuallySimilarImages: 0レスポンス
写真に写っているのは、アメリカンコッカースパニエル。
推測したラベルは、
google cloud vision api label direction 。
一致する画像のあるサイトは、
https://cloud.google.com/vision/docs/detecting-labels
一致する画像は、
https://cloud.google.com/vision/docs/images/faulkner.jpgサンプルコードをgithub に公開した。
https://github.com/ohwada/Android_Samples/tree/master/CloudVision2
- 投稿日:2019-07-07T17:23:48+09:00
DarkMode実装する際に便利なAndroidStudioの機能
DarkModeを実装する際に便利なチップスを紹介します。
ダークモードのデザイン確認
DarkModeは、以下のようにリソース末尾に-nightを付与することで昼間向けテーマと夜間向けテーマを設定することができます。
例
res/values/color.xml
res/values-night/color.xmlこれまで、上記のようにリソースをダークモード用と通常用に設定した後、
ダークモードのデザインを確認したいと思ったら、
- ビルドして
- ダークモードに切り替えて
- デザイン確認
という流れで、必ずビルドしないと確認できないと思っていました。
が、、、AndroidStudio上で簡単にダークモードのデザインも通常モードのデザインも確認できる便利な方法がありました。
サンブルとして以下のレイアウトでダークモードを実装しています。
このとき、以下の図のように、レイアウトの左上にある、Orientation For Previewのアイコンを選択します。
すると、そこにNight Modeというものがあります。
そこで、Night Modeを選択すると、ダークモードのデザインに一瞬で切り替わります。
簡単に切り替えられて、どちらのモードのデザインもAndroid Studio上ですぐに確認できるのがとても便利です。
これを知るまでは、ダークモードのデザインを確認するには、少しのカラー変更でも毎回ビルドして確認していたので、とても非効率でした
いつからこの機能があったのかわかりませんが、早く知りたかった・・・Android Studioは、便利な機能がたくさんあって、まだまだ知らない便利な機能がありそうです。
便利な機能は活用して、開発効率がどんどん上げていきたいなーと思います
- 投稿日:2019-07-07T13:49:57+09:00
Android(JAVA)でTensorFlowLiteを使って画像分類をやってみる
初めに
今回は、Android(Java)でTensorFlowLiteを使って、画像分類をしようと思います!
もし、コード等に間違え、改善点があれば、教えてください!TensorFlowLiteとは
TensorFlowLiteのガイドによると、、、
TensorFlowLiteはスマートフォンやIotデバイスなどでTensorFlowモデルを使用するためのツールセットです。
TensorFlow Liteのインタプリタ携帯電話、組み込みLinuxデバイス、およびマイクロコントローラを含む多くの異なるハードウェアの種類、に特別に最適化されたモデルを実行します。
TensorFlowライトコンバータインタプリタによって使用するための効率的な形式にTensorFlowモデルを変換し、バイナリサイズとパフォーマンスを向上させるために最適化を導入することができます。
⇒つまり、PCだけじゃなくて、スマートフォンやIotデバイスなどでも簡単に実行できる、TensorFlowの軽量版的なやつか!
将来的には、スマートフォンだけで学習までできるとか!
すごい!TensorFlowLiteを使用した開発手順
1.TensorFlowモデルを用意する
TensorFlowで学習済みのモデルを用意します。
今回は、ホストされたモデルを使用するので、割愛します!2.TensorFlowのモデルを変換する
TensorFlowLiteではTensorFlowのモデルをそのまま使用することができないので、専用の形式(tflite)に変換します。
変換方法等はこちらの記事がわかりやすいので、参考にしてください。3.組み込む!
今回は、Android(Java)の組み込み方法を解説します!
組み込む!
新規プロジェクトの作成
プロジェクト名等は任意の名前にしてください!
今回は、AndroidXを使用します。
「Use androidx.* artifacts」にチェックすれば、OKです。
AndroidXの使用については任意なので、使わなくても大丈夫です。依存関係の追加
appディレクトリ下のbuild.gradleに
build.gradle(app)dependencies { implementation 'org.tensorflow:tensorflow-lite:0.0.0-nightly' implementation 'org.tensorflow:tensorflow-lite-gpu:0.0.0-nightly' }を追加します。
このままだと、すべてのCPUと命令セット用のABIが含まれていますが、「armeab-v7a」と「arm64-v8a」が含まれていれば、ほとんどのAndroidデバイスをカバーできるので、ほかのABIは含めいないように設定します。
含まれていても問題はないですが、アプリのサイズが減るので、おすすめです。build.gradle(app)android { defaultConfig { ndk { abiFilters 'armeabi-v7a', 'arm64-v8a' } } }ABIについてはこちらの記事がわかりやすいので参考にしてください。
Androidではassetフォルダーに入れられたものを圧縮してしまうので、モデルをassetフォルダーに入れると、圧縮されて読み込むことができなくなってしまいます。そこで、tfliteファイルを圧縮しないように指定してあげます。
build.gradle(app)android { defaultConfig { aaptOptions { noCompress "tflite" } } }クラスのコピー&カスタマイズ
TensorFlowLiteのAndroidSampleの3つのクラスと、こちらのLogger.javaをコピーします。
コピーしただけだと、エラーが発生します。
Classifier.javaでLoggerクラスのインポート先を書き換えます。Classfier.javaimport org.tensorflow.lite.Interpreter; //ここを削除するimport org.tensorflow.lite.examples.classification.env.Logger; import org.tensorflow.lite.gpu.GpuDelegate; /** A classifier specialized to label images using TensorFlow Lite. */ public abstract class Classifier { private static final Logger LOGGER = new Logger();
削除すると、AndroidStudioがこんなことを聞いてくるので、「Alt+Enter」を押せば、自動でインポートしてくれます。
インポートする際に、
2種類出てくると思いますので、(android.jar)と書かれていないほうを選択します。
これで、エラーがすべて消えたと思います。
モデルの設置
モデルとlabel_textをassetフォルダーに設置します。
こちらよりモデルをダウンロードしてください。
まずは、assetフォルダーフォルダーを作成します。
解凍したフォルダの中から、ファイルをコピーします。
コピーしたら、名前を「model.tflite」と「labels.txt」に変更します。
これでモデルの設置は完了です。Viewの配置
こんな感じにTextView,Button,ImageViewを配置します。
ButtonにはonClickを設定押しておきます。
↑ onClickを設定する方法ってリスナーのほうがいいのかな。詳しい人おしえてくださいな
activity_main.xml<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <LinearLayout android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical"> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <TextView android:id="@+id/textView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:text="TextView" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <Button android:id="@+id/button" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" android:onClick="select" android:text="画像を選択" /> </LinearLayout> <LinearLayout android:layout_width="match_parent" android:layout_height="wrap_content" android:orientation="horizontal"> <ImageView android:id="@+id/imageView" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_weight="1" tools:srcCompat="@tools:sample/avatars" /> </LinearLayout> </LinearLayout> </androidx.constraintlayout.widget.ConstraintLayout>コードを書く
まず、使用する変数を宣言しましょう!
MainActivity.javaImageView imageView; TextView textView; Classifier classifier; private static final int RESULT_IMAGEFILE = 1001; //画像取得時に使用するリクエストコードonCreate内でtextview,ImageViewの紐づけを行います。
MainActivity.javaimageView = findViewById(R.id.imageView); textView = findViewById(R.id.textView);次に、Classfierの呼び出しを行います。
MainActivity.javatry { classifier = Classifier.create(this,QUANTIZED,CPU,2); } catch (IOException e) { e.printStackTrace(); }引数は、Acritivy、Modelの種類、演算に使用するデバイスの指定、使用するスレッド数を指定します。
基本はこの設定で動くと思いますが、臨機応変に変更しましょう。ボタンの動作を書く
ボタンを押したら、ギャラリーを開いて画像が選択できるようにIntentを飛ばします。
MainAcritivy.javapublic void image(View V){ Intent intent = new Intent(Intent.ACTION_OPEN_DOCUMENT); intent.addCategory(Intent.CATEGORY_OPENABLE); intent.setType("image/*"); startActivityForResult(intent, RESULT_IMAGEFILE); }これについて詳しくはこちら
ギャラリーから戻ってきてからの処理
ギャラリーから戻て来たら、画像を取得して、処理します。
MainAcritivty.java@Override public void onActivityResult(int requestCode, int resultCode, Intent resultData) { super.onActivityResult(requestCode, resultCode, resultData); if (requestCode == RESULT_IMAGEFILE && resultCode == Activity.RESULT_OK) { if (resultData.getData() != null) { ParcelFileDescriptor pfDescriptor = null; try { Uri uri = resultData.getData(); pfDescriptor = getContentResolver().openFileDescriptor(uri, "r"); if (pfDescriptor != null) { FileDescriptor fileDescriptor = pfDescriptor.getFileDescriptor(); Bitmap bmp = BitmapFactory.decodeFileDescriptor(fileDescriptor); pfDescriptor.close(); int height = bmp.getHeight(); int width = bmp.getWidth(); while (true) { int i = 2; if (width < 500 && height < 500) { break; } else { if (width > 500 || height > 500) { width = width / i; height = height / i; } else { break; } i++; } } Bitmap croppedBitmap = Bitmap.createScaledBitmap(bmp, width, height, false); imageView.setImageBitmap(croppedBitmap); List<Classifier.Recognition> results = classifier.recognizeImage(croppedBitmap,classfier); String text; for (Classifier.Recognition result : results) { RectF location = result.getLocation(); Float conf = result.getConfidence(); String title = result.getTitle(); text += title + "\n"; } textView.setText(text); } } catch (IOException e) { e.printStackTrace(); } finally { try { if (pfDescriptor != null) { pfDescriptor.close(); } } catch (Exception e) { e.printStackTrace(); } } } } }長いので、区切って説明します。
このコードは、アクティビティに戻ってきたときに呼ばれ、戻ってきたのが、ギャラリーからのものかを判定しています。
MainAcrivity.java@Override public void onActivityResult(int requestCode, int resultCode, Intent resultData) { super.onActivityResult(requestCode, resultCode, resultData); if (requestCode == RESULT_IMAGEFILE && resultCode == Activity.RESULT_OK) { } }このコードは、戻り値からURIを取得し、ParceFileDescriptorでファイルデータをとっています。
こんなURI「content://com.android.providers.media.documents/document/image%3A325268」が取得できるので、ここから画像を取得しています。MainAcrivity.javaif (resultData.getData() != null) { ParcelFileDescriptor pfDescriptor = null; try { Uri uri = resultData.getData(); pfDescriptor = getContentResolver().openFileDescriptor(uri, "r"); if (pfDescriptor != null) { FileDescriptor fileDescriptor = pfDescriptor.getFileDescriptor();このコードは先ほど取得した画像をbitmapに変換し、画像のサイズを300より小さくなるようにしています。
300よりでかい画像だと、正常に判定することができず、エラーで落ちてしまいます。
Caused by: java.lang.ArrayIndexOutOfBoundsException
そのため、縦横比を維持しつつ、縦横が300以内に収まるようにしています。MainAcrivity.javaBitmap bmp = BitmapFactory.decodeFileDescriptor(fileDescriptor); pfDescriptor.close(); if (!bmp.isMutable()) { bmp = bmp.copy(Bitmap.Config.ARGB_8888, true); } int height = bmp.getHeight(); int width = bmp.getWidth(); while (true) { int i = 2; if (width < 300 && height < 300) { break; } else { if (width > 300 || height > 300) { width = width / i; height = height / i; } else { break; } i++; } } Bitmap croppedBitmap = Bitmap.createScaledBitmap(bmp, width, height, false);いよいよ判別です。
このコードでは、加工した画像で判別をし、独自のリストで受け取っています。
そして、リストをforで回して、結果を取得し、textViewに表示させています。
今回は、判別された品目名のみ出力していますが、品目である可能性がどれくらいかなども取得することができます。MainAcrivity.javaList<Classifier.Recognition> results = classifier.recognizeImage(croppedBitmap); String text=""; for (Classifier.Recognition result : results) { /* RectF location = result.getLocation(); Float conf = result.getConfidence(); */ String title = result.getTitle(); text += title + "\n"; } textView.setText(text);以上で完成です!!
実際に動かしてみる
それでは、実際に動かしてみたいと思います!
まずは、犬の画像
公園のベンチ、、
爪、、、
アメリカンカメレオン、、、
んー
精度は微妙ですね
次は、美しい景色の画像。
デルフトの街並みですウィンドウスクリーン、、、
ドアマット、、、
ブラインド、、、
んー
ダメやん!まとめ
精度は微妙でしたが、うまく?画像を分類することができました!
今度は、リアルタイムで分類をしてみたいと思います!
ではでは!
- 投稿日:2019-07-07T13:01:47+09:00
はじめてのWorkManager
WorkManagerの使い方と得られた知見を忘れないようにまとめました。
WorkManagerの導入を検討している方などに参考になると嬉しいです。Versionは、2019/6/27にリリースされている現時点で最新の2.1.0-rc01です。
https://developer.android.com/jetpack/androidx/releases/work準備
まず、WorkManagerを使えるようにするために、AndroidプロジェクトにWorkManagerライブラリを追加します。
※WorkManagerには、compileSdkバージョン28以上が必要です。build.gradleimplementation 'androidx.work:work-runtime:2.1.0-rc01' implementation 'androidx.work:work-testing:2.1.0-rc01' implementation 'androidx.work:work-runtime-ktx:2.1.0-rc01'WorkManagerについて
WorkManagerは、Background作業を行いたい際に使用されるライブラリです。
内部的に端末のAPIレベルに合わせて、ライブラリを適切に選択してくれるAPIです。
- 選択されるライブラリ
- API 23+にJobScheduler
- API 14-22用のカスタムAlarmManager + BroadcastReceiver
なので、同じWorkManagerを使用していても、APIレベルによって内部的な処理は異なります。
そのほか詳細はドキュメントを読んでください。公式ドキュメントは以下です。
https://developer.android.com/topic/libraries/architecture/workmanager/basics#conceptsWorkManagerの基本的な構成
Worker
- このクラスを継承したクラスを作成する
- そして、そこに非同期で実行したい内容を実装する
WorkRequest
- 自動生成された一意のIDをもつ
- このIDからキューに入れたタスクをキャンセルしたり、タスクの状態を見れる
- OneTimeWorkRequestまたはPeriodicWorkRequestを使用する
- OneTimeWorkRequest
- Workerを1度だけ実行したいときに使用する
- PeriodicWorkRequest
- Workerを繰り返し、定期実行したいときに使用する
- WorkRequestオブジェクトを作成するためのヘルパークラスがあり
- ヘルパークラスは、WorkRequest.Builder、PeriodicWorkRequest.Builderの2つある
WorkManager
- 制約(時間指定、WiFi接続時、電池が十分にあるときとか)が満たされた後に実行する作業をエンキューする
- 作業はバックグラウンドスレッドで実行される
WorkInfo
- タスクに関する情報
- LiveDataでタスクの状態の観察、完了後の戻り値を取得できる
実際に使ってみる
OneTimeWorkRequestを使う
では実際にOneTimeWorkRequestを使ってみます。
今回は、画面にボタンが1つあり、そのボタンをタップするとWorkerがすぐに実行される簡単なサンプルを作ってみました。
はじめに、レイアウトです。
レイアウトはボタンが1つあるだけのレイアウトを作成します。activity_main.xml<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/workerButton" android:layout_height="wrap_content" android:layout_width="wrap_content" android:text="Workerのテスト" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"> </Button> </androidx.constraintlayout.widget.ConstraintLayout>次に、Workerを継承したMyWorkerクラスを作成します。
doWork()の中にWorkerが起動したときに実行したい処理を書きます。
今回の場合は、Logを仕込んでいます。MyWorker.ktclass MyWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) { companion object { const val MY_WORKER = "com.example.workmanager.MyWorker" const val WORKER_NAME = "MyWorker" } override fun doWork(): Result { Log.d("hanakoLog", "doWork!") return Result.success() } }最後に、MainActivityです。
ボタンがタップされたタイミングで、OneTimeWorkRequestをWorkManagerにセットします。MainActivity.ktimport android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import androidx.work.* import com.example.workmanager.MyWorker.Companion.MY_WORKER import kotlinx.android.synthetic.main.activity_main.* import java.util.concurrent.TimeUnit class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) WorkManager.initialize(this, Configuration.Builder().build()) workerButton.setOnClickListener { val workManager = WorkManager.getInstance(applicationContext) val request = OneTimeWorkRequest .Builder(MyWorker::class.java) .addTag(MY_WORKER) .build() workManager.enqueueUniqueWork(MyWorker.WORKER_NAME, ExistingWorkPolicy.REPLACE, request) workManager.getWorkInfoByIdLiveData(request.id).observe(this, Observer { workInfo -> Log.d("hanakoLog", workInfo.state.name) Log.d("hanakoLog", workInfo.id.toString()) Log.d("hanakoLog", workInfo.tags.toString()) }) } } }※ここでデバッグする際のチップス
workManager.getWorkInfoByIdLiveData(request.id)をobserveすることで、リクエストのstatusなどを確認できます。今回は、リクエストのstatus、id、タグをLogで確認します。Workerの動作を確認する際にこれは便利です。以上で実装完了です!
では、Logを確認してみましょう。2019-07-03 22:47:05.912 12670-12670/com.example.workmanager D/hanakoLog: ENQUEUED 2019-07-03 22:47:05.914 12670-12670/com.example.workmanager D/hanakoLog: aa67594e-b05e-4c07-9e48-391948279864 2019-07-03 22:47:05.916 12670-12670/com.example.workmanager D/hanakoLog: [com.example.workmanager.MyWorker] 2019-07-03 22:47:05.963 12670-13502/com.example.workmanager D/hanakoLog: doWork! 2019-07-03 22:47:06.013 12670-12670/com.example.workmanager D/hanakoLog: SUCCEEDED 2019-07-03 22:47:06.014 12670-12670/com.example.workmanager D/hanakoLog: aa67594e-b05e-4c07-9e48-391948279864 2019-07-03 22:47:06.014 12670-12670/com.example.workmanager D/hanakoLog: [com.example.workmanager.MyWorker]Logから、
- ボタンをタップすると
- MyWorkerの
- a67594e-b05e-4c07-9e48-391948279864というidのリクエストが
- ENQUEUEDになり
- doWork()が実行され
- SUCCEEDEDされている
様子がわかります。
OneTimeWorkRequestは、PeriodicWorkRequestに比べて動きがシンプルで動作の確認もしやすいです。
ちなみにdoWork()が実行されるまでに遅延時間を指定することなども可能です。
詳しくはドキュメントを読んでみてください!
https://developer.android.com/reference/androidx/work/OneTimeWorkRequestPeriodicWorkRequestを使う
次に、定期的にdoWork()を実行するPeriodicWorkRequestを実際にやってみます。
今回は、15分に1回Workerが動作するようにしたいと思います。レイアウトとMyWorkerクラスはOneTimeWorkRequestのときと一緒です。MainActivityのみを以下のように修正します。
MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) WorkManager.initialize(this, Configuration.Builder().build()) workerButton.setOnClickListener { val workManager = WorkManager.getInstance(applicationContext) val request = PeriodicWorkRequest .Builder(MyWorker::class.java, PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS, PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS) .addTag(MY_WORKER) .build() workManager.enqueueUniquePeriodicWork(MyWorker.WORKER_NAME, ExistingPeriodicWorkPolicy.REPLACE, request) workManager.getWorkInfoByIdLiveData(request.id).observe(this, Observer { workInfo -> Log.d("hanakoLog", workInfo.state.name) Log.d("hanakoLog", workInfo.id.toString()) Log.d("hanakoLog", workInfo.tags.toString()) }) } } }PeriodicWorkRequest.Builder()の中では、以下の3つを指定しています。
実行するWorkerクラス
繰り返す周期(repeat interval)
- Workerを実行したい間隔を指定します。今回は15分に1回です。デフォルト値はMIN_PERIODIC_INTERVAL_MILLIS(15分)でそれより短い時間を指定しても、15分になります。
jobが実行される最小駆動時間(flex interval)
- WorkManagerは、repeat intervalで例え1時間に指定しても、きっちり1時間で実行されるわけではありません
1時間ごとにここで指定するflex intervalの範囲内で実行されます。 (とはいえ、そこもきっちり保証されるわけではなく、あくまで目安と思っておいた方がよいです) デフォルト値はMIN_PERIODIC_FLEX_MILLIS(5分)でそれより短い時間を指定しても、5分になります。 今回はデフォルトの値を設定しているので、1時間間隔で5分以内にリクエストするコードになっています。
以上で実装完了です!
では、Logを確認してみましょう。2019-07-07 12:29:56.284 18571-18571/com.example.workmanager D/hanakoLog: ENQUEUED 2019-07-07 12:29:56.293 18571-18571/com.example.workmanager D/hanakoLog: cabde0e3-3a17-47ff-9ee6-a4c833cfcd8a 2019-07-07 12:29:56.297 18571-18571/com.example.workmanager D/hanakoLog: [com.example.workmanager.MyWorker] 2019-07-07 12:45:56.100 18571-18571/com.example.workmanager D/hanakoLog: doWork! 2019-07-07 13:03:50.254 18571-18571/com.example.workmanager D/hanakoLog: doWork! 2019-07-07 13:19:82.124 18571-18571/com.example.workmanager D/hanakoLog: doWork!Logから
- ボタンをタップすると
- MyWorkerの
- a67594e-b05e-4c07-9e48-391948279864というidのリクエストが
- ENQUEUEDになり
- その約15分後
- doWork()が実行され
- またその約15分後
- doWork()が実行され
- またその約15分後
- doWork()が実行され・・・
と、繰り返しWorker様子が動いている様子がわかります。
flex intervalintervalの説明で書きましたが、指定した15分できっちり時間通りには動いていないことが見てわかりますね。
なので、この時間に絶対送りたい!という要件にはWorkManagerのPeriodicWorkRequestは向いていないと思います。概ねx時間、x分位の間隔で定期実行させたいな~っという場合に使うとよいと思います。知見
知見①
OneTimeWorkRequestには、ENQUEUED、SUCCEEDED、FAILED、RETRY、CANCELEDステータスがあります。
OneTimeWorkRequestの実行を確認する際は、workInfo.state.namenameでどのステータスにいるのかを確認できます。一方、PeriodicWorkRequestには、SUCCEEDED、FAILED、RETRYステータスはありません。
基本ENQUEUEDのみで、明示的にキャンセルするとCANCELEDになります。なので、PeriodicWorkRequestの実行を確認する際、SUCCEEDED、FAILED、RETRYステータスステータスは取れないことに注意が必要です。知見②
RETRYの挙動は複雑で要注意。
BackoffPolicy
- デフォルトでBackoffPolicy.EXPONENTIALという設定がされているようですが、これは、リトライ間隔を指数関数的に増加させていく設定です。大抵の場合、このデフォルト設定で問題はないですが、リトライが繰り返された場合、最終的に数日後などにリトライされる可能性があることを理解しておきましょう。
リトライ回数
- リトライ回数の最大値を設定することはできません。回数制限したい場合は、Workerクラス内でgetRunAttemptCountを呼ぶと、現在の試行回数を取得可能なので、これを使用して、任意の回数に達したら、FailureさせてRetlyを防ぐとよいそうです。ちなみにgetRunAttemptCountは、一度もリトライされずに問題なく実行されている場合は、0です。
知見③
1.0.0alphaから2.1.0-rc01にあげたことでことで、変わったことがありました。
それは、WorkManagerのテストコードです。公式ドキュメントに記載されている通り、2.1.0からTestWorkerBuilderとTestListenableWorkerBuilderが提供されて、テストコードが書きやすくなっていました!
以下、公式ドキュメントの引用です。WorkManager 2.1.0 provides new TestWorkerBuilder and TestListenableWorkerBuilder classes, which let you test the business logic in your workers without having to initialize WorkManager with WorkManagerTestInitHelper.
詳細については以下をご覧ください。
https://developer.android.com/topic/libraries/architecture/workmanagerWorkManagerのテストについては、改めて書きたいと思います。
おわりに
workManagerの動作を実際に確認することができました。
非同期処理なので動作確認やテストが少しやりにくい所もありますが、versionが上がるたびに少しずつバグFIXされ、テストもしやすいようになってきています。
ぜひ、まだ1.0.0alphaなど使用している場合は2.1.0rc-01にバージョンを上げるのがおススメです(WorkManager関連の内部的なバグでクラッシュが多かったのが、2.1.0にあげてからだいぶ減りました。)
参考にした文献
https://medium.com/androiddevelopers/workmanager-periodicity-ff35185ff006
https://medium.com/androiddevelopers/workmanager-meets-kotlin-b9ad02f7405e
- 投稿日:2019-07-07T13:01:47+09:00
WorkManager_最新version2.1.0-rc01を実践した際に得られた知見
WorkManagerの使い方と得られた知見を忘れないようにまとめました。
WorkManagerの導入を検討している方などに参考になると嬉しいです。Versionは、2019/6/27にリリースされている現時点で最新の2.1.0-rc01です。
https://developer.android.com/jetpack/androidx/releases/work準備
まず、WorkManagerを使えるようにするために、AndroidプロジェクトにWorkManagerライブラリを追加します。
※WorkManagerには、compileSdkバージョン28以上が必要です。build.gradleimplementation 'androidx.work:work-runtime:2.1.0-rc01' implementation 'androidx.work:work-testing:2.1.0-rc01' implementation 'androidx.work:work-runtime-ktx:2.1.0-rc01'WorkManagerについて
WorkManagerは、Background作業を行いたい際に使用されるライブラリです。
内部的に端末のAPIレベルに合わせて、ライブラリを適切に選択してくれるAPIです。
- 選択されるライブラリ
- API 23+にJobScheduler
- API 14-22用のカスタムAlarmManager + BroadcastReceiver
なので、同じWorkManagerを使用していても、APIレベルによって内部的な処理は異なります。
そのほか詳細はドキュメントを読んでください。公式ドキュメントは以下です。
https://developer.android.com/topic/libraries/architecture/workmanager/basics#conceptsWorkManagerの基本的な構成
構成を知っていれば、ここは読み飛ばしてもらっても大丈夫です。
Worker
- このクラスを継承したクラスを作成する
WorkRequest
- OneTimeWorkRequestまたはPeriodicWorkRequestを使用する
- OneTimeWorkRequest
- Workerを1度だけ実行したいときに使用する
- PeriodicWorkRequest
- Workerを繰り返し、定期実行したいときに使用する
WorkManager
- バックグラウンドで実行する作業をエンキューする
WorkInfo
- タスクに関する情報
- LiveDataでタスクの状態の観察、完了後の戻り値を取得できる
実践
OneTimeWorkRequestを使う
では実際にOneTimeWorkRequestを使ってみます。
今回は、画面にボタンが1つあり、そのボタンをタップするとWorkerがすぐに実行される簡単なサンプルを作ってみました。
はじめに、レイアウトです。
レイアウトはボタンが1つあるだけのレイアウトを作成します。activity_main.xml<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/workerButton" android:layout_height="wrap_content" android:layout_width="wrap_content" android:text="Workerのテスト" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"> </Button> </androidx.constraintlayout.widget.ConstraintLayout>次に、Workerを継承したMyWorkerクラスを作成します。
doWork()の中にWorkerが起動したときに実行したい処理を書きます。
今回の場合は、Logを仕込んでいます。MyWorker.ktclass MyWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) { companion object { const val MY_WORKER = "com.example.workmanager.MyWorker" const val WORKER_NAME = "MyWorker" } override fun doWork(): Result { Log.d("hanakoLog", "doWork!") return Result.success() } }最後に、MainActivityです。
ボタンがタップされたタイミングで、OneTimeWorkRequestをWorkManagerにセットします。MainActivity.ktimport android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import androidx.work.* import com.example.workmanager.MyWorker.Companion.MY_WORKER import kotlinx.android.synthetic.main.activity_main.* import java.util.concurrent.TimeUnit class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) WorkManager.initialize(this, Configuration.Builder().build()) workerButton.setOnClickListener { val workManager = WorkManager.getInstance(applicationContext) val request = OneTimeWorkRequest .Builder(MyWorker::class.java) .addTag(MY_WORKER) .build() workManager.enqueueUniqueWork(MyWorker.WORKER_NAME, ExistingWorkPolicy.REPLACE, request) workManager.getWorkInfoByIdLiveData(request.id).observe(this, Observer { workInfo -> Log.d("hanakoLog", workInfo.state.name) Log.d("hanakoLog", workInfo.id.toString()) Log.d("hanakoLog", workInfo.tags.toString()) }) } } }※ここでデバッグする際のチップス
workManager.getWorkInfoByIdLiveData(request.id)をobserveすることで、リクエストのstatusなどを確認できます。今回は、リクエストのstatus、id、タグをLogで確認します。Workerの動作を確認する際にこれは便利です。以上で実装完了です!
では、Logを確認してみましょう。2019-07-03 22:47:05.912 12670-12670/com.example.workmanager D/hanakoLog: ENQUEUED 2019-07-03 22:47:05.914 12670-12670/com.example.workmanager D/hanakoLog: aa67594e-b05e-4c07-9e48-391948279864 2019-07-03 22:47:05.916 12670-12670/com.example.workmanager D/hanakoLog: [com.example.workmanager.MyWorker] 2019-07-03 22:47:05.963 12670-13502/com.example.workmanager D/hanakoLog: doWork! 2019-07-03 22:47:06.013 12670-12670/com.example.workmanager D/hanakoLog: SUCCEEDED 2019-07-03 22:47:06.014 12670-12670/com.example.workmanager D/hanakoLog: aa67594e-b05e-4c07-9e48-391948279864 2019-07-03 22:47:06.014 12670-12670/com.example.workmanager D/hanakoLog: [com.example.workmanager.MyWorker]Logから、
- ボタンをタップすると
- MyWorkerの
- a67594e-b05e-4c07-9e48-391948279864というidのリクエストが
- ENQUEUEDになり
- doWork()が実行され
- SUCCEEDEDされている
様子がわかります。
OneTimeWorkRequestは、PeriodicWorkRequestに比べて動きがシンプルで動作の確認もしやすいです。
ちなみにdoWork()が実行されるまでに遅延時間を指定することなども可能です。
詳しくはドキュメントを読んでみてください!
https://developer.android.com/reference/androidx/work/OneTimeWorkRequestPeriodicWorkRequestを使う
次に、定期的にdoWork()を実行するPeriodicWorkRequestを実際にやってみます。
今回は、15分に1回Workerが動作するようにしたいと思います。レイアウトとMyWorkerクラスはOneTimeWorkRequestのときと一緒です。MainActivityのみを以下のように修正します。
MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) WorkManager.initialize(this, Configuration.Builder().build()) workerButton.setOnClickListener { val workManager = WorkManager.getInstance(applicationContext) val request = PeriodicWorkRequest .Builder(MyWorker::class.java, PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS, PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS) .addTag(MY_WORKER) .build() workManager.enqueueUniquePeriodicWork(MyWorker.WORKER_NAME, ExistingPeriodicWorkPolicy.REPLACE, request) workManager.getWorkInfoByIdLiveData(request.id).observe(this, Observer { workInfo -> Log.d("hanakoLog", workInfo.state.name) Log.d("hanakoLog", workInfo.id.toString()) Log.d("hanakoLog", workInfo.tags.toString()) }) } } }PeriodicWorkRequest.Builder()の中では、以下の3つを指定しています。
実行するWorkerクラス
繰り返す周期(repeat interval)
- Workerを実行したい間隔を指定します。今回は15分に1回です。デフォルト値はMIN_PERIODIC_INTERVAL_MILLIS(15分)でそれより短い時間を指定しても、15分になります。
jobが実行される最小駆動時間(flex interval)
- WorkManagerは、repeat intervalで例え1時間に指定しても、きっちり1時間で実行されるわけではありません
1時間ごとにここで指定するflex intervalの範囲内で実行されます。 (とはいえ、そこもきっちり保証されるわけではなく、あくまで目安と思っておいた方がよいです) デフォルト値はMIN_PERIODIC_FLEX_MILLIS(5分)でそれより短い時間を指定しても、5分になります。 今回はデフォルトの値を設定しているので、1時間間隔で5分以内にリクエストするコードになっています。
以上で実装完了です!
では、Logを確認してみましょう。2019-07-07 12:29:56.284 18571-18571/com.example.workmanager D/hanakoLog: ENQUEUED 2019-07-07 12:29:56.293 18571-18571/com.example.workmanager D/hanakoLog: cabde0e3-3a17-47ff-9ee6-a4c833cfcd8a 2019-07-07 12:29:56.297 18571-18571/com.example.workmanager D/hanakoLog: [com.example.workmanager.MyWorker] 2019-07-07 12:45:56.100 18571-18571/com.example.workmanager D/hanakoLog: doWork! 2019-07-07 13:03:50.254 18571-18571/com.example.workmanager D/hanakoLog: doWork! 2019-07-07 13:19:82.124 18571-18571/com.example.workmanager D/hanakoLog: doWork!Logから
- ボタンをタップすると
- MyWorkerの
- a67594e-b05e-4c07-9e48-391948279864というidのリクエストが
- ENQUEUEDになり
- その約15分後
- doWork()が実行され
- またその約15分後
- doWork()が実行され
- またその約15分後
- doWork()が実行され・・・
と、繰り返しWorker様子が動いている様子がわかります。
flex intervalintervalの説明で書きましたが、指定した15分できっちり時間通りには動いていないことが見てわかりますね。
なので、この時間に絶対送りたい!という要件にはWorkManagerのPeriodicWorkRequestは向いていないと思います。概ねx時間、x分位の間隔で定期実行させたいな~っという場合に使うとよいと思います。知見
知見①
OneTimeWorkRequestには、ENQUEUED、SUCCEEDED、FAILED、RETRY、CANCELEDステータスがあります。
OneTimeWorkRequestの実行を確認する際は、workInfo.state.namenameでどのステータスにいるのかを確認できます。一方、PeriodicWorkRequestには、SUCCEEDED、FAILED、RETRYステータスはありません。
基本ENQUEUEDのみで、明示的にキャンセルするとCANCELEDになります。なので、PeriodicWorkRequestの実行を確認する際、SUCCEEDED、FAILED、RETRYステータスステータスは取れないことに注意が必要です。知見②
RETRYの挙動は複雑で要注意。
BackoffPolicy
- デフォルトでBackoffPolicy.EXPONENTIALという設定がされているようですが、これは、リトライ間隔を指数関数的に増加させていく設定です。大抵の場合、このデフォルト設定で問題はないですが、リトライが繰り返された場合、最終的に数日後などにリトライされる可能性があることを理解しておきましょう。
リトライ回数
- リトライ回数の最大値を設定することはできません。回数制限したい場合は、Workerクラス内でgetRunAttemptCountを呼ぶと、現在の試行回数を取得可能なので、これを使用して、任意の回数に達したら、FailureさせてRetlyを防ぐとよいそうです。ちなみにgetRunAttemptCountは、一度もリトライされずに問題なく実行されている場合は、0です。
知見③
1.0.0alphaから2.1.0-rc01にあげたことでことで、変わったことがありました。
それは、WorkManagerのテストコードです。公式ドキュメントに記載されている通り、2.1.0からTestWorkerBuilderとTestListenableWorkerBuilderが提供されて、テストコードが書きやすくなっていました!
以下、公式ドキュメントの引用です。WorkManager 2.1.0 provides new TestWorkerBuilder and TestListenableWorkerBuilder classes, which let you test the business logic in your workers without having to initialize WorkManager with WorkManagerTestInitHelper.
詳細については以下をご覧ください。
https://developer.android.com/topic/libraries/architecture/workmanagerWorkManagerのテストについては、改めて書きたいと思います。
おわりに
workManagerの動作を実際に確認することができました。
非同期処理なので動作確認やテストが少しやりにくい所もありますが、versionが上がるたびに少しずつバグFIXされ、テストもしやすいようになってきています。
ぜひ、まだ1.0.0alphaなど使用している場合は2.1.0rc-01にバージョンを上げるのがおススメです(WorkManager関連の内部的なバグでクラッシュが多かったのが、2.1.0にあげてからだいぶ減りました。)
参考にした文献
https://medium.com/androiddevelopers/workmanager-periodicity-ff35185ff006
https://medium.com/androiddevelopers/workmanager-meets-kotlin-b9ad02f7405e
- 投稿日:2019-07-07T13:01:47+09:00
WorkManager-version2.1.0-rc01の実践と知見
WorkManagerの使い方と得られた知見を忘れないようにまとめました。
WorkManagerの導入を検討している方などに参考になると嬉しいです。Versionは、2019/6/27にリリースされている現時点で最新の2.1.0-rc01です。
https://developer.android.com/jetpack/androidx/releases/work本記事の要約
- WorkManagerを使うと、非同期処理が簡単
![]()
- 1回だけの処理も、1時間ごとなど繰り返し処理も簡単にできる
![]()
- ただ、時間間隔には幅があるので、「この時間に絶対送りたい!」という要件には不向き
- RETRYの挙動についてはよく理解しておいた方がよい(バグの元凶)
- デバッグする際にはWorkInfoが使えて、今どういうステータスにあるのか確認できる
![]()
- Version2.1.0からテストがしやすくなった!
![]()
準備
まず、WorkManagerを使えるようにするために、AndroidプロジェクトにWorkManagerライブラリを追加します。
※WorkManagerには、compileSdkバージョン28以上が必要です。build.gradleimplementation 'androidx.work:work-runtime:2.1.0-rc01' implementation 'androidx.work:work-testing:2.1.0-rc01' implementation 'androidx.work:work-runtime-ktx:2.1.0-rc01'WorkManagerについて
WorkManagerは、Background作業を行いたい際に使用されるライブラリです。
内部的に端末のAPIレベルに合わせて、ライブラリを適切に選択してくれるAPIです。
- 選択されるライブラリ
- API 23+にJobScheduler
- API 14-22用のカスタムAlarmManager + BroadcastReceiver
なので、同じWorkManagerを使用していても、APIレベルによって内部的な処理は異なります。
そのほか詳細はドキュメントを読んでください。公式ドキュメントは以下です。
https://developer.android.com/topic/libraries/architecture/workmanager/basics#conceptsWorkManagerの基本的な構成
構成を知っていれば、ここは読み飛ばしてもらっても大丈夫です。
Worker
- このクラスを継承したクラスを作成する
WorkRequest
- OneTimeWorkRequestまたはPeriodicWorkRequestを使用する
- OneTimeWorkRequest
- Workerを1度だけ実行したいときに使用する
- PeriodicWorkRequest
- Workerを繰り返し、定期実行したいときに使用する
WorkManager
- バックグラウンドで実行する作業をエンキューする
WorkInfo
- タスクに関する情報
- LiveDataでタスクの状態の観察、完了後の戻り値を取得できる
実践
OneTimeWorkRequestを使う
では実際にOneTimeWorkRequestを使ってみます。
今回は、画面にボタンが1つあり、そのボタンをタップするとWorkerがすぐに実行される簡単なサンプルを作ってみました。
はじめに、レイアウトです。
レイアウトはボタンが1つあるだけのレイアウトを作成します。activity_main.xml<?xml version="1.0" encoding="utf-8"?> <androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android" xmlns:app="http://schemas.android.com/apk/res-auto" xmlns:tools="http://schemas.android.com/tools" android:layout_width="match_parent" android:layout_height="match_parent" tools:context=".MainActivity"> <Button android:id="@+id/workerButton" android:layout_height="wrap_content" android:layout_width="wrap_content" android:text="Workerのテスト" app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintLeft_toLeftOf="parent" app:layout_constraintRight_toRightOf="parent" app:layout_constraintTop_toTopOf="parent"> </Button> </androidx.constraintlayout.widget.ConstraintLayout>次に、Workerを継承したMyWorkerクラスを作成します。
doWork()の中にWorkerが起動したときに実行したい処理を書きます。
今回の場合は、Logを仕込んでいます。MyWorker.ktclass MyWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) { companion object { const val MY_WORKER = "com.example.workmanager.MyWorker" const val WORKER_NAME = "MyWorker" } override fun doWork(): Result { Log.d("hanakoLog", "doWork!") return Result.success() } }最後に、MainActivityです。
ボタンがタップされたタイミングで、OneTimeWorkRequestをWorkManagerにセットします。MainActivity.ktimport android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity import androidx.lifecycle.Observer import androidx.work.* import com.example.workmanager.MyWorker.Companion.MY_WORKER import kotlinx.android.synthetic.main.activity_main.* import java.util.concurrent.TimeUnit class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) WorkManager.initialize(this, Configuration.Builder().build()) workerButton.setOnClickListener { val workManager = WorkManager.getInstance(applicationContext) val request = OneTimeWorkRequest .Builder(MyWorker::class.java) .addTag(MY_WORKER) .build() workManager.enqueueUniqueWork(MyWorker.WORKER_NAME, ExistingWorkPolicy.REPLACE, request) workManager.getWorkInfoByIdLiveData(request.id).observe(this, Observer { workInfo -> Log.d("hanakoLog", workInfo.state.name) Log.d("hanakoLog", workInfo.id.toString()) Log.d("hanakoLog", workInfo.tags.toString()) }) } } }※ここでデバッグする際のチップス
workManager.getWorkInfoByIdLiveData(request.id)をobserveすることで、リクエストのstatusなどを確認できます。今回は、リクエストのstatus、id、タグをLogで確認します。Workerの動作を確認する際にこれは便利です。以上で実装完了です!
では、Logを確認してみましょう。2019-07-03 22:47:05.912 12670-12670/com.example.workmanager D/hanakoLog: ENQUEUED 2019-07-03 22:47:05.914 12670-12670/com.example.workmanager D/hanakoLog: aa67594e-b05e-4c07-9e48-391948279864 2019-07-03 22:47:05.916 12670-12670/com.example.workmanager D/hanakoLog: [com.example.workmanager.MyWorker] 2019-07-03 22:47:05.963 12670-13502/com.example.workmanager D/hanakoLog: doWork! 2019-07-03 22:47:06.013 12670-12670/com.example.workmanager D/hanakoLog: SUCCEEDED 2019-07-03 22:47:06.014 12670-12670/com.example.workmanager D/hanakoLog: aa67594e-b05e-4c07-9e48-391948279864 2019-07-03 22:47:06.014 12670-12670/com.example.workmanager D/hanakoLog: [com.example.workmanager.MyWorker]Logから、
- ボタンをタップすると
- MyWorkerの
- a67594e-b05e-4c07-9e48-391948279864というidのリクエストが
- ENQUEUEDになり
- doWork()が実行され
- SUCCEEDEDされている
様子がわかります。
OneTimeWorkRequestは、PeriodicWorkRequestに比べて動きがシンプルで動作の確認もしやすいです。
ちなみにdoWork()が実行されるまでに遅延時間を指定することなども可能です。
詳しくはドキュメントを読んでみてください!
https://developer.android.com/reference/androidx/work/OneTimeWorkRequestPeriodicWorkRequestを使う
次に、定期的にdoWork()を実行するPeriodicWorkRequestを実際にやってみます。
今回は、15分に1回Workerが動作するようにしたいと思います。レイアウトとMyWorkerクラスはOneTimeWorkRequestのときと一緒です。MainActivityのみを以下のように修正します。
MainActivity.ktclass MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) WorkManager.initialize(this, Configuration.Builder().build()) workerButton.setOnClickListener { val workManager = WorkManager.getInstance(applicationContext) val request = PeriodicWorkRequest .Builder(MyWorker::class.java, PeriodicWorkRequest.MIN_PERIODIC_INTERVAL_MILLIS, TimeUnit.MILLISECONDS, PeriodicWorkRequest.MIN_PERIODIC_FLEX_MILLIS, TimeUnit.MILLISECONDS) .addTag(MY_WORKER) .build() workManager.enqueueUniquePeriodicWork(MyWorker.WORKER_NAME, ExistingPeriodicWorkPolicy.REPLACE, request) workManager.getWorkInfoByIdLiveData(request.id).observe(this, Observer { workInfo -> Log.d("hanakoLog", workInfo.state.name) Log.d("hanakoLog", workInfo.id.toString()) Log.d("hanakoLog", workInfo.tags.toString()) }) } } }PeriodicWorkRequest.Builder()の中では、以下の3つを指定しています。
実行するWorkerクラス
繰り返す周期(repeat interval)
- Workerを実行したい間隔を指定します。今回は15分に1回です。デフォルト値はMIN_PERIODIC_INTERVAL_MILLIS(15分)でそれより短い時間を指定しても、15分になります。
jobが実行される最小駆動時間(flex interval)
- WorkManagerは、repeat intervalで例え1時間に指定しても、きっちり1時間で実行されるわけではありません
1時間ごとにここで指定するflex intervalの範囲内で実行されます。 (とはいえ、そこもきっちり保証されるわけではなく、あくまで目安と思っておいた方がよいです) デフォルト値はMIN_PERIODIC_FLEX_MILLIS(5分)でそれより短い時間を指定しても、5分になります。 今回はデフォルトの値を設定しているので、1時間間隔で5分以内にリクエストするコードになっています。
以上で実装完了です!
では、Logを確認してみましょう。2019-07-07 12:29:56.284 18571-18571/com.example.workmanager D/hanakoLog: ENQUEUED 2019-07-07 12:29:56.293 18571-18571/com.example.workmanager D/hanakoLog: cabde0e3-3a17-47ff-9ee6-a4c833cfcd8a 2019-07-07 12:29:56.297 18571-18571/com.example.workmanager D/hanakoLog: [com.example.workmanager.MyWorker] 2019-07-07 12:45:56.100 18571-18571/com.example.workmanager D/hanakoLog: doWork! 2019-07-07 13:03:50.254 18571-18571/com.example.workmanager D/hanakoLog: doWork! 2019-07-07 13:19:82.124 18571-18571/com.example.workmanager D/hanakoLog: doWork!Logから
- ボタンをタップすると
- MyWorkerの
- a67594e-b05e-4c07-9e48-391948279864というidのリクエストが
- ENQUEUEDになり
- その約15分後
- doWork()が実行され
- またその約15分後
- doWork()が実行され
- またその約15分後
- doWork()が実行され・・・
と、繰り返しWorker様子が動いている様子がわかります。
flex intervalintervalの説明で書きましたが、指定した15分できっちり時間通りには動いていないことが見てわかりますね。
なので、この時間に絶対送りたい!という要件にはWorkManagerのPeriodicWorkRequestは向いていないと思います。概ねx時間、x分位の間隔で定期実行させたいな~っという場合に使うとよいと思います。知見
知見①
OneTimeWorkRequestには、ENQUEUED、SUCCEEDED、FAILED、RETRY、CANCELEDステータスがあります。
OneTimeWorkRequestの実行を確認する際は、workInfo.state.namenameでどのステータスにいるのかを確認できます。一方、PeriodicWorkRequestには、SUCCEEDED、FAILED、RETRYステータスはありません。
基本ENQUEUEDのみで、明示的にキャンセルするとCANCELEDになります。なので、PeriodicWorkRequestの実行を確認する際、SUCCEEDED、FAILED、RETRYステータスステータスは取れないことに注意が必要です。知見②
RETRYの挙動は複雑で要注意。
BackoffPolicy
- デフォルトでBackoffPolicy.EXPONENTIALという設定がされているようですが、これは、リトライ間隔を指数関数的に増加させていく設定です。大抵の場合、このデフォルト設定で問題はないですが、リトライが繰り返された場合、最終的に数日後などにリトライされる可能性があることを理解しておきましょう。
リトライ回数
- リトライ回数の最大値を設定することはできません。回数制限したい場合は、Workerクラス内でgetRunAttemptCountを呼ぶと、現在の試行回数を取得可能なので、これを使用して、任意の回数に達したら、FailureさせてRetlyを防ぐとよいそうです。ちなみにgetRunAttemptCountは、一度もリトライされずに問題なく実行されている場合は、0です。
知見③
1.0.0alphaから2.1.0-rc01にあげたことでことで、変わったことがありました。
それは、WorkManagerのテストコードです。公式ドキュメントに記載されている通り、2.1.0からTestWorkerBuilderとTestListenableWorkerBuilderが提供されて、テストコードが書きやすくなっていました!
以下、公式ドキュメントの引用です。WorkManager 2.1.0 provides new TestWorkerBuilder and TestListenableWorkerBuilder classes, which let you test the business logic in your workers without having to initialize WorkManager with WorkManagerTestInitHelper.
詳細については以下をご覧ください。
https://developer.android.com/topic/libraries/architecture/workmanagerWorkManagerのテストについては、改めて書きたいと思います。
おわりに
workManagerの動作を実際に確認することができました。
非同期処理なので動作確認やテストが少しやりにくい所もありますが、versionが上がるたびに少しずつバグFIXされ、テストもしやすいようになってきています。
ぜひ、まだ1.0.0alphaなど使用している場合は2.1.0rc-01にバージョンを上げるのがおススメです(WorkManager関連の内部的なバグでクラッシュが多かったのが、2.1.0にあげてからだいぶ減りました。)
参考にした文献
https://medium.com/androiddevelopers/workmanager-periodicity-ff35185ff006
https://medium.com/androiddevelopers/workmanager-meets-kotlin-b9ad02f7405e
- 投稿日:2019-07-07T00:59:24+09:00
Room + Coroutines + Koin + Kotlin構成での実装
Room
今回は
Kotlin + Room + Coroutines + Koinの構成で進めていきたいと思います。構成要素
※公式ページから引用
以下の3つの要素で構成されている
・Database: データベース内のエンティティとデータアクセスオブジェクトのリストを定義
・Entity: 各エンティティに対して、アイテムを保持するためのデータベーステーブルが作成される
・Dao: データベースにアクセスするメソッドを定義する役割を果たす
環境構築
必要なパッケージをインストールします。
app/build.gradledef koin_version = '1.0.2' implementation "org.koin:koin-android:$koin_version" implementation "org.koin:koin-androidx-scope:$koin_version" implementation "org.koin:koin-androidx-viewmodel:$koin_version" def coroutines_version = '1.2.2' implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" // viewmodel + coroutines implementation "androidx.lifecycle:lifecycle-viewmodel-ktx:2.2.0-alpha02" def room_version = "2.1.0" implementation "androidx.room:room-runtime:$room_version" kapt "androidx.room:room-compiler:$room_version" implementation "androidx.room:room-ktx:$room_version"
実装
簡単なサンプルとしてメッセージを扱う構成のDatabaseを実装していきます。
Entity作成
まずメッセージの所有者を扱うユーザー管理用のEntityを作成します。
@Entity data class UserData( @PrimaryKey(autoGenerate = true) val id: Long, val name: String )
idに関しては@PrimaryKey(autoGenerate = true)で主キーかつ
自動採番するように設定しています。次ににメッセージを保存するEntityを作成
@Entity(foreignKeys = [ForeignKey( entity = UserData::class, parentColumns = arrayOf("id"), childColumns = arrayOf("owner_id") )]) data class MessageData( @PrimaryKey(autoGenerate = true) val id: Long, @ColumnInfo(name = "owner_id") val ownerId: Long, val body: String, val type: Int, @ColumnInfo(name = "is_owner") val isOwner: Boolean, @ColumnInfo(name = "is_delete") val isDelete: Boolean, @ColumnInfo(name = "created_at") val createdAt: Date )
ForeignKeyで外部キーをowner_idに設定し、
1つ前に作成したUserDataのidと紐づけています。
@ColumnInfo(name = "xxxxx")では名前が実際の列名と異なる場合に設定しています。Converter作成
Entityの
MessageDataでDate型を使用していますが、
実際のDBデータにはDate型は無い為、変換が必要になります。class DateConverter { @TypeConverter fun fromTimestamp(value: Long?): Date? = value?.let { Date(it) } @TypeConverter fun dateToTimestamp(date: Date?): Long? = date?.let { it.time } }上記では、Date型をLongの値へ変換しDBデータとして格納するように変換しています。
Dao作成
ユーザーを検索したり登録するDaoを作成します。
@Dao interface UserDao { // 登録されている全ユーザーを取得 @Query("SELECT * FROM UserData") suspend fun findAll(): List<UserData> // userIdで指定したユーザーを検索 @Query("SELECT * FROM UserData WHERE id = :userId") suspend fun find(userId: Long): UserData // ユーザー登録 @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun add(userData: UserData) }メッセージ一覧の取得や、登録を行うDaoを作成
@Dao interface MessageDao { // メッセージ登録 @Insert(onConflict = OnConflictStrategy.REPLACE) suspend fun add(messageData: MessageData) // 全メッセージ取得 @Query("SELECT * FROM MessageData") suspend fun findAll(): List<MessageData> }Database作成
@androidx.room.Database(entities = [ MessageData::class, UserData::class ], version = 1) @TypeConverters(DateConverter::class) abstract class Database: RoomDatabase() { abstract fun messageDao(): MessageDao abstract fun userDao(): UserDao }EntitiyとConverter、Dao生成メソッドを定義しています。
Koinを使用してDI
object KoinModule { fun appModule() = module { // Databaseインスタンスをシングルトンで生成 single { Room.databaseBuilder(androidContext(), Database::class.java, "xxx.db").build() } } fun messageModule() = module { // Daoを生成 factory { get<Database>().messageDao() } // viewModelのinjection viewModel { MessageViewModel(get()) } // repositoryのinjection single { MessageRepository(get()) } } }使用する
- MessageRepository
class MessageRepository(private val messageDao: MessageDao) { /** * Query all messages. */ suspend fun getMessages(): List<MessageData> = messageDao.findAll() }
- ViewModel
class MessageViewModel(private val messageRepository: MessageRepository): ViewModel() { private var messageList = MutableLiveData<List<MessageData>>() /** * LiveData取得 * * @return {@link LiveData} */ fun getMessagesLiveData(): LiveData<List<MessageData>> = messageList /** * List更新 */ fun fetchList() { viewModelScope.launch { messageList.value = messageRepository.getMessages() } } }
バッドノウハウ
- ビルド時に以下のエラーが発生
![]()
org.koin.error.BeanInstanceCreationException: Can't create definition for 'Factory [name='ListViewModel',class='xxxxxxxxxxxx.ui.ListViewModel']' due to error : Can't create definition for 'Factory [name='MessageBoxesRepository',class='xxxxxxxxxxxx.repository.MessageBoxesRepository']' due to error : Can't create definition for 'Factory [name='MessageDao',class='xxxxxxxxxxxx.repository.db.dao.MessageDao']' due to error : Can't create definition for 'Single [name='Database',class='xxxxxxxxxxxx.repository.db.Database']' due to error : cannot find implementation for xxxxxxxxxxxx.repository.db.Database. Database_Impl does not exist ....
kaptとしないといけない所がannotationProcessorになっていないか要確認apply plugin: 'kotlin-kapt' ... kapt "androidx.room:room-compiler:$room_version"
- ビルド時に以下のエラーが発生
Maybe you forgot to add the referenced entity in the entities list of the @Database annotation?
@ForeignKeyを使った時に↑のエラーに遭遇エラーメッセージ通り、
@DatabaseアノテーションにEntityを追加するのを忘れていただけ...
参考URL
















