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

Androidで通知許可をいつ求めるか(そして「拒否」からどう回復するか)

AndroidのPOST_NOTIFICATIONSランタイム権限の実践ガイド — いつ求めるべきか、リクエストをどう事前に準備するか、そして誰かが拒否したときにどうするか。

MFKAPPS 1 分で読めます

Android 13以降、通知はデフォルトでは許可されない — ユーザーはカメラや位置情報へのアクセスに同意するのと同じように、POST_NOTIFICATIONSに明示的に同意しなければならない。通知が価値のすべてを担っているアプリ — 水分補給のリマインダー、服薬アラート — にとって、あの1つのシステムダイアログこそが、アプリが機能することと、アプリがたまに開くだけのアイコンになることの分かれ目になる。タイミングを誤れば、ほとんどの人は同意する理由を見る前に、反射的に「許可しない」をタップする。タイミングが合えば、同じダイアログはごく当たり前のものになる。権限そのものは変わらない。変わるのはそれを取り巻くリクエストの方だ。

ダイアログは一度しか出ない

POST_NOTIFICATIONSは他のすべてのAndroidランタイム権限と同じように振る舞う。システムはインストールごとに一度だけダイアログを表示する。ユーザーが拒否すると、次のrequestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)呼び出しは静かにfalseを返す — ダイアログもエラーも出ず、ただ即座に拒否されるだけだ。これがテスト中に人々をつまずかせる部分だ。2回目のActivityResultLauncherリクエストは何もしていないように見えるが、実際にはやるべきことを正確にやっている。

val requestPermissionLauncher = registerForActivityResult(
    ActivityResultContracts.RequestPermission()
) { granted ->
    if (granted) {
        scheduleDailyReminder()
    } else {
        showNotificationsDisabledState()
    }
}

システムダイアログへの本当のチャンスは一度しかないため、launch()をどう呼ぶかよりも、いつ呼ぶかの方が重要になる。

初回起動時には求めない

反射的にやりがちなのは、アプリが将来必要とするすべての権限をオンボーディング中に要求し、アプリの残りの部分ではすべて許可されていると仮定できるようにすることだ。特に通知に関して言えば、これはほぼ最悪のタイミングに近い。初回起動時、ユーザーには、リマインダーアプリが後で自分を中断させたい理由についての文脈が何もない — アイコンを見てまだ十秒しか経っていないのだから。先立つ理由のない権限ダイアログは摩擦として読み取られ、まさにその瞬間の摩擦は、熟考された「許可する」よりも反射的な「許可しない」を招くことの方が多い。

解決策は、権限の価値が自明になった時点で求めることだ。Hydrameでは、それは誰かが1日の水分目標の設定を終えた瞬間だ — その人はたった今、いつもっと水を飲みたいかをアプリに伝えたばかりなので、リマインダーは中断ではなく、本人が求めた機能そのものになる。OldSchoolでは、最初の薬とそのスケジュールを追加した直後がそれにあたる。リクエストは、ユーザーがすでにその権限が可能にすることを望んでいることを示唆する行動に続いて行われる。システムダイアログはそれを正式なものにするだけだ。

事前説明画面は、剥き出しのシステムダイアログに勝る

システムの権限ダイアログは、設計上そっけない — タイトル、アプリ名、許可すると許可しない。なぜかを説明することはできず、Androidはその文言をカスタマイズする手段を一切与えてくれない。あなたがコントロールできるのは、その直前に表示される画面だ。アプリのニーズではなく、ユーザー自身の目標を軸にした、たった一文の文脈説明だ。

「今設定したスケジュールに沿って、水を飲むタイミングをお知らせします。
 これらのリマインダーを表示するには、Androidの許可が必要です。」

 [ リマインダーをオンにする ]   [ 今はしない ]

これは「プレ権限」画面や「ソフトアスク」画面と呼ばれることもあり、その価値は見た目だけのものではない — 少し後にシステムダイアログが現れたときにそれが意味することを変えてしまうのだ。それがなければ、OSのダイアログはユーザーが受け取る最初で唯一の説明になる。それがあれば、OSのダイアログは、ユーザーがすでに同意した何かの単なる確認にすぎなくなる。事前説明画面での「今はしない」も何のコストもかからない — 実際の権限には一切触れないので、システムへの唯一の試みは、ユーザーがアプリでもっと時間を過ごし、その求めを信頼する理由をもっと持った後でも、まだ後で使える。

拒否は最終的なものではない — shouldShowRequestPermissionRationale

誰かがシステムダイアログを拒否した後、ActivityCompat.shouldShowRequestPermissionRationale()は、次のリクエストの前に自分自身の説明を再度表示する価値があるかどうか、あるいはユーザーが恒久的に拒否したのか(「二度と表示しない」にチェックを入れたか、Androidのより新しく厳格な再質問ルールの下ですでに一度拒否している)を教えてくれる。

fun onReminderCardTapped(activity: Activity) {
    val granted = ContextCompat.checkSelfPermission(
        activity, Manifest.permission.POST_NOTIFICATIONS
    ) == PackageManager.PERMISSION_GRANTED

    when {
        granted -> scheduleDailyReminder()
        ActivityCompat.shouldShowRequestPermissionRationale(
            activity, Manifest.permission.POST_NOTIFICATIONS
        ) -> showRationaleThenRequest()
        else -> openAppNotificationSettings(activity)
    }
}

以前の拒否の後にfalseを返す場合、launch()をどう呼ぼうとシステムは二度とダイアログを表示しない — 戻る唯一の道は、自分のパッケージに絞ったIntent(Settings.ACTION_APP_NOTIFICATION_SETTINGS)でたどり着くアプリの通知設定ページだ。これを「設定を確認してください」のような一般的なメッセージの裏に隠さないこと。このインテントはユーザーをナビゲーション不要で正しい画面へ直接連れて行ってくれる。

「いいえ」をバグではなく、正当な状態として設計する

一部のユーザーは本気で拒否する — それがサポートされた状態であり、壊れた状態ではないようにアプリを構築すること。Hydrameはリマインダーなしでも水分記録を続ける。連続記録と履歴がひと押しをもらえないだけだ。OldSchoolはアプリ内の服薬リストを完全に使える状態のまま保ち、機能が動かないのに存在するふりをするのではなく、常時表示され消去可能なバナー — 「リマインダーはオフです」— を表示する。リマインダーがスケジュールされたのに権限が一度も与えられなかったために表示されないというサイレントな失敗は、正直に欠けている機能よりも悪い。それはユーザーが選んだことのようには見えず、アプリが信頼できないように見えてしまう。

唯一のシステムダイアログには、準備する価値がある

POST_NOTIFICATIONSは難しいAPIではない — registerForActivityResultとたった一つの権限文字列で、その仕組みは数行でカバーできる。実際に許可率を左右するのは、その数行を取り巻くすべてだ。価値が明らかになった瞬間の後に求めること、平易な言葉での一文の文脈で事前説明すること、そして拒否が起こらないと仮定するのではなく、それに対する本当の答えを持っていること。リマインダーそのものが製品であるどんなアプリにとっても、それは仕上げの磨き上げではない — それはその機能がそもそも機能するかどうかの話なのだ。