※ChatGPTを使用して記事を作成しています。
「アプリを閉じても裏で動いてほしい」
――Androidアプリ開発者なら誰しもが考えることです。
昔はサービスを起動してしまえば何でもできました。
音楽再生も、定期同期も、通知送信も、全部自由。
しかし年を重ねるごとに、Androidはバックグラウンド処理に対して厳しい制約を課すようになりました。
今回は、私が「バックグラウンド処理」に振り回され、幾度となく不具合報告とクレームを受け、ようやく理解した教訓をまとめます。
背景:バックグラウンドで同期したい
数年前、とあるアプリで「定期的にサーバーからデータを取得し、ユーザーに通知する」機能を実装することになりました。
当時の私の発想はシンプルです。
- サービスを起動する
- タイマーを使って数時間おきにリクエストを投げる
- 新しいデータがあれば通知する
コードもこんな具合でした。
public class SyncService extends Service {
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
new Thread(() -> {
while (true) {
fetchDataFromServer();
try {
Thread.sleep(1000 * 60 * 60); // 1時間ごと
} catch (InterruptedException e) {
break;
}
}
}).start();
return START_STICKY;
}
}
これで「サービスを殺さなければ」動き続けます。
ユーザーも満足、私も満足。
――のはずでした。
Androidの進化と制約の増加
ところが、Android 6.0 以降からバッテリー最適化 (Doze モード) が導入され、さらに8.0 でバックグラウンド実行制限が課されました。
結果、この実装は全く動かなくなりました。
- サービスが数分で強制終了
- スリープ時はThread.sleep中に止められる
- 期待したタイミングで同期されない
ユーザーからは「通知が届かない」「データが更新されない」という苦情が殺到。
私は「え、昨日まで動いてたのに!」と頭を抱えました。
失敗談1:BroadcastReceiverが呼ばれない
最初に試したのは、AlarmManagerで定期的にBroadcastを投げ、それを受け取って処理する方法です。
AlarmManager am = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
PendingIntent pi = PendingIntent.getBroadcast(context, 0,
new Intent(context, SyncReceiver.class), 0);
am.setRepeating(AlarmManager.RTC_WAKEUP,
System.currentTimeMillis(),
1000 * 60 * 60,
pi);
しかし Android 6.0 以降では Dozeモード中はアラームが遅延 され、全く思った通りに動きませんでした。
ユーザーが「朝起きたら通知が来ていない」と怒ってアンインストール。
この時点で「バックグラウンドは好き勝手できない」ことを初めて痛感しました。
失敗談2:JobSchedulerを過信した
次に導入したのがJobScheduler
。
「これがGoogle推奨なんだから間違いない」と思い、以下のように設定しました。
JobInfo job = new JobInfo.Builder(1, new ComponentName(context, SyncJob.class))
.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY)
.setPeriodic(1000 * 60 * 60) // 1時間ごと
.build();
JobScheduler scheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
scheduler.schedule(job);
ところが……
端末によって挙動がまちまち。
特に中華系メーカーの独自カスタムROMでは、バッテリー節約のために勝手にジョブがキャンセルされることが頻発しました。
QAチームからは「テスト端末によって動いたり動かなかったりして不安定すぎる」と指摘され、プロジェクト内で炎上。
失敗談3:Foreground Serviceで解決?→却下
「じゃあ常にForeground Serviceにすればいいじゃん」と考え、通知を出し続ける実装にしました。
Notification notification = new NotificationCompat.Builder(this, "sync_channel")
.setContentTitle("同期中")
.setContentText("バックグラウンドでデータを取得しています")
.setSmallIcon(R.drawable.ic_sync)
.build();
startForeground(1, notification);
これなら絶対に殺されません。
しかし――
- ユーザーから「通知が常に出てて邪魔」とクレーム
- Google Playの審査で「不要なForeground Service」と警告
- 結局リジェクトされる
結果、またやり直し。
WorkManagerに辿り着く
最終的に辿り着いたのはWorkManager
でした。
Google公式が推奨している「バックグラウンド処理の最終回答」です。
val workRequest =
PeriodicWorkRequestBuilder<SyncWorker>(1, TimeUnit.HOURS)
.setConstraints(
Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.build()
)
.build()
WorkManager.getInstance(context).enqueue(workRequest)
これなら端末ごとの制約を吸収してくれる……はずでした。
失敗談4:WorkManagerでも思い通りにならない
しかしWorkManagerにも罠がありました。
- 最低15分間隔 → 1分おきにチェックしたい要件では使えない
- 端末メーカー独自の省電力機能に潰される場合もある
- アプリを強制終了するとタスクもキャンセルされる
結局、ユーザーから「通知が遅れる」「アプリを閉じたら動かない」と再び報告が来ました。
「バックグラウンドは完全に制御できない」
この現実を受け入れるしかないことに気づいた瞬間でした。
学んだこと
1. バックグラウンドは「保証できない」
- Androidはユーザーのバッテリーを守るのが最優先
- 「必ず動く」は幻想。あくまで「動くかもしれない」
2. ユースケースを考え直す
- 本当にバックグラウンドで必要か?
- ユーザー操作時にまとめて更新できないか?
- サーバー側プッシュ通知に任せられないか?
3. Google推奨を使う
- AlarmManagerやサービス直叩きは時代遅れ
- WorkManagerを基本に、どうしても必要ならForeground Service
4. ユーザーに説明する
- 省電力機能によって通知が遅れる可能性を伝える
- 設定画面で「バックグラウンド動作を許可」する方法を案内する
結果:設計を根本から変えた
最終的にそのアプリでは「端末で定期同期する」のを諦め、サーバーからのプッシュ通知に切り替えました。
- サーバーが新しいデータを検知 → FCMで通知
- アプリは通知を受け取ったタイミングでAPIを叩く
- ユーザーにとっては「常に新鮮なデータが届く」体験
結果、バッテリー消費も減り、レビューも改善しました。
教訓
- Androidのバックグラウンド制約は年々厳しくなる一方
- 「昔できたこと」が今はできない
- 技術でゴリ押しするのではなく、設計で回避するのが正解
まとめ
私が学んだ最大の教訓はこれです。
「バックグラウンドで好き勝手に動かそうとするな」
アプリはユーザーの端末に居候している存在。
バッテリーやパフォーマンスを犠牲にしてまで自分の処理を優先することは許されません。
今では、バックグラウンド処理を考えるとき、まず 「本当に必要か?」「サーバー側でできないか?」 を自問するようになりました。