WorkManagerとは、すぐ実行しなくてよい処理をAndroid端末で確実に片づけるためのJetpackライブラリです。アプリを閉じても端末を再起動しても、登録したタスクは残り、条件がそろった時点で走ります。公式のリリースノートでは、2026年9月23日に2.12.0が安定版として公開され、同時にminSdkがAPI 24へ引き上げられました(2026年10月時点)。この記事では、なぜ再起動をまたいで復元できるのかという仕組みから、依存追加・Workerの実装・登録・テストまでの動くコード、制約が実行タイミングをどう遅らせるかを押さえたうえで、FCMやサーバー側キューではなくWorkManagerを選ぶ条件まで整理します。
まとめ:WorkManagerとは何か・採用判断と使いどころ
Androidアプリで「アプリが起動していなくても、いつか必ず終わらせたい処理」があるなら、既定の選択肢はWorkManagerです。ログ送信、サーバーとのデータ同期、撮影した画像のアップロード。この種の処理をコルーチンで直接投げると、画面が閉じた時点で消えます。WorkManagerは登録内容を端末内のデータベースへ書き、OSの実行基盤へ委ねる形をとるため、プロセスが死んでも予定が残ります。
一方で、秒単位の即時性が要る処理には向きません。制約や省電力の都合で、実行は数分から数時間ずれます。ユーザーの操作に即応する通知や、サーバー側の都合で今すぐ端末を動かしたい場面は、FCMの高優先度メッセージやフォアグラウンドサービスの領分です。
採用を見送ってよい条件も明確です。処理が画面の生存期間で完結する、端末側でやる必然性がなくサーバーで回せる、実行が15分より短い周期を必要とする。このいずれかに当たるなら、別の道具を選んだほうが構成は簡単になります。判断の分かれ目は「アプリが消えても続きをやる必要があるか」の一点だと考えてください。
実装面では、KotlinならCoroutineWorkerで処理を書き、Constraintsとバックオフを付けて一意処理として登録し、work-testingで実時間を待たずに検証する。この3点をそろえれば、最初の1本は半日かからず動かせます。以降の章でコードごと示します。
WorkManagerとは何か:再起動後も処理を復元する永続化の仕組みと構造
WorkManagerが引き受ける処理と扱わない処理を分ける要件の境目
WorkManagerが対象とするのは、公式ガイドの永続的な処理の章の言い方では「遅延可能で、かつ確実に実行したい」処理です。遅延可能とは、今すぐでなくてよいという意味。確実にとは、アプリが終了しても端末が再起動しても実行の約束が残るという意味です。この2条件がそろって初めて、このライブラリを持ち出す意味が出ます。
逆に、片方しか満たさない処理は対象外になります。画面を表示している間だけ走ればよい通信は、ViewModelのスコープで済ませるほうが素直です。即時性が必要で遅延を許さない処理も、後述するとおり別の仕組みへ回します。メインスレッドから切り離したいだけの処理を全部ここへ寄せると、DBへの書き込みとスケジューリングの往復が無駄になります。
非同期処理そのものの考え方を先に整理しておきたい場合は、同期処理と非同期処理の違いをまとめた解説を読んでおくと、この境目の判断が速くなります。
タスクをデータベースへ保存して端末の再起動後も復元する処理の流れ
確実さの正体は永続化です。enqueueしたWorkRequestは、実行条件・入力データ・リトライ回数を含めて端末内のデータベースへ書き込まれます。メモリ上のキューではないため、プロセスが強制終了されても記録は消えません。
端末を再起動すると、WorkManagerは起動完了のブロードキャストを受けてデータベースを読み直し、未完了のタスクを組み立て直します。この復元があるおかげで、「夜間に同期する」といった予定が再起動をまたいで生き残ります。裏を返せば、登録した覚えのない古いタスクが延々と残り続ける事故も起きうるということです。開発中にIDを変えながら試すと、端末内に重複した予定が積み上がります。
APIレベルごとに内部の実行基盤を切り替えて差異を吸収する構造
WorkManagerは自前でタイマーを持ちません。実際に時刻や条件を見張るのはOS側のジョブ基盤で、ライブラリはその上に載る薄い層です。歴史的にはAPIレベルに応じてJobSchedulerとAlarmManagerを使い分けてきましたが、2.11.0でminSdkがAPI 21からAPI 23へ、2.12.0でさらにAPI 24へ引き上げられ、対象範囲が整理されました。ソースはAndroidXリポジトリのworkディレクトリで公開されており、内部のデータベース定義やスケジューラの実装まで追えます。
実装者から見ると、この構造の意味は2つあります。ひとつは、OS側の制限がそのまま自分のタスクに効いてくること。もうひとつは、端末やOSの版によって実行タイミングの傾向が変わること。同じコードでも、検証端末では数分で走り、別の端末では1時間待たされる場面が出てきます。
| 層 | 担当 | 備考 |
|---|---|---|
| アプリ | Worker実装 | doWorkを書く |
| WorkManager | 永続化と復元 | DBへ保存 |
| OS | 条件監視と起動 | JobScheduler |
2.12系で変わったminSdk 24と実行履歴を記録する新機能の中身
2.12.0の変更点は大きく3つです。1つ目はminSdkがAPI 23からAPI 24(Android 7.0)へ上がったこと。Android 6.0端末をまだ対象に含むアプリは、2.11系にとどめるか対象端末を見直すかの判断が先に要ります。2つ目はandroidx.work:work-analyticsという成果物の追加で、Workごとの開始・終了時刻、実行時間、試行回数を記録して後から問い合わせられるようになりました。3つ目は、Workerの開始・終了・停止・例外と、登録・更新・取り消しといったスケジューリングの出来事を受け取るリスナーがConfiguration.Builderへ加わったことです。
work-analyticsとリスナーはいずれも実験的APIの扱いで、@ExperimentalWorkMetricsApiなどへのオプトインが必要です。本番の監視に組み込むのは、次の安定版でAPIが固まるのを待ってからでも遅くありません。ただ「いつ登録され、何回リトライされ、なぜ止まったか」をアプリ内で記録できるのは、後述する実行されない不具合の調査で効いてきます。
依存追加から登録・観測・テストまでWorkManagerを動かす実装手順
ここからは、ログファイルをサーバーへ送る処理を例に、最初の1本を動かすまでを順に書きます。公式のスタートガイドと同じ流れで、Kotlinとコルーチンを前提にしています。
build.gradle.ktsへのwork-runtime-ktxの2.12系追加
アプリモジュールの依存関係へ、本体のwork-runtime-ktxとテスト用のwork-testingを足します。版番号はリリースノートの最新安定版に合わせてください。2.12系はminSdk 24が前提なので、モジュールのminSdkが23以下だとビルドの段階で止まります。
// app/build.gradle.kts
android {
defaultConfig {
minSdk = 24
}
}
dependencies {
implementation("androidx.work:work-runtime-ktx:2.12.0")
androidTestImplementation("androidx.work:work-testing:2.12.0")
// androidx.test.ext:junit は新規プロジェクトのテンプレートに含まれるものを使う
}
テストは端末上で走るandroidTest側に置く前提です。WorkManagerは既定でアプリ起動時に自動初期化されます。ログレベルを上げたい、Workerの生成を差し替えたいといった理由で初期化を自分で握る場合は、ApplicationにConfiguration.Providerを実装し、マニフェストで既定の初期化を外します。Android Studioの基本操作に不慣れな場合は、Android Studioの使い方とインストール手順をまとめた解説で環境を先に整えておくと迷いません。
CoroutineWorkerで失敗の種類を分けて返すアップロード処理を書く
処理本体はCoroutineWorkerを継承し、doWorkをsuspend関数として書きます。CoroutineWorkerの公式解説にあるとおり、既定ではDispatchers.Defaultで動くため、ファイルや通信を扱う箇所はDispatchers.IOへ切り替えます。戻り値は成功・失敗・再試行の3つで、ここで決めるのは各応答をどの戻り値に対応させるかという分類です。
class UploadLogWorker(
appContext: Context,
params: WorkerParameters
) : CoroutineWorker(appContext, params) {
override suspend fun doWork(): Result = withContext(Dispatchers.IO) {
val path = inputData.getString(KEY_PATH)
?: return@withContext Result.failure()
val file = File(path)
if (!file.exists()) return@withContext Result.failure()
val conn = URL(UPLOAD_URL).openConnection() as HttpURLConnection
try {
conn.requestMethod = "POST"
conn.doOutput = true
conn.setRequestProperty("Content-Type", "text/plain")
file.inputStream().use { input ->
conn.outputStream.use { output -> input.copyTo(output) }
}
when (conn.responseCode) {
in 200..299 -> Result.success(workDataOf(KEY_SIZE to file.length()))
in 500..599 -> Result.retry() // サーバー側の一時障害は再試行
else -> Result.failure() // 401や422は再試行しても通らない
}
} catch (e: IOException) {
Result.retry() // 通信断は再試行
} finally {
conn.disconnect()
}
}
companion object {
const val KEY_PATH = "path"
const val KEY_SIZE = "size"
const val UPLOAD_URL = "https://api.example.com/logs"
}
}
ポイントは、入力には実体でなくファイルパスだけを渡している点と、再試行する失敗としない失敗をwhenで分けている点です。UPLOAD_URLは自分のサーバーの受け口へ置き換えてください。停止要求はコルーチンのキャンセルとして届くので、isStoppedを自分で見張るコードは要りません。
制約とバックオフを付けて一意処理として登録し状態をFlowで観測する
登録側では、通信がある時だけ走る制約、指数バックオフ、一意処理の名前をまとめて指定します。WorkRequestの定義方法を示した公式ページによれば、バックオフの初回待ち時間は10秒(MIN_BACKOFF_MILLIS)を下回れず、何も指定しなければ30秒の指数方式になります。
fun enqueueLogUpload(context: Context, logFile: File) {
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresBatteryNotLow(true)
.build()
val request = OneTimeWorkRequestBuilder<UploadLogWorker>()
.setInputData(workDataOf(UploadLogWorker.KEY_PATH to logFile.absolutePath))
.setConstraints(constraints)
.setBackoffCriteria(BackoffPolicy.EXPONENTIAL, 30, TimeUnit.SECONDS)
.addTag("log-upload")
.build()
WorkManager.getInstance(context).enqueueUniqueWork(
"log-upload",
ExistingWorkPolicy.APPEND_OR_REPLACE,
request
)
}
// 画面側で状態を購読する(lifecycleScope などから呼ぶ)
suspend fun observeLogUpload(context: Context) {
WorkManager.getInstance(context)
.getWorkInfosForUniqueWorkFlow("log-upload")
.collect { infos ->
infos.forEach { Log.d("LogUpload", "${it.id} ${it.state} attempt=${it.runAttemptCount}") }
}
}
APPEND_OR_REPLACEを選んだのは、ログ送信を取りこぼしたくないからです。前段が失敗で終わっていても新しい登録で鎖を張り直せるため、1回の失敗で以降の送信がすべて止まる事態を避けられます。状態の購読結果を画面へ出す場合は、Jetpack Composeの宣言的UIを解説した記事の書き方でcollectAsStateへつなぐと、進捗表示まで短く書けます。
TestListenableWorkerBuilderによる失敗分類の単体検証
work-testingのTestListenableWorkerBuilderを使うと、Workerの中身はWorkManagerを経由せず単体で呼び出して確かめられます。公式のテストガイドでは、制約の充足や周期の経過を手で起こすTestDriverを使う結合テストも紹介されています。まずは失敗の分類が狙いどおりかを単体テストで固めるのが近道です。
@RunWith(AndroidJUnit4::class)
class UploadLogWorkerTest {
@Test
fun パスが無ければ失敗で終わる() {
val context = ApplicationProvider.getApplicationContext<Context>()
val worker = TestListenableWorkerBuilder<UploadLogWorker>(context).build()
val result = runBlocking { worker.doWork() }
assertEquals(ListenableWorker.Result.failure(), result)
}
}
結合テストではWorkManagerTestInitHelperで初期化し、setAllConstraintsMetやsetPeriodDelayMetで実行条件を満たした扱いにします。15分周期の処理も実時間を待たずに検証できます。2.11.0ではTestDriverに停止理由を指定して実行中の処理を止めるstopRunningWorkWithReasonが加わり、中断時の挙動も再現しやすくなりました。
OneTimeとPeriodic・WorkerとCoroutineWorkerの選定基準
OneTimeとPeriodicのWorkRequestを使い分ける基準
登録の入口は2種類です。1回だけ実行するならOneTimeWorkRequest、繰り返すならPeriodicWorkRequestを使います。判断軸は単純で、同じ処理を定期的に走らせ続けたいかどうかで決まります。
ただしPeriodicには外せない制限があります。公式ドキュメントが明記しているとおり、繰り返し間隔の下限はJobSchedulerと同じ15分で、これより短い周期は指定できません。5分ごとの同期を求められた場合、この方式では実現できないため、要件を満たすには別の設計へ切り替える判断が必要です。またPeriodicは「15分ちょうど」を保証するものでもなく、条件がそろわなければ次の窓へ持ち越されます。各周期の終わり寄りで走らせたい場合は、フレックス期間を指定して実行の窓を絞れます。
val req = PeriodicWorkRequestBuilder<SyncWorker>(
15, TimeUnit.MINUTES
).setConstraints(constraints).build()
WorkManager.getInstance(context).enqueueUniquePeriodicWork(
"periodic-sync",
ExistingPeriodicWorkPolicy.UPDATE,
req
)
WorkerとCoroutineWorkerを選び分ける基準
処理本体を書くクラスは4種類あります。Workerは用意されたバックグラウンドスレッドで同期的に走る素朴な形。CoroutineWorkerはsuspend fun doWork()で書け、内部で中断関数をそのまま呼べます。ほかにRxJava向けのRxWorkerと、独自の非同期APIへつなぐListenableWorkerがあります。
Kotlinで新規に書くならCoroutineWorkerが既定です。Retrofitのsuspend関数もRoomの中断クエリもそのまま呼べるうえ、WorkManagerが処理を止めるときにコルーチンのキャンセルとして伝わります。Workerで同じことをやると、停止シグナルを自分でisStoppedで見張る手間を避けられません。コルーチン側の前提はsuspendと構造化並行性を整理した解説で押さえておくと、キャンセルの伝わり方まで一続きで理解できます。
なお2.10.0ではConfiguration.workerCoroutineContextが加わり、CoroutineWorkerを走らせるコンテキストをアプリ全体で指定できるようになりました。テストで差し替えたい場合の入口としても使えます。
| 種別 | 書き方 | 向く場面 |
|---|---|---|
| Worker | 同期のdoWork | 単純な処理 |
| CoroutineWorker | suspendのdoWork | Kotlin全般 |
| RxWorker | Singleを返す | RxJava資産 |
| ListenableWorker | Futureを返す | 独自の非同期 |
入出力Dataの上限を守って大きな値を安全に受け渡す実装作法
Workerとの値のやり取りに使うのはDataですが、受け渡せるサイズには上限がある仕様です。Dataクラスのリファレンスでは、直列化した大きさがMAX_DATA_BYTES(10KB程度)を超えるとIllegalStateExceptionを投げると説明されています。画像そのものやレスポンス全文を詰めると、この壁に当たります。
回避の型は決まっています。実体はファイルやRoomのテーブルへ置き、Dataにはそのパスや主キーだけを渡す。前章のアップロード例がこの形です。チェーンで次のWorkerへ引き継ぐ場合も同じで、識別子を渡してWorker側が読み直す形にします。この設計にしておくと、リトライで再実行されたときにも同じ入力を再現できます。
Constraintsとバックオフを設定して実行時刻と遅延の幅を見積もる手順
5種類のConstraintsで通信量と電池を守る実行条件の絞り方
WorkManagerの実行条件はConstraintsで表します。指定できるのはネットワーク接続、充電中かどうか、バッテリー残量、ストレージ残量、端末がアイドル状態かの5種類です。条件を付けた分だけ実行は後ろへずれますが、ユーザーの通信量やバッテリーを守れます。
実務でよく使うのはネットワークと充電の組み合わせです。大きなファイルの同期ならNetworkType.UNMETEREDで従量課金の回線を避け、さらに充電中に限定して深夜へ寄せる。2.10.0で追加されたsetRequiredNetworkRequestを使えば、回線種別より細かい要件も指定できます。
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED)
.setRequiresCharging(true)
.build()
Dozeとスタンバイバケットにより実行の遅延が長引く仕組みを解く
制約を満たしても、すぐ走るとは限りません。端末が動かされずに放置されるとOSはDozeへ入り、ジョブの実行を定期的な保守窓へまとめます。DozeとApp Standbyの公式解説にあるとおり、この窓は時間が経つほど間隔が開くため、放置された端末では待ち時間が数時間規模になります。
さらにアプリごとのスタンバイバケットが効きます。ユーザーが触らないアプリほど低いバケットへ落ち、ジョブの実行枠が絞られます。「テスト端末では走るのに、実機の利用者からは同期されないと言われる」という報告の多くは、この2つが原因です。OS側の省電力の仕組みはDozeモードの動作とDeep/Lightの違いを解説した記事で押さえておくと、遅延の見積もりが現実的になります。
加えて、Android 15以降はdataSync型のフォアグラウンドサービスに24時間あたり合計6時間という上限が入り、Android 16以降はフォアグラウンドサービスから開始したジョブもランタイムクォータの対象になりました。対象SDKを上げるたびにバックグラウンドの扱いは変わるため、Android 17の変更点とtargetSdk 37で必要な対応をまとめた記事も合わせて確認しておくと、次の版で慌てずに済みます。長時間の同期を常駐サービスで押し通す設計は、年々通りにくくなっています。
バックオフとリトライの既定値をエラー別に決める判断基準の定め方
Workerの戻り値は成功・失敗・再試行の3つです。Result.retry()を返すとWorkManagerがバックオフ方針に従って再登録します。方針は指数と線形の2種類で、初回の待ち時間の下限は10秒、既定値は30秒の指数方式です。
ここで判断が要るのは、どのエラーを再試行に回すかです。ネットワーク断や5xxは再試行の価値がありますが、401や422のようにサーバー側が明確に拒否した応答を再試行へ回すと、無駄なリクエストを繰り返します。失敗の分類を先に決め、再試行しない側はResult.failure()で打ち切る。この切り分けを省くと、電池と通信量だけが減っていきます。前章のUploadLogWorkerは、この分類をそのままコードにした例です。
| 失敗の種類 | 返す値 | 理由 |
|---|---|---|
| 通信断 | retry | 回線復帰で通る |
| 5xx応答 | retry | 一時障害の可能性 |
| 401や422 | failure | 再送しても同じ |
| 入力の欠落 | failure | 登録側の不具合 |
ExistingWorkPolicyとチェーンで多重起動を防ぎ進行状況を追う設定方法
ExistingWorkPolicyで重複登録を捌く4つの選択肢
同じ処理を二重に登録してしまう事故は、名前付きの一意処理で防げます。Workの管理方法を示した公式ページのとおり、enqueueUniqueWorkに名前と方針を渡すと、同名の処理が既にある場合の振る舞いを指定できます。方針は既存を捨てて置き換える、既存を残して今回を捨てる、後ろへ追加する、失敗時のみ置き換えて追加する、の4つです。
選び方は処理の性質に応じた判断です。同期のように最新の1回だけ走ればよいものは既存を残す方針、アップロードのように取りこぼしを許さないものは追加する方針が合います。定期処理側にも同種の指定があり、間隔や制約を変えたいときはExistingPeriodicWorkPolicy.UPDATEを選べば、既存の予定を壊さずに設定だけ差し替えられます。
| 方針 | 既存がある時 | 向く処理 |
|---|---|---|
| KEEP | 今回を捨てる | 定期同期 |
| REPLACE | 既存を捨てる | 設定の反映 |
| APPEND | 後ろへ連結 | 順次アップロード |
| APPEND_OR_REPLACE | 失敗なら張り直す | ログ送信 |
チェーンとWorkInfoで複数処理の進行状況を追う組み立て方
複数の処理を順番に走らせたい場合は、beginWithとthenで連結します。前段が成功したときだけ次が走り、途中で失敗すれば以降は動きません。並列に走らせたい処理はリストで渡し、そろってから次段へ進む形も書けます。
WorkManager.getInstance(context)
.beginWith(listOf(compressA, compressB)) // 並列に圧縮
.then(uploadAll) // そろってから送信
.then(cleanup) // 成功時だけ後片づけ
.enqueue()
進行状況の観測にはWorkInfoを使います。IDやタグ、一意処理の名前を指定して、キュー待ち・実行中・成功・失敗といった状態を受け取れます。画面へ進捗を出す場合はFlowで購読し、setProgressでWorker側から中間値を渡す形が扱いやすい構成です。
優先処理と10分を超える長時間処理で通知を出す条件と実装方法
今すぐ始めたい処理にはsetExpeditedがあります。システムの割り当てが尽きたときの振る舞いをOutOfQuotaPolicyで指定する必要があり、通常のタスクへ格下げするか、要求ごと捨てるかを選びます。無条件に速くなる仕組みではない点は押さえておいてください。
実行が10分を超えうる処理は、長時間実行として扱います。長時間実行のWorkerを扱う公式ガイドが示すとおり、setForegroundで通知を提示し、フォアグラウンドサービスとして走らせる形です。通知が出る以上ユーザーからは見えるため、体験を壊さない文言と進捗表示を用意しておく。10分の目安を超えたまま通知を出さない実装は、途中で停止されます。Android 14以降はマニフェストでサービス種別(dataSyncなど)の宣言も求められます。
JobScheduler・AlarmManager・FCM対WorkManagerの採否
JobSchedulerとAlarmManagerとの比較で決める採用の3条件
OS標準のJobSchedulerでも、条件付きのジョブ登録はできます。それでもWorkManagerを選ぶ理由は3つに絞れます。ひとつはAPIレベルごとの差異を吸収してくれること。ふたつ目はチェーンや一意処理といった運用上の道具がそろっていること。3つ目はテスト用の実装が公式に提供され、実行タイミングを操作した検証が書けることです。
AlarmManagerとの線引きはさらに明快です。あちらは「指定時刻にユーザーへ知らせる」ための仕組みで、目覚ましやカレンダー通知が本来の用途になります。時刻の正確さが要件でないなら、選ぶ理由はありません。逆に「毎朝7時ちょうど」を外せない要件なら、WorkManagerでは満たせないと判断してください。
| 手段 | 得意な要件 | WorkManagerとの関係 |
|---|---|---|
| WorkManager | 遅延可で確実に実行 | 既定の選択肢 |
| JobScheduler | OS標準の条件実行 | 内部で使われる土台 |
| AlarmManager | 時刻の正確さ | 用途が別 |
| FCM | サーバー起点の即時性 | 受信後に登録で併用 |
即時性が要る処理をFCMと常駐サービスへ寄せる要件の境界を判断する
サーバー側の都合で今すぐ端末を動かしたい場合は、WorkManagerの出番ではありません。FCMによるプッシュ配信の仕組みを解説した記事にあるとおり、メッセージを受けた時点で処理を起こす形が筋です。実務では、FCMで通知を受け取り、その中で重い処理をWorkManagerへ登録する組み合わせがよく使われます。
音楽再生や位置情報の追跡のように、ユーザーが認識したうえで継続してほしい処理はフォアグラウンドサービスの領分です。ただしAndroid 14でサービス種別の宣言が強制され、Android 15以降はdataSync型に合計6時間の上限が入りました。「バックグラウンドで長時間動かしたいから常駐サービスにする」という発想は、通らなくなってきています。
端末側ジョブを見送りサーバー側キューへ移してよい処理の条件と判断軸
そもそも端末で実行する必然性を疑う視点も持っておきたいところです。集計、変換、外部APIへの転送。この種の処理は、端末の電池と回線と省電力仕様に左右されるより、サーバー側で回したほうが確実に終わります。Cloud Tasksのリトライと課金を整理した解説のような非同期キューを使えば、実行保証もリトライも運用側で握れます。
端末側に残すべきなのは、端末にしかないデータを扱う処理だけだと考えると整理しやすくなります。カメラで撮った画像、センサーのログ、オフライン中にローカルへ溜めた入力。これらをサーバーへ届けるまでがWorkManagerの仕事で、届いた後の加工はサーバーの仕事です。この線を引かずに端末側へ処理を積むと、動かない端末が出るたびに再現できない不具合として跳ね返ってきます。
実行されないWorkを調べるadbコマンドと失敗パターンの切り分け手順
15分未満の定期実行を狙って処理の連鎖が破綻する典型パターン
最も多い詰まり方は、短い周期の定期実行を無理に作ろうとする設計です。下限が15分だと分かった後、OneTimeWorkRequestに遅延を付けて自分自身を再登録する回避策へ流れる例をよく見ます。この方法は書けてしまいますが、Dozeとバケット制御で間隔が伸びるため、狙った周期にはなりません。
加えて、再登録が失敗した瞬間に連鎖が止まる弱さがあります。一度切れると次が二度と来ないため、障害の発見も遅れる。要件が5分周期の同期なら、サーバー側からのプッシュで起こす設計へ組み替えるか、そもそも画面を開いたときに同期する方式へ寄せる判断をおすすめします。
端末メーカー独自の省電力で処理が止まる事象の原因切り分けの手順
「特定メーカーの端末だけ同期されない」という報告は、この分野の定番です。標準のDozeに加えて、独自の電池管理でバックグラウンド実行を打ち切る実装を載せている端末があり、アプリを電池の管理対象から外してもらわないと動きません。
切り分けの手順は決めておくと早くなります。まずADBでジョブの登録状態を確認し、登録自体が消えているのか、登録は残っていて実行されていないのかを分ける。前者ならアプリ側の再登録漏れかメーカー独自の打ち切り、後者なら制約の未充足かバケットの影響です。この2つを混ぜたまま調べると、原因にたどり着けません。
adb shell dumpsys jobscheduler | grep -A5 パッケージ名
adb shell dumpsys deviceidle get deep
adb shell am get-standby-bucket パッケージ名
診断ブロードキャストとBackground Task Inspectorでの登録確認
登録状態のもう一つの確かめ方が、WorkManager自身の診断機能です。公式のデバッグ手順では、デバッグビルドへ次のブロードキャストを送ると、直近24時間に完了したWork、実行中のWork、予定されているWorkがログへ出力されると説明されています。ログレベルはConfiguration.Builder().setMinimumLoggingLevel(Log.DEBUG)で上げておき、WM-で始まるタグを追います。
adb shell am broadcast -a "androidx.work.diagnostics.REQUEST_DIAGNOSTICS" -p "パッケージ名"
adb logcat -s WM-DiagnosticsWrkr
画面で追いたい場合は、Android StudioのApp InspectionにあるBackground Task Inspectorが使えます。Workごとの状態、制約、チェーンのつながりをグラフで確認でき、登録が消えたのか実行を待たされているのかを目で見分けられる点が利点です。2.12系のwork-analyticsが安定したら、同じ情報を利用者の端末からも集められるようになります。
バックグラウンド処理を外部へ任せる際に定める受け入れ基準の項目
バックグラウンド処理は、コードを読むだけでは正しさを判定しにくい領域です。実行されないという不具合は再現に時間がかかり、検証端末では出ないことも珍しくありません。だから外部へ委託する場合は、受け入れ基準を契約時に文章で決めておく必要があります。
基準は3つに絞れます。制約と周期の設計根拠が文書化されていること、リトライする失敗としない失敗の分類が定義されていること、そしてWorkManagerのテスト用実装を使った検証が成果物に含まれていること。Androidアプリ開発の受託では既存アプリの同期処理の棚卸しから対応しており、省電力仕様の変更に伴う実行漏れの調査や、2.12系へのminSdk引き上げに伴う影響確認、サーバー側キューへの移し替えも含めて相談できます。
よくある質問
WorkManagerの導入でよく挙がる質問を、実装と発注の両面から5つ整理します。
WorkManagerで指定できる最短の繰り返し間隔は?
PeriodicWorkRequestの繰り返し間隔は15分が下限です。これより短い値を渡しても15分へ丸められます。さらに実行タイミングは制約と省電力の状態に左右されるため、15分ちょうどで走る保証もありません。短い周期が要件になる場合は、サーバー側からのプッシュや画面起動時の同期へ設計を寄せてください。
WorkerとCoroutineWorkerはどちらを使うべきですか?
Kotlinで新しく書くならCoroutineWorkerを選びます。中断関数をそのまま呼べるうえ、WorkManagerからの停止要求がコルーチンのキャンセルとして伝わるためです。Workerを使う場面は、Javaで書かれた既存コードへ組み込む場合や、単純な同期処理で依存を増やしたくない場合に限られます。
再起動してもタスクは残りますか?
再起動後もタスクは残る仕様です。登録内容は端末内のデータベースへ保存され、起動完了を受けて未完了のタスクが復元されます。ただし、アプリのデータを消去した場合や、ユーザーがアプリを強制停止した直後は挙動が変わります。強制停止後は次にアプリが起動されるまで復帰しない点を、仕様として説明できるようにしておいてください。
WorkManagerのテストはどう書きますか?
Workerの中身はTestListenableWorkerBuilderで単体に生成し、doWorkの戻り値を確かめます。登録から実行までを確かめる結合テストはwork-testingのWorkManagerTestInitHelperで初期化し、TestDriverで制約の充足や遅延の経過を手動で起こします。実時間を待たずに実行条件を満たせるため、15分周期の処理も即座に検証できる仕組みです。
導入に必要な最小のAPIレベルは?
2026年9月23日公開の2.12.0からminSdkがAPI 24(Android 7.0)へ引き上げられました。2.11系はAPI 23、2.10系はAPI 21から使えます。古い端末を対象に含むアプリでは、版を上げる前に対応範囲を確認してください。2026年10月時点の最新安定版は2.12.0で、最新の版番号はリリースノートで確かめるのが確実です。
関連記事
- Jetpack Composeとは?宣言的UIの仕組みとBOM運用・採用判断を解説:進捗表示を組むUI層の前提です。
- Kotlinとは?Javaとの違いとK2コンパイラ・2.4系の現在地:Worker実装の言語側の土台になります。
- Androidアプリ開発の言語はKotlinとJavaのどちらか?選定基準と環境:既存資産との兼ね合いを判断できます。
- 非同期処理とは?同期処理との違いから実装方式まで解説:スケジューリングとの違いを整理できます。
- Androidアプリ開発とは?できること・開発の流れ・iOSとの違い:発注側の全体像を補います。