本文へスキップ
すべての記事

2026年の信頼できるAndroidリマインダー:WorkManager、正確なアラーム、そして新しいバッテリールール

2026年のAndroidで本当に発火するリマインダーを出荷する方法 — WorkManager対AlarmManager、SCHEDULE_EXACT_ALARM、POST_NOTIFICATIONS、そして今なお噛みついてくるOEM特有のクセ。

MFKAPPS 2 分で読めます

Androidのリマインダーアプリは、リマインダーが表示されて初めて機能する。当たり前に聞こえる — 実際に出荷するまでは。そこであなたは、ドーズモード、アプリスタンバイバケット(app standby buckets)、OEMのバッテリー最適化機能、SCHEDULE_EXACT_ALARM権限、POST_NOTIFICATIONSランタイム権限、フォアグラウンドサービスの制限、そしてどれだけ正しく書いても、SamsungとXiaomiが「バッテリー節約」のためにスケジュールされた処理を静かに殺してくれるという、あの愛おしい事実に気づくことになる。

私はHydrameという、「ちょうどいいタイミングで、そっとリマインドしてほしい」ということだけを価値提案にした水分補給トラッキングアプリを作った。Androidのリマインダースタックを信頼できるものへとねじ伏せるために、認めたくないほどの時間を費やしてきた。この記事は、ついに機能するようになったプレイブックの2026年版だ。

リマインダー問題を一段落で

Androidはバックグラウンド処理を殺すことに積極的で、それは当然でもある — 野放しにすれば、あらゆるアプリが数分おきに無線をたたき起こし、あなたのバッテリーは昼食までに尽きてしまう。正当なリマインダーアプリにとっての結末は、バージョンやOEMスキンごとに微妙に異なる挙動をする、重なり合ったAPIの積み重ねだ。仕事は、あなたの「正確でなければならない」レベルに合った適切なプリミティブを選び、残りは実機でテストすることだと受け入れることにある。

2026年に重要なプリミティブは2つだ。

  • WorkManager — 不正確で先延ばし可能な処理向け。定期的なチェック、「次の1時間のどこかでリマインドして」といったユースケース。バッテリーを意識し、まとめられ、再起動後でも実行される。
  • AlarmManager.setExactAndAllowWhileIdle — 特定の時刻に正確にユーザーに見える通知を出すため。高コストなほう。控えめに使い、「15分ずれる」と機能が壊れて感じられる場合にのみ使う。

リマインダーがきっかり19:30に発火しなければならないなら、正確なアラームが必要だ。「19:30あたりのどこか」でよければWorkManagerを使う。ほとんどのアプリはデフォルトで間違ったほうを選んでいる。

正しく扱わなければならない3つの権限

2026年、3つのランタイム/インストール権限が、ユーザーがそもそもリマインダーを見られるかどうかを決める。

  1. POST_NOTIFICATIONS(ランタイム、Android 13+)。これがないと、通知は文字通り表示されない。初回起動時ではなく、適切な瞬間に求めること。
  2. SCHEDULE_EXACT_ALARM(Android 12+、13+で制限あり)。setExactAndAllowWhileIdleに必要。Android 14+では、ユーザーがシステム設定で明示的に切り替えて初めて付与される。アプリはユーザーをそこへ送らなければならない。
  3. USE_EXACT_ALARM(Android 13+、特別用途)。正確なアラームが中核となるアプリ(目覚まし時計、カレンダーのリマインダー、水分補給)向け。Playは正当な理由を求める。

Hydrameのマニフェストはこうなっている。

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.USE_EXACT_ALARM" />
<uses-permission android:name="android.permission.SCHEDULE_EXACT_ALARM" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />

RECEIVE_BOOT_COMPLETEDは、ほとんどの人が忘れるものだ — これがないと、スケジュールしたアラームは次の再起動で消える。ACTION_BOOT_COMPLETED上のBroadcastReceiverから再スケジュールすること。

POST_NOTIFICATIONSを適切な瞬間に求める

初回起動時に求めてはいけない。価値に対応する瞬間、つまりユーザーが最初のリマインダーを有効にするまさにそのときに求めれば、承諾率は劇的に高くなる。実際のフローはこうだ。

@Composable
fun EnableReminderButton(onEnabled: () -> Unit) {
    val context = LocalContext.current
    val launcher = rememberLauncherForActivityResult(
        contract = ActivityResultContracts.RequestPermission(),
    ) { granted ->
        if (granted) onEnabled()
    }

    FilledButton(onClick = {
        if (Build.VERSION.SDK_INT >= 33) {
            val state = ContextCompat.checkSelfPermission(
                context,
                Manifest.permission.POST_NOTIFICATIONS,
            )
            if (state == PackageManager.PERMISSION_GRANTED) onEnabled()
            else launcher.launch(Manifest.permission.POST_NOTIFICATIONS)
        } else {
            onEnabled()
        }
    }) { Text("Enable reminders") }
}

私が必ずやることがもう2つある。

  • セカンドチャンスの理由説明。 ユーザーが拒否した場合、すぐに再度求めてはいけない。次にリマインダーを有効にしようとしたとき、アプリの設定画面を開くボタンを添えた小さなインラインメッセージを表示し、ユーザー自身で切り替えられるようにする。
  • 「今後表示しない」を伴う拒否状態を検出するには、最後の拒否をDataStoreに記憶しておく — shouldShowRequestPermissionRationaleだけでは、この用途には信頼できない。

不正確:WorkManagerの定期処理

「午前中のどこか」で許容される種類のリマインダーには、WorkManagerが退屈ながら正しい答えだ。

class DailyHydrationCheckWorker(
    appContext: Context,
    params: WorkerParameters,
) : CoroutineWorker(appContext, params) {

    override suspend fun doWork(): Result {
        scheduleTodayReminders()
        return Result.success()
    }
}

fun scheduleDailyHydrationCheck(context: Context) {
    val request = PeriodicWorkRequestBuilder<DailyHydrationCheckWorker>(
        repeatInterval = 1, repeatIntervalTimeUnit = TimeUnit.DAYS,
    )
        .setConstraints(
            Constraints.Builder()
                .setRequiresBatteryNotLow(true)
                .build(),
        )
        .setInitialDelay(secondsUntil(6, 0).toLong(), TimeUnit.SECONDS)
        .build()

    WorkManager.getInstance(context).enqueueUniquePeriodicWork(
        "daily_hydration_check",
        ExistingPeriodicWorkPolicy.UPDATE,
        request,
    )
}

人を噛むポイントがいくつかある。

  • 15分の最小間隔。 PeriodicWorkRequestは、何を頼もうと15分より頻繁には実行されない。15分未満のスケジューリングには、自分自身を連鎖させるワンタイム処理を使う。
  • 正確なタイミングはない。 WorkManagerはある時間枠内でスケジュールする。「8:00:00きっかり」が必要なら、これは間違ったプリミティブだ。
  • Battery-not-lowはユーザーにもシステムにも優しい。そうしない理由がない限りオンにすること。

正確:AlarmManager.setExactAndAllowWhileIdle

リマインダーが特定の瞬間に発火しなければならないとき — たとえばユーザーが「19:30にリマインドして」と言い、本気でそう思っているとき — 正確なアラームが必要だ。パターンはこうだ。

fun scheduleExactReminder(
    context: Context,
    triggerAtMillis: Long,
    requestCode: Int,
) {
    val am = context.getSystemService(AlarmManager::class.java)

    if (Build.VERSION.SDK_INT >= 31 && !am.canScheduleExactAlarms()) {
        // Send the user to the system page to grant the permission.
        context.startActivity(
            Intent(Settings.ACTION_REQUEST_SCHEDULE_EXACT_ALARM)
                .setData(Uri.parse("package:${context.packageName}"))
                .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK),
        )
        return
    }

    val intent = Intent(context, ReminderReceiver::class.java)
        .putExtra("requestCode", requestCode)
    val pending = PendingIntent.getBroadcast(
        context, requestCode, intent,
        PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE,
    )

    am.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, triggerAtMillis, pending)
}

setExactAndAllowWhileIdleは、ドーズを尊重するほうだ。代わりにsetAlarmClockを使うのは、アプリが本当に目覚まし時計である場合だけにすること — システムUIに次のアラームのチップを表示し、最高優先度を得る。

重い瞬間のためのレシーバー+フォアグラウンドサービス

アラームが発火すると、通知を出すために数秒のウェイクタイムが与えられる。単純な「ピン!」というリマインダーなら、それで十分だ。

class ReminderReceiver : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        // Post the notification synchronously.
        val nm = context.getSystemService(NotificationManager::class.java)
        nm.notify(
            intent.getIntExtra("requestCode", 0),
            buildHydrationNotification(context),
        )
        // Re-schedule the next one.
        scheduleNext(context)
    }
}

アラーム時に本当の処理(同期、ダウンロード)をする必要があるなら、ただちにフォアグラウンドサービスへ昇格させること。小賢しくやろうとしないこと — レシーバー内で手間取れば、システムは一瞬であなたを取り消す。

再起動とタイムゾーンの変更

2つのイベントが、素朴な実装を静かに壊す。

  • 再起動。 アラームは消去される。ACTION_BOOT_COMPLETEDをリッスンし、永続化した状態から再スケジュールする。
  • タイムゾーンの変更。 午前7時のリマインダーは、ユーザーが移動する前は現地時間の7時に、移動した後はイスタンブール時間の7時に発火すべきではない。ACTION_TIMEZONE_CHANGEDをリッスンして再計算する。

1つのレシーバーで両方を扱える。

<receiver android:name=".BootAndTimezoneReceiver" android:exported="false">
  <intent-filter>
    <action android:name="android.intent.action.BOOT_COMPLETED"/>
    <action android:name="android.intent.action.TIMEZONE_CHANGED"/>
    <action android:name="android.intent.action.MY_PACKAGE_REPLACED"/>
  </intent-filter>
</receiver>

MY_PACKAGE_REPLACEDは、アプリ更新が発火するものだ — コードは新鮮だが、アラームは消えている。そこでも再スケジュールすること。

OEMの最適化機能 — 静かな殺し屋たち

2026年になっても、残る最悪の犯人は依然としてSamsungの「スリープ中のアプリ」リストで、Xiaomiの自動起動とバッテリーセーバーがすぐ後ろに続く。実用的な防御策が2つある。

  1. アプリ内診断バナー。 ユーザーがリマインダーを有効にしているのに24時間通知が発火していないなら、正しいOEM設定ページへの「これを修正」ディープリンクを添えた小さなバナーを出す。Stack OverflowとDon’t Kill My Appプロジェクトがこれらのディープリンクを最新に保っている。ローカルにキャッシュしておくこと。
  2. バッテリーのホワイトリストを性急に求めないこと。 ユーザーを怖がらせるし、Playも渋い顔をする。すでにリマインダーの取りこぼしを経験したユーザーのために取っておくこと。

最終的に機能したパターン

Hydrameで3回のイテレーションを経て、ついに信頼できると感じたアーキテクチャはこうだ。

  • 1つの定期WorkManagerジョブが毎晩実行される。ユーザーのスケジュールを読み、次の24時間分の正確なアラームを挿入する。
  • 各正確なアラームBroadcastReceiver経由で通知を出す。レシーバーは愚直だ — DB呼び出しなし、ネットワークなし。
  • 再起動+タイムゾーン変更+アプリ更新のレシーバーが、スケジュールを再作成するために同じ夜間ジョブを手動でトリガーする。
  • ハートビートメトリクス(DataStoreに保存された、最後に成功した通知のタイムスタンプ)が、アプリ内の「リマインダーがスマホに殺されているかもしれません」バナーを動かす。

これは、最初の直感(「WorkManagerだけ使え」)を信じていたら書いていたであろう量より、多くの配線だ。しかし、どのピースも、それがなければ何かが静かに壊れたからこそ存在している。

もう一度、実機でテストする

私が書くAndroidの記事すべてで同じ一文を繰り返す。信頼できるリマインダーは、エミュレータだけではテストできない。 安いSamsung、安いXiaomi、そしてPixelを手に入れよう。今から30分後にリマインダーをセットする。スマホをロックして机の上に置いておく。戻ってきて、何が発火し、何が発火しなかったかを見る。そのたった1つの実験は、どんなブログ記事よりも価値がある。

この記事も含めて。さあ、テストしよう。