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

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のソース依存ぽい。

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

Android で CloudVision API を使ってウェブ検出する

Android で CloudVision API を使ってラベル検出する
の続きです。

前回の記事では、ラベル検出を行った。
今回は、ウェブ検出を行う。

下記を参考にした。

docs : ウェブ検出のチュートリアル

参考 : Android : Google Cloud Vision APIを用いてWEB_DETECTIONする

リクエストを作成する

基本的な処理は、ラベル検出と同じです。

違いは、機能のタイプにウェブ検出を指定すること。

docs : Feature 機能

                Feature feature = new Feature();
// ウェブ検出を指定する
                feature.setType("WEB_DETECTION");

ウェブ検出のレスポンス

ラベル検出では、取得できるアノテーションはラベルだけでした。

ウェブ検出では、下記のアノテーションが取得できる。

  • ウェブから取得したラベル
  • 一致する画像のあるサイト URL
  • リクエスト中の画像に部分一致または完全一致するウェブ画像を参照する URL
  • 視覚的に類似している画像を参照する URL

WebDetection

ウェブ検出のレスポンスのクラス

documentation : 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

ウェブラベルのメタデータ
ラベルが取得できる

documentation : WebLabel

WebEntity

ウェブエンティティのメタデータ
ラベルとスコアが取得できる

documentation : WebEntity

WebPage

ウェブページのメタデータ
- 一致する画像のあるサイトのURLと、
- 一致する画像のURLが取得できる

documentation : WebPage

WebImage

オンライン画像のメタデータ
- 一致する画像のURLが取得できる

documentation : WebImage

レスポンスの処理

  • 一致する画像のあるサイトのURLから ウェブブラウザアプリでそのサイトを表示する。
  • 一致する画像のURLから その画像を表示する。

実行例

スクリーンショット
cloud_vision2_image_list.png

テキスト

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

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

DarkMode実装する際に便利なAndroidStudioの機能

DarkModeを実装する際に便利なチップスを紹介します。

ダークモードのデザイン確認

DarkModeは、以下のようにリソース末尾に-nightを付与することで昼間向けテーマと夜間向けテーマを設定することができます。


res/values/color.xml
res/values-night/color.xml

これまで、上記のようにリソースをダークモード用と通常用に設定した後、
ダークモードのデザインを確認したいと思ったら、

  1. ビルドして
  2. ダークモードに切り替えて
  3. デザイン確認

という流れで、必ずビルドしないと確認できないと思っていました。

が、、、AndroidStudio上で簡単にダークモードのデザインも通常モードのデザインも確認できる便利な方法がありました。

サンブルとして以下のレイアウトでダークモードを実装しています。
darkmode1.png

このとき、以下の図のように、レイアウトの左上にある、Orientation For Previewのアイコンを選択します。
すると、そこにNight Modeというものがあります。
darkmode2.png

そこで、Night Modeを選択すると、ダークモードのデザインに一瞬で切り替わります。
darkmode3.png

簡単に切り替えられて、どちらのモードのデザインもAndroid Studio上ですぐに確認できるのがとても便利です。

これを知るまでは、ダークモードのデザインを確認するには、少しのカラー変更でも毎回ビルドして確認していたので、とても非効率でした:cry:
いつからこの機能があったのかわかりませんが、早く知りたかった・・・

Android Studioは、便利な機能がたくさんあって、まだまだ知らない便利な機能がありそうです。
便利な機能は活用して、開発効率がどんどん上げていきたいなーと思います:sunny:

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

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)の組み込み方法を解説します!

組み込む!

新規プロジェクトの作成

プロジェクト名等は任意の名前にしてください!
image.png
今回は、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をコピーします。
image.png
コピーしただけだと、エラーが発生します。
Classifier.javaでLoggerクラスのインポート先を書き換えます。

Classfier.java
import 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();

image.png
削除すると、AndroidStudioがこんなことを聞いてくるので、「Alt+Enter」を押せば、自動でインポートしてくれます。
image.png
インポートする際に、
image.png

2種類出てくると思いますので、(android.jar)と書かれていないほうを選択します。

これで、エラーがすべて消えたと思います。

モデルの設置

モデルとlabel_textをassetフォルダーに設置します。
こちらよりモデルをダウンロードしてください。
image.png

まずは、assetフォルダーフォルダーを作成します。
image.png
解凍したフォルダの中から、ファイルをコピーします。
image.png
コピーしたら、名前を「model.tflite」と「labels.txt」に変更します。
image.png
これでモデルの設置は完了です。

Viewの配置

こんな感じにTextView,Button,ImageViewを配置します。
ButtonにはonClickを設定押しておきます。
↑ onClickを設定する方法ってリスナーのほうがいいのかな。詳しい人おしえてくださいな
image.png

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.java
    ImageView imageView;
    TextView textView;
    Classifier classifier;
    private static final int RESULT_IMAGEFILE = 1001;  //画像取得時に使用するリクエストコード

onCreate内でtextview,ImageViewの紐づけを行います。

MainActivity.java
        imageView = findViewById(R.id.imageView);
        textView = findViewById(R.id.textView);

次に、Classfierの呼び出しを行います。

MainActivity.java
        try {
            classifier = Classifier.create(this,QUANTIZED,CPU,2);
        } catch (IOException e) {
            e.printStackTrace();
        }

引数は、Acritivy、Modelの種類、演算に使用するデバイスの指定、使用するスレッド数を指定します。
基本はこの設定で動くと思いますが、臨機応変に変更しましょう。

ボタンの動作を書く

ボタンを押したら、ギャラリーを開いて画像が選択できるようにIntentを飛ばします。

MainAcritivy.java
public 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.java
            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に変換し、画像のサイズを300より小さくなるようにしています。
300よりでかい画像だと、正常に判定することができず、エラーで落ちてしまいます。
Caused by: java.lang.ArrayIndexOutOfBoundsException
そのため、縦横比を維持しつつ、縦横が300以内に収まるようにしています。

MainAcrivity.java
                        Bitmap 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.java
                        List<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);

以上で完成です!!

実際に動かしてみる

それでは、実際に動かしてみたいと思います!
まずは、犬の画像
image.png
公園のベンチ、、
爪、、、
アメリカンカメレオン、、、
んー
精度は微妙ですね
次は、美しい景色の画像。
デルフトの街並みです

image.png

ウィンドウスクリーン、、、
ドアマット、、、
ブラインド、、、
んー
ダメやん!

まとめ

精度は微妙でしたが、うまく?画像を分類することができました!
今度は、リアルタイムで分類をしてみたいと思います!
ではでは!

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

はじめての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.gradle
implementation '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#concepts

WorkManagerの基本的な構成

  • 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.kt
class 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.kt
import 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())
            })
        }
    }
}

※ここでデバッグする際のチップス:point_up::sparkles:
workManager.getWorkInfoByIdLiveData(request.id)をobserveすることで、リクエストのstatusなどを確認できます。今回は、リクエストのstatus、id、タグをLogで確認します。Workerの動作を確認する際にこれは便利です。

以上で実装完了です!:ok_hand:
では、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/OneTimeWorkRequest

PeriodicWorkRequestを使う

次に、定期的にdoWork()を実行するPeriodicWorkRequestを実際にやってみます。

今回は、15分に1回Workerが動作するようにしたいと思います。レイアウトとMyWorkerクラスはOneTimeWorkRequestのときと一緒です。MainActivityのみを以下のように修正します。

MainActivity.kt
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 = 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時間で実行されるわけではありません:no_good:1時間ごとにここで指定するflex intervalの範囲内で実行されます。 (とはいえ、そこもきっちり保証されるわけではなく、あくまで目安と思っておいた方がよいです) デフォルト値はMIN_PERIODIC_FLEX_MILLIS(5分)でそれより短い時間を指定しても、5分になります。 今回はデフォルトの値を設定しているので、1時間間隔で5分以内にリクエストするコードになっています。

以上で実装完了です!:ok_hand:
では、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の挙動は複雑で要注意。:rolling_eyes:

  • 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/workmanager

WorkManagerのテストについては、改めて書きたいと思います。

おわりに

workManagerの動作を実際に確認することができました。
非同期処理なので動作確認やテストが少しやりにくい所もありますが、versionが上がるたびに少しずつバグFIXされ、テストもしやすいようになってきています。
ぜひ、まだ1.0.0alphaなど使用している場合は2.1.0rc-01にバージョンを上げるのがおススメです:thumbsup_tone1:(WorkManager関連の内部的なバグでクラッシュが多かったのが、2.1.0にあげてからだいぶ減りました。)

参考にした文献

https://medium.com/androiddevelopers/workmanager-periodicity-ff35185ff006
https://medium.com/androiddevelopers/workmanager-meets-kotlin-b9ad02f7405e

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

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.gradle
implementation '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#concepts

WorkManagerの基本的な構成

構成を知っていれば、ここは読み飛ばしてもらっても大丈夫です。

  • 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.kt
class 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.kt
import 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())
            })
        }
    }
}

※ここでデバッグする際のチップス:point_up::sparkles:
workManager.getWorkInfoByIdLiveData(request.id)をobserveすることで、リクエストのstatusなどを確認できます。今回は、リクエストのstatus、id、タグをLogで確認します。Workerの動作を確認する際にこれは便利です。

以上で実装完了です!:ok_hand:
では、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/OneTimeWorkRequest

PeriodicWorkRequestを使う

次に、定期的にdoWork()を実行するPeriodicWorkRequestを実際にやってみます。

今回は、15分に1回Workerが動作するようにしたいと思います。レイアウトとMyWorkerクラスはOneTimeWorkRequestのときと一緒です。MainActivityのみを以下のように修正します。

MainActivity.kt
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 = 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時間で実行されるわけではありません:no_good:1時間ごとにここで指定するflex intervalの範囲内で実行されます。 (とはいえ、そこもきっちり保証されるわけではなく、あくまで目安と思っておいた方がよいです) デフォルト値はMIN_PERIODIC_FLEX_MILLIS(5分)でそれより短い時間を指定しても、5分になります。 今回はデフォルトの値を設定しているので、1時間間隔で5分以内にリクエストするコードになっています。

以上で実装完了です!:ok_hand:
では、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の挙動は複雑で要注意。:rolling_eyes:

  • 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/workmanager

WorkManagerのテストについては、改めて書きたいと思います。

おわりに

workManagerの動作を実際に確認することができました。
非同期処理なので動作確認やテストが少しやりにくい所もありますが、versionが上がるたびに少しずつバグFIXされ、テストもしやすいようになってきています。
ぜひ、まだ1.0.0alphaなど使用している場合は2.1.0rc-01にバージョンを上げるのがおススメです:thumbsup_tone1:(WorkManager関連の内部的なバグでクラッシュが多かったのが、2.1.0にあげてからだいぶ減りました。)

参考にした文献

https://medium.com/androiddevelopers/workmanager-periodicity-ff35185ff006
https://medium.com/androiddevelopers/workmanager-meets-kotlin-b9ad02f7405e

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

WorkManager-version2.1.0-rc01の実践と知見

WorkManagerの使い方と得られた知見を忘れないようにまとめました。
WorkManagerの導入を検討している方などに参考になると嬉しいです。

Versionは、2019/6/27にリリースされている現時点で最新の2.1.0-rc01です。
https://developer.android.com/jetpack/androidx/releases/work

本記事の要約

  • WorkManagerを使うと、非同期処理が簡単:thumbsup:
  • 1回だけの処理も、1時間ごとなど繰り返し処理も簡単にできる:thumbsup:
  • ただ、時間間隔には幅があるので、「この時間に絶対送りたい!」という要件には不向き
  • RETRYの挙動についてはよく理解しておいた方がよい(バグの元凶)
  • デバッグする際にはWorkInfoが使えて、今どういうステータスにあるのか確認できる:thumbsup:
  • Version2.1.0からテストがしやすくなった!:thumbsup:

準備

まず、WorkManagerを使えるようにするために、AndroidプロジェクトにWorkManagerライブラリを追加します。
※WorkManagerには、compileSdkバージョン28以上が必要です。

build.gradle
implementation '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#concepts

WorkManagerの基本的な構成

構成を知っていれば、ここは読み飛ばしてもらっても大丈夫です。

  • 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.kt
class 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.kt
import 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())
            })
        }
    }
}

※ここでデバッグする際のチップス:point_up::sparkles:
workManager.getWorkInfoByIdLiveData(request.id)をobserveすることで、リクエストのstatusなどを確認できます。今回は、リクエストのstatus、id、タグをLogで確認します。Workerの動作を確認する際にこれは便利です。

以上で実装完了です!:ok_hand:
では、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/OneTimeWorkRequest

PeriodicWorkRequestを使う

次に、定期的にdoWork()を実行するPeriodicWorkRequestを実際にやってみます。

今回は、15分に1回Workerが動作するようにしたいと思います。レイアウトとMyWorkerクラスはOneTimeWorkRequestのときと一緒です。MainActivityのみを以下のように修正します。

MainActivity.kt
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 = 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時間で実行されるわけではありません:no_good:1時間ごとにここで指定するflex intervalの範囲内で実行されます。 (とはいえ、そこもきっちり保証されるわけではなく、あくまで目安と思っておいた方がよいです) デフォルト値はMIN_PERIODIC_FLEX_MILLIS(5分)でそれより短い時間を指定しても、5分になります。 今回はデフォルトの値を設定しているので、1時間間隔で5分以内にリクエストするコードになっています。

以上で実装完了です!:ok_hand:
では、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の挙動は複雑で要注意。:rolling_eyes:

  • 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/workmanager

WorkManagerのテストについては、改めて書きたいと思います。

おわりに

workManagerの動作を実際に確認することができました。
非同期処理なので動作確認やテストが少しやりにくい所もありますが、versionが上がるたびに少しずつバグFIXされ、テストもしやすいようになってきています。
ぜひ、まだ1.0.0alphaなど使用している場合は2.1.0rc-01にバージョンを上げるのがおススメです:thumbsup_tone1:(WorkManager関連の内部的なバグでクラッシュが多かったのが、2.1.0にあげてからだいぶ減りました。)

参考にした文献

https://medium.com/androiddevelopers/workmanager-periodicity-ff35185ff006
https://medium.com/androiddevelopers/workmanager-meets-kotlin-b9ad02f7405e

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

Room + Coroutines + Koin + Kotlin構成での実装

Room

公式ページ

今回は Kotlin + Room + Coroutines + Koin の構成で進めていきたいと思います。

構成要素

※公式ページから引用

以下の3つの要素で構成されている

Database: データベース内のエンティティとデータアクセスオブジェクトのリストを定義
Entity: 各エンティティに対して、アイテムを保持するためのデータベーステーブルが作成される
Dao: データベースにアクセスするメソッドを定義する役割を果たす

:computer:環境構築


必要なパッケージをインストールします。

app/build.gradle
    def 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"

:pencil: 実装


簡単なサンプルとしてメッセージを扱う構成の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つ前に作成した UserDataid と紐づけています。
@ColumnInfo(name = "xxxxx") では名前が実際の列名と異なる場合に設定しています。

Converter作成

Entityの MessageDataDate 型を使用していますが、
実際の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()
        }
    }
}

:bomb: バッドノウハウ


  • ビルド時に以下のエラーが発生 :warning:
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"

  • ビルド時に以下のエラーが発生 :warning:
Maybe you forgot to add the referenced entity in the entities list of the @Database annotation?

@ForeignKey を使った時に↑のエラーに遭遇

エラーメッセージ通り、@Database アノテーションにEntityを追加するのを忘れていただけ...

:link: 参考URL


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