バックグラウンド処理の制約 〜ケーススタディで学ぶ失敗と対策〜

※ChatGPTを使用して記事を作成しています。

Android 開発において、バックグラウンド処理は避けて通れません。

API通信、データベース同期、通知スケジュール、ファイルアップロードなど、多くの機能が裏で動いています。

しかし「制約」を軽視すると、思わぬトラブルにつながります。

今回は実際の失敗例と修正版コードを交えて、バックグラウンド処理の怖さと対処法をまとめます。

ケーススタディ1:Service暴走事件

失敗例

「アプリが終了しても動いてほしい」と考えて Service を常駐させた実装。

class MyBackgroundService : Service() {
    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
        Thread {
            while (true) {
                // 定期的にAPI呼び出し
                fetchDataFromApi()
                Thread.sleep(60_000)
            }
        }.start()
        return START_STICKY
    }

    override fun onBind(intent: Intent?): IBinder? = null
}

🔴 問題点

  • 無限ループ+Threadでバッテリーを消耗
  • Android 8.0以降ではバックグラウンド制限により強制終了される
  • START_STICKY を乱用して再起動を繰り返す

修正版

WorkManager を利用し、制約に従った形に修正。

class SyncWorker(appContext: Context, workerParams: WorkerParameters) :
    CoroutineWorker(appContext, workerParams) {
    override suspend fun doWork(): Result {
        fetchDataFromApi()
        return Result.success()
    }
}

// 定期実行の登録
val request = PeriodicWorkRequestBuilder<SyncWorker>(15, TimeUnit.MINUTES)
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()
    )
    .build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "SyncWork",
    ExistingPeriodicWorkPolicy.KEEP,
    request
)

✅ WorkManagerは

  • バックグラウンド制約に対応済み
  • OSに合わせて最適化された方法で実行
  • 再起動後もスケジュール維持

ケーススタディ2:AlarmManagerを信じすぎた話

失敗例

「午前9時に必ず通知を出す」として AlarmManager を利用。

val intent = Intent(context, MyReceiver::class.java)
val pendingIntent = PendingIntent.getBroadcast(
    context, 0, intent, PendingIntent.FLAG_IMMUTABLE
)

val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.setExact(
    AlarmManager.RTC_WAKEUP,
    Calendar.getInstance().apply {
        set(Calendar.HOUR_OF_DAY, 9)
        set(Calendar.MINUTE, 0)
    }.timeInMillis,
    pendingIntent
)

🔴 問題点

  • Dozeモード中は正確に動かない
  • バッテリー最適化の影響で端末によって挙動が違う
  • 「通知が来ない」とユーザーからクレーム

修正版

WorkManager を使って「9時頃に通知する」設計に変更。

class NotificationWorker(appContext: Context, params: WorkerParameters) :
    CoroutineWorker(appContext, params) {
    override suspend fun doWork(): Result {
        showNotification("お知らせ", "朝の時間です!")
        return Result.success()
    }
}

val request = PeriodicWorkRequestBuilder<NotificationWorker>(24, TimeUnit.HOURS)
    .setInitialDelay(calculateDelayUntil9AM(), TimeUnit.MILLISECONDS)
    .build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "DailyNotification",
    ExistingPeriodicWorkPolicy.REPLACE,
    request
)

✅ 「正確な9時」は保証されないが、制約下でも確実に動作する。

ケーススタディ3:ForegroundServiceを忘れた事件

失敗例

大きなファイルをダウンロードする処理をバックグラウンドで開始。

fun downloadFile(url: String) {
    GlobalScope.launch {
        val data = api.download(url)
        saveToFile(data)
    }
}

🔴 問題点

  • OSによっては途中でプロセスが落とされる
  • ユーザーに進行状況が見えない
  • ダウンロード失敗の報告が多数

修正版

ForegroundService + Notification で修正。

class DownloadService : LifecycleService() {
    override fun onCreate() {
        super.onCreate()
        val notification = NotificationCompat.Builder(this, "download_channel")
            .setContentTitle("ダウンロード中")
            .setSmallIcon(R.drawable.ic_download)
            .build()
        startForeground(1, notification)
    }

    fun startDownload(url: String) {
        lifecycleScope.launch {
            val data = api.download(url)
            saveToFile(data)
            stopForeground(STOP_FOREGROUND_REMOVE)
        }
    }
}

✅ 長時間処理はForegroundServiceに委ねるべき。

学びのまとめ

  • 無限ループや独自ThreadはNG → OSにKILLされる
  • AlarmManagerは信じすぎない → Dozeで止まる
  • 長時間処理はForegroundService必須
  • 最終的にはWorkManagerに寄せるのが安定

おわりに

バックグラウンド処理は、見た目には地味ですがアプリの信頼性を大きく左右します。

「動いて当然」と思っていたコードが、OSや端末によって止まるのは日常茶飯事です。

今回のケーススタディを通じて、「制約を理解した上で設計する」ことの重要性を実感していただけたら幸いです。

タイトルとURLをコピーしました