Android Jetpack Compose Kotiln 通知 Notification 教學(含權限申請)

在 Android 8.0 Oreo (Android SDK 26) 以上新增了通知權限,在發出通知前多了許多前置步驟,本文會以 Android 8.0 以上的寫法做示範。

宣告會使用到通知權限

在 AndroidManifest.xml 中加入 <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:tools="http://schemas.android.com/tools">

    <uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
    <application
        android:allowBackup="true"
        android:dataExtractionRules="@xml/data_extraction_rules"
        android:fullBackupContent="@xml/backup_rules"
        android:icon="@mipmap/ic_launcher"
        android:label="@string/app_name"
        android:roundIcon="@mipmap/ic_launcher_round"
        android:supportsRtl="true"
        android:theme="@style/Theme.Application1"
        tools:targetApi="31">
        <activity
            android:name=".MainActivity"
            android:exported="true"
            android:label="@string/app_name"
            android:theme="@style/Theme.Application1">
            <intent-filter>
                <action android:name="android.intent.action.MAIN" />

                <category android:name="android.intent.category.LAUNCHER" />
            </intent-filter>
        </activity>
    </application>

</manifest>
    

一開始建立的 App 在加入之前應用程式不會要求任何權限:

加入後重新執行時此「權限」按鈕就可以點擊,進入後會發現多了一個「通知」權限:

取得通知權限

從上面的截圖中有看到「通知」權限是「不允許」的,那是因為筆者使用的裝置版本是 Android 13 T(Android SDK 33),所以又多了一個請求權限的步驟。
由於通知非常麻煩,且不是本篇的主要內容,這裡直接放上程式碼簡單帶過。
    
    // import android.Manifest

    private val requestCode = 101 // 本次權限請求的代碼,可以自訂

    //    @RequiresApi(Build.VERSION_CODES.TIRAMISU)
    private fun getNotificationPermission(context: Context) {
        when {
            ContextCompat.checkSelfPermission(
                context,
                Manifest.permission.POST_NOTIFICATIONS
            ) == PackageManager.PERMISSION_GRANTED -> {
                // 具有權限
                Log.i("Notification", "具有權限")
            }

            shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> {
                // 顯示為什麼需要通知權限
                Log.i("Notification", "顯示為什麼需要通知權限")

                // TODO: 依據使用者的回應,決定是否發起權限請求

                // 若使用者不想要同意,不再詢問

                // 若使用者願意給予權限,拒絕權限,但沒有勾選「不再詢問」,可以再次發起權限請求
                // requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), requestCode)

                // 若使用者拒絕權限,且勾選「不再詢問」,需要引導使用者至設定頁面開啟權限
                // val intent = Intent()
                // intent.action = "android.settings.APP_NOTIFICATION_SETTINGS"
                // intent.putExtra("android.provider.extra.APP_PACKAGE", context.packageName)
                // startActivity(intent)
            }

            else -> {
                // 發起權限請求
                Log.i("Notification", "發起權限請求")
                requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), requestCode)
            }
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            requestCode
            -> {
                if ((grantResults.isNotEmpty() &&
                            grantResults[0] == PackageManager.PERMISSION_GRANTED)
                ) {
                    Log.i("Notification", "已允許,使用者給權限")
                } else {
                    Log.i("Notification", "被拒絕,使用者不給權限")
                }
                return
            }

            else -> {
                // Ignore all other requests.
            }
        }
    }
    


註:此程式碼需要放在 MainActivity class 中,不然會出現 Unresolved reference: shouldShowRequestPermissionRationale 錯誤

如果出現錯誤:
    
Manifest.permission.POST_NOTIFICATIONS // No documentation found.
    

只要在最上面加入以下程式碼即可:
    
import android.Manifest
    

註冊通知渠道

原本是沒有區分通知渠道,後來增加的主要原因是因為應用程式一直發通知,使用者不想看到就直接把通知關了,一個通知也收不到。增加通知渠道後 App 就可以開始區分這個通知是「好友訊息」、「陌生訊息」等通知,使用者可以慢慢關閉通知,不會一下就把全部通知關了。

通知有以下等級:
  • IMPORTANCE_NONE: 0 不重要通知,不顯示
  • IMPORTANCE_MIN: 1 最小通知,只會顯示在通知列中
  • IMPORTANCE_LOW: 2 低重要性通知,以摺疊方式顯示在通知列中
  • IMPORTANCE_DEFAULT: 3 預設通知,以展開方式顯示在通知列中,會有通知音效
  • IMPORTANCE_HIGH: 4 更高重要性通知,以展開方式顯示在通知列中,收到通知時會顯示在畫面上,會有通知音效
  • IMPORTANCE_MAX: 5 最高重要性通知,以展開方式顯示在通知列中,收到通知時會顯示在畫面上,會有通知音效,可以以全螢幕方式顯示

註:筆者在測試時通知渠道註冊後若要變更優先度需要重新安裝,不然可能會不準確。
    
    fun setNotificationChannel(context: Context, channelId: String) {

        val notificationManager = NotificationManagerCompat.from(context)

        val channel = NotificationChannelCompat.Builder(
            channelId,
            NotificationManagerCompat.IMPORTANCE_HIGH
        )
            .setName("預設通知渠道")
            .setDescription("這個是預設通知渠道的說明")
            .build()

        notificationManager.createNotificationChannel(channel)
    }
    

發布通知

    
    fun postNotification(context: Context, channelId: String, notificationId: Int = 1) {
        val notificationManager = NotificationManagerCompat.from(context)

        var builder = NotificationCompat.Builder(context, channelId)
            .setContentTitle("通知的標題")
            .setContentText("通知的內容")
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)

        val notification: Notification = builder.build()
        if (ActivityCompat.checkSelfPermission(
                this,
                Manifest.permission.POST_NOTIFICATIONS
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            Log.i("Notification", "沒有權限,不發送通知")
            return
        }
        notificationManager.notify(notificationId, notification)
    }
    


註: 一定要設定 SmallIcon ,不然會拋出例外。

建立 Button 測試發布通知

    
    @Composable
    fun PostNotificationPage() {
        val context = LocalContext.current

        val channelId = "default_channel_id"
        Button(
            onClick = {
                setNotificationChannel(context, channelId)
                Log.i("Notification", "Button Clicked")
                getNotificationPermission(context)
                postNotification(context, channelId)

            }
        ) {
            Text("Notification")
        }
    }
    

完整程式碼:
    
package app.ruyut.application1

import android.Manifest
import android.app.Notification
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.material3.Button
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationChannelCompat
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import androidx.core.content.ContextCompat
import com.example.application1.ui.theme.Application1Theme

class MainActivity : ComponentActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            Application1Theme {
                // A surface container using the 'background' color from the theme
                Surface(
                    modifier = Modifier.fillMaxSize(),
                    color = MaterialTheme.colorScheme.background
                ) {
                    Box() {
                        PostNotificationPage()
                    }
                }
            }

        }
    }

    private val requestCode = 101 // 本次權限請求的代碼,可以自訂

    private fun getNotificationPermission(context: Context) {
        when {
            ContextCompat.checkSelfPermission(
                context,
                Manifest.permission.POST_NOTIFICATIONS
            ) == PackageManager.PERMISSION_GRANTED -> {
                // 具有權限
                Log.i("Notification", "具有權限")
            }

            shouldShowRequestPermissionRationale(Manifest.permission.POST_NOTIFICATIONS) -> {
                // 顯示為什麼需要通知權限
                Log.i("Notification", "顯示為什麼需要通知權限")

                // TODO: 依據使用者的回應,決定是否發起權限請求

                // 若使用者不想要同意,不再詢問

                // 若使用者願意給予權限,拒絕權限,但沒有勾選「不再詢問」,可以再次發起權限請求
                // requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), requestCode)

                // 若使用者拒絕權限,且勾選「不再詢問」,需要引導使用者至設定頁面開啟權限
                // val intent = Intent()
                // intent.action = "android.settings.APP_NOTIFICATION_SETTINGS"
                // intent.putExtra("android.provider.extra.APP_PACKAGE", context.packageName)
                // startActivity(intent)
            }

            else -> {
                // 發起權限請求
                Log.i("Notification", "發起權限請求")
                requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS), requestCode)
            }
        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        when (requestCode) {
            requestCode
            -> {
                if ((grantResults.isNotEmpty() &&
                            grantResults[0] == PackageManager.PERMISSION_GRANTED)
                ) {
                    Log.i("Notification", "已允許,使用者給權限")
                } else {
                    Log.i("Notification", "被拒絕,使用者不給權限")
                }
                return
            }

            else -> {
                // Ignore all other requests.
            }
        }
    }

    // 註冊通知渠道
    fun setNotificationChannel(context: Context, channelId: String) {

        val notificationManager = NotificationManagerCompat.from(context)

        val channel = NotificationChannelCompat.Builder(
            channelId,
            NotificationManagerCompat.IMPORTANCE_HIGH
        )
            .setName("預設通知渠道")
            .setDescription("這個是預設通知渠道的說明")
            .build()

        notificationManager.createNotificationChannel(channel)
    }

    // 發布通知
    fun postNotification(context: Context, channelId: String, notificationId: Int = 1) {
        val notificationManager = NotificationManagerCompat.from(context)

        var builder = NotificationCompat.Builder(context, channelId)
            .setContentTitle("通知的標題")
            .setContentText("通知的內容")
            .setSmallIcon(R.drawable.ic_launcher_foreground)
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)

        val notification: Notification = builder.build()
        if (ActivityCompat.checkSelfPermission(
                this,
                Manifest.permission.POST_NOTIFICATIONS
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            Log.i("Notification", "沒有權限,不發送通知")
            return
        }
        notificationManager.notify(notificationId, notification)
    }

    @Composable
    fun PostNotificationPage() {
        val context = LocalContext.current

        val channelId = "default_channel_id"
        Button(
            onClick = {
                setNotificationChannel(context, channelId)
                Log.i("Notification", "Button Clicked")
                getNotificationPermission(context)
                postNotification(context, channelId)

            }
        ) {
            Text("Notification")
        }
    }
}
    



參考資料: Android developers - Request runtime permissions
Android developers - Notifications overview
Android developers - Notification runtime permission
Android developers - Create a Notification
stack overflow - Android ActivityResult API unresolved reference error registerForActivityResult

留言