※ChatGPTを使用して記事を作成しています。
「通知をタップしたら、前に見た記事が開いたんですけど?」
「リマインダー押したら、全然違う画面に飛ばされました」
ある日、ユーザーから寄せられた謎の報告。
ログを確認してもクラッシュはなし。でも、遷移先が明らかにおかしい。
調べてみると、原因は……古いPendingIntentが再利用されていたことでした。
今回は、通知やAlarmManagerを扱ううえで避けて通れないPendingIntentという仕組みと、
その意図しない“再利用”によって引き起こされた大混乱の顛末を語ります。
発端:通知タップ後に意図しない画面が開く
あるニュースアプリで、通知から記事詳細にジャンプする仕様を実装していました。
ユーザーが通知をタップすると、その記事の内容が表示される──それだけのシンプルなもの。
コードも非常に単純:
val intent = Intent(context, ArticleDetailActivity::class.java).apply {
putExtra("articleId", article.id)
}
val pendingIntent = PendingIntent.getActivity(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
val notification = NotificationCompat.Builder(context, CHANNEL_ID)
.setContentTitle(article.title)
.setContentText("記事をチェックしよう")
.setSmallIcon(R.drawable.ic_notification)
.setContentIntent(pendingIntent)
.build()
しかしある日、あるユーザーからこんな報告が寄せられました。
「昨日見た記事が、今日の通知でも開かれるんだけど?」
最初は「誤解かな」と思いました。
でも複数のユーザーから報告が上がってきて、再現もできるようになってきました。
原因:Intentの中身は変わっても、同じPendingIntentが使われていた
実は、PendingIntent.getActivity()
の第2引数であるrequestCodeが常に0になっており、
Intentの内容が変わっても、OSは同一のPendingIntentとして扱って再利用していたのです。
つまり、
articleId = 1001
の通知を最初に出したときのIntentがキャッシュされ、- 次に
articleId = 1002
で通知を出しても、中身が上書きされないまま古いIntentが使われる
という事態が発生していました。
PendingIntentの仕様:Intentの“同一性”って?
Androidは、以下の4つの要素を使ってPendingIntentを識別しています:
Intent
のアクションIntent
のデータ(URI)Intent
のカテゴリIntent
のコンポーネント名(class)
つまり、putExtra()
で渡しているようなExtrasは識別対象に含まれないのです。
そのため、Intentの見た目が違っていても、OSから見れば「同じもの」と判断され、前に生成されたPendingIntentが再利用されるというわけです。
これが地獄:AlarmManagerとの併用でさらなる混乱
この問題は通知だけにとどまりませんでした。
ユーザーの「毎日20時にリマインダーを送る」という機能をAlarmManagerで実装していたのですが、
ここでも同じようにPendingIntentが使われていました。
val intent = Intent(context, ReminderReceiver::class.java).apply {
putExtra("type", "DAILY_REMINDER")
}
val pendingIntent = PendingIntent.getBroadcast(
context,
0,
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
alarmManager.setExact(
AlarmManager.RTC_WAKEUP,
triggerTime,
pendingIntent
)
これを複数のリマインダーで同じrequestCode = 0
、同じIntent構成で出していたため、「最新のリマインダー設定が、すべての古いリマインダーを上書きしてしまう」という事態が発生。
- 毎週のリマインダー
- 1回限りの特別通知
- アプリ起動後の遅延通知
これらがひとつのPendingIntentに集約され、同じ動作しかしなくなったのです。
暴れ回る古いIntentたち
アプリの利用が進むほど、これらの問題が積もり積もってユーザー体験を崩壊させていきました。
- 過去の通知をタップ → 関係ない記事が開く
- リマインダーが何度も届く or 一度も届かない
- アプリ起動時に通知が出るはずなのに出ない
原因はすべて、古いPendingIntentが“いつまでも使い回される”設計にしていたことにありました。
解決策:PendingIntentに一意性を持たせよ
✅ 対策①:requestCodeに一意の値を使う
val pendingIntent = PendingIntent.getActivity(
context,
article.id.hashCode(), // 記事ごとに異なるrequestCode
intent,
PendingIntent.FLAG_UPDATE_CURRENT
)
これにより、Intentの中身が違えば異なるPendingIntentとして扱われるようになります。
✅ 対策②:Intentの内容を変える(DataやAction)
val intent = Intent(context, ArticleDetailActivity::class.java).apply {
action = "com.example.ACTION_VIEW_${article.id}"
data = Uri.parse("myapp://article/${article.id}")
putExtra("articleId", article.id)
}
Intentのdata
やaction
を工夫することで、識別情報が変わり、OSが別のPendingIntentとして認識してくれます。
✅ 対策③:APIレベルに応じたフラグ指定を確認
val flags = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT } else { PendingIntent.FLAG_UPDATE_CURRENT }
Android 12以降ではFLAG_MUTABLE / FLAG_IMMUTABLEの指定が必須になりました。
これを忘れると、通知が出ない・タップできないなどの別の問題が起きるため、要注意です。
あとがき:フラグひとつで大混乱
この一連の事件を通して感じたのは、PendingIntentは“慣れ”で扱ってはいけないということ。
たった1つのrequestCode
の使い回し、たった1つのIntentの構成ミスが、数万人のユーザーに混乱をもたらす可能性があります。
表面上は動いているように見えても、内部では古いIntentが暴れ続けている。
そうした“見えない地雷”を踏まないためにも、PendingIntentの再利用性と識別ルールをきちんと理解することが重要だと痛感しました。