まえがき
これはWorkManagerを使って,1日に1度のバックグラウンドタスクをスケジューリングするためのアプリ作ることを目的とします.
今回は,24時間分のユーザのアプリ使用履歴を取得してテキストとして記録するという機能を,1日に1度発火させるようにします.
あくまでも備忘録なモチベーションなのであしからず.
WorkManagerとは
一応,WorkManagerについて触れておきます.
以下公式ドキュメント
WorkManager は、永続処理のための推奨ソリューションです。アプリの再起動やシステムの再起動後もスケジュール設定が維持された場合、処理は永続します。ほとんどのバックグラウンド処理は永続処理で実現することが最善であるため、WorkManager が、バックグラウンド処理に推奨される主要な API です。
また,AlarmManagerも似たようなことができますが,これは絶亭に正確な時刻に発火が必要なものに対して使うものであり,WorkManagerに比べるとバッテリーに優しくないそうです.
また,デバイスがオンラインの場合,電源に接続されている場合のみ実行など条件をつけることも可能です.
今回は,正確性よりも,ひっそりバックグラウンで動いてくれることを望むのでWorkManagerで実装します.
早速実装
WorkManagerを実現させるには大まかに以下が必要です.
- WorkManagerを使って動かしたいなにか
- Worker
- Workを登録する処理
WorkManagerを使って動かしたいものは,準備してあるものとします.
自身の場合は以下です
val usageStats = GetUsageStats(context).getUsageStats()
usageRepository.appendUsage(fileName, usageStats)
getUsageStats()
を使ってList<UsageStats>
を取得しています(使用履歴のリスト).
こちらをUsageRepository
を通して,appendUsage()
を呼び出し,端末内に保存しています.
また,今回はDIを通すために,Applicatoin()を継承したクラスも使用します.
依存関係の追加
//workmanager
def work_version = "2.7.1"
implementation "androidx.work:work-runtime:$work_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
//Hilt
implementation 'com.google.dagger:hilt-android:2.44'
kapt 'com.google.dagger:hilt-compiler:2.43.2'
implementation 'androidx.hilt:hilt-navigation-compose:1.0.0'
//worker with hilt
implementation 'androidx.hilt:hilt-work:1.0.0'
// When using Kotlin.
kapt 'androidx.hilt:hilt-compiler:1.0.0'
// When using Java.
annotationProcessor 'androidx.hilt:hilt-compiler:1.0.0'
// For instrumentation tests
androidTestImplementation 'com.google.dagger:hilt-android-testing:2.43.2'
kaptAndroidTest 'com.google.dagger:hilt-compiler:2.43.2'
// For local unit tests
testImplementation 'com.google.dagger:hilt-android-testing:2.43.2'
kaptTest 'com.google.dagger:hilt-compiler:2.43.2'
HiltとWorkerについての依存関係を載せておきます.
Workerをセットする
Hiltを使っているため,普通のWorkerにちょっと記載が増えてます.
Corutineが使われる,CorutineWorker
を継承したWorkerを作成します.
以下のdoWork()
のブロック内に期待する処理を記述することでバックグラウンドでその処理が実行されます.
今回はHiltを使ってDIを行うため, @HiltWorker
というアノテーションを付与します.
また,DIを通してあるRepositoryを使用するために@AssistedInject constructor()
を使います.
もともと,CorutineWorkerで必要な引数には,@Assited
を付与します.
これらアノテーションを用いることで,本来呼び出すことのできないRepositoryを呼び出すことができます.
これは,実質的にはWorkerFactory
を作り直すことで実現しています.
WorkerFactoryの宣言はApplication
を継承したクラスで行います.後で記載します.
@HiltWorker
class GetUsageWorker @AssistedInject constructor(
@Assisted appContext: Context,
@Assisted params: WorkerParameters,
private val usageRepository: UsageRepository,
): CoroutineWorker(appContext, params) {
companion object {
const val WORK_NAME = "com.example.accumulateusage.works.GetUsageWorker"
}
private val fileName = "data.txt"
private val context = appContext
//ここに何をバックグラウンドで実行するのかを記述する.
override suspend fun doWork(): Result {
try {
Log.i("Worker","Work request for sync is run")
val usageStats = GetUsageStats(context).getUsageStats()
usageRepository.appendUsage(fileName, usageStats)
}catch (e: Exception){
Log.i("Worker","Error: $e")
}
return Result.success()
}
}
上記のワーカーをどこかからセットすることでWorkとして登録できます.
Workの登録
今回は画面上のボタンを押すことでWorkerを登録します
登録処理自体はViewModelに記載されています.
Screen側のボタンのタップで処理を呼び出しますが,今回はUIの方のコードは省略します.
PeriodicWorkRequestBuilder<GetUsageWorker>(24, TimeUnit.HOURS)
の部分で24時間おきに動いてくれるWorkerを登録しています.
class MainViewModel @Inject constructor(
private val usageRepository: UsageRepository
): ViewModel(){
fun setWorkManager(context: Context){
val request = PeriodicWorkRequestBuilder<GetUsageWorker>(24, TimeUnit.HOURS)
.build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
GetUsageWorker.WORK_NAME,
ExistingPeriodicWorkPolicy.KEEP,
request
)
}
}
Application ClassにFactoryを作る
今回はDIを行うために,Workerの引数がデフォルトと違います.
普段は自動でインスタンスを作ってくれますが,今回の場合はWorkerを作る手順を記述する必要があります.
そのため,WorkerFactoryを定義したりします.
@HiltAndroidApp
class AccumulateUsageApp: Application(), Configuration.Provider {
//Hilt用のWorkerFactory
@Inject lateinit var workerFactory: HiltWorkerFactory
override fun onCreate() {
super.onCreate()
instance = this
}
companion object {
lateinit var instance: AccumulateUsageApp private set
}
WorkerFactoryをセットする.
override fun getWorkManagerConfiguration() =
Configuration.Builder()
.setWorkerFactory(workerFactory)
.build()
}
Manifestを編集する.
上記のようなWorkManagerの設定を行うと,マニフェストを編集しなければWokerが正常にセットできなくなります.
理由としては,WorkManagerがandroidx.startupのイニシャライザを使用するためです.
Manifestに,以下の記述をすることでデフォルトのイニシャライザを削除できます.
アプリでアプリの起動をしない場合.
<!-- If you want to disable android.startup completely. -->
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
tools:node="remove">
</provider>
アプリでアプリの起動を行う場合
<provider
android:name="androidx.startup.InitializationProvider"
android:authorities="${applicationId}.androidx-startup"
android:exported="false"
tools:node="merge">
<!-- If you are using androidx.startup to initialize other components -->
<meta-data
android:name="androidx.work.WorkManagerInitializer"
android:value="androidx.startup"
tools:node="remove" />
</provider>
こちらでコードの記述は以上になります.
すべてのコードは以下に乗せてあります.
https://github.com/M0710Fa/AccumulateUsage
まとめ
WorkMangerでHiltを使うときに困らないように残しておきました.
動作確認などは,Logを見たり,AppInspectionのBackground Task Inspectorなどを使うと良いと思います.
自分自身勉強中の身で,誤りもあるかもわかりませんが,公式情報などと並行して参考にしていただければ幸いです.
参考
[1] Android Developers,WorkManager でタスクのスケジュールを設定する,https://developer.android.com/topic/libraries/architecture/workmanager?hl=ja
[2] Android Developers,他の Jetpack ライブラリで Hilt を使用する,https://developer.android.com/training/dependency-injection/hilt-jetpack?hl=ja
[3] M0710Fa,Jetpack ComposeとHiltによるDIのお勉強備忘録,https://qiita.com/m0710fa/items/ee19eac824d240944698
[4] Android Developers,WorkManager のカスタム構成と初期化,https://developer.android.com/topic/libraries/architecture/workmanager/advanced/custom-configuration?authuser=1&hl=ja
[5] Android Developers,WorkManager リリースノート,https://developer.android.com/jetpack/androidx/releases/work?authuser=1&hl=ja#2.6.0-alpha01