Dans la partie précédente de ce guide, nous avons abordé la création d'un composant de notification au sein de l'application. Cette section se concentrera sur la gestion des notifications au niveau du système d'exploitation.
Sous Android, l'implémentation standard pour les notifications repose sur androidx.core.app.NotificationCompat. Pour les environnements de bureau, une approche courante consiste à faire clignoter l'icône dans la barre d'état système pour signaler une nouvelle notification. Il est également possible d'opter pour un système de notification plus traditionnel, similaire à celui d'Android, en utilisant des bibliothèques comme KMPNotifier.
Mise en œuvre
Pour la gestion des dépendances, nous utiliserons Koin. Les utilisateurs familiers avec Koin peuvent se référer à la section dédiée dans l'article sur la gestion de la navigation en Kotlin Compose Multiplatform. Alternativement, il est possible d'implémenter des singletons à l'aide d'objets statiques.
L'interface commune nécessitera trois fonctions principales :
sendAppNotification(title: String, content: String): Pour envoyer une notification système.clearAppNotification(): Pour effacer les notifications système.CheckAppNotificationPermission(requestPermission: (()->Unit) -> Unit): Pour vérifier les permissions de notification (spécifique aux plateformes mobiles).
expect fun sendAppNotification(title: String, content: String)
expect fun clearAppNotification()
@Composable
expect fun CheckAppNotificationPermission(requestPermission: (() -> Unit) -> Unit)
Plateforme Android
Initialisation sous Android
Sous Android, il est nécessaire d'initialiser le canal de notification avant toute utilisation :
override fun onCreate(savedInstanceState: Bundle?) {
// ...
super.onCreate(savedInstanceState)
createAppNotificationChannel(this)
setContent {
MainApp(
//...
)
}
}
fun createAppNotificationChannel(context: Context) {
val channelId = "your_channel_id"
val channelName = "Your Notification Channel"
val channelDescription = "Description for your notification channel"
val importance = NotificationManager.IMPORTANCE_DEFAULT
val channel = NotificationChannel(channelId, channelName, importance).apply {
description = channelDescription
}
val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
Il est également requis d'ajouter la permission POST_NOTIFICATIONS dans le fichier AndroidManifest.xml :
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- ... -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<!-- ... -->
</manifest>
Voici l'implémentation pour la vérification des permissions de notification :
@SuppressLint("PermissionLaunchedDuringComposition")
@RequiresApi(Build.VERSION_CODES.TIRAMISU)
@OptIn(ExperimentalPermissionsApi::class)
@Composable
actual fun CheckAppNotificationPermission(
requestPermission: (() -> Unit) -> Unit
) {
val notificationPermission = rememberPermissionState(
permission = Manifest.permission.POST_NOTIFICATIONS
)
if (!notificationPermission.status.isGranted) {
requestPermission {
notificationPermission.launchPermissionRequest()
}
}
}
La fonction requestPermission accepte une lambda qui sera appelée pour déclencher la demande de permission. Cette lambda contient la logique pour afficher une boîte de dialogue à l'utilisateur, lui expliquant la nécessité de la permission et lui permettant de l'accorder. La méthode NotificationManager.createDialogAlert, mentionnée dans la partie précédente, peut être utilisée à cet effet.
Exemple d'utilisation :
CheckAppNotificationPermission { permissionLauncher ->
NotificationManager.createDialogAlert(
MainDialogAlert(
message = "Notification Permission Required",
confirmOperationText = "Allow",
confirmOperation = {
permissionLauncher()
NotificationManager.removeDialogAlert()
},
)
)
}
Il est important de noter que ce code est un exemple simplifié. Une implémentation complète devrait gérer avec soin le moment et la fréquence d'appel de CheckAppNotificationPermission pour optimiser l'expérience utilisateur.
Envoi et suppression de notifications sous Android
Pour envoyer une notification :
actual fun sendAppNotification(title: String, content: String) {
val context = MainActivity.mainContext!! // Assurez-vous que mainContext est correctement initialisé
val channelId = "your_channel_id" // Utilisez le même channelId que lors de la création du canal
val builder = NotificationCompat.Builder(context, channelId)
.setSmallIcon(android.R.drawable.ic_dialog_info) // Remplacez par votre icône
.setContentTitle(title)
.setContentText(content)
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
val notificationManager: NotificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
// Utilisez un ID unique pour chaque notification, ici incrémenté
notificationManager.notify(notificationId++, builder.build())
}
Pour supprimer toutes les notifications :
actual fun clearAppNotification() {
val context = MainActivity.mainContext!! // Assurez-vous que mainContext est correctement initialisé
val notificationManager =
context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.cancelAll()
}
L'identifiant de notification notificationId peut être géré en interne ou récupéré depuis un serveur, selon l'architecture de votre application.
Plateforme de bureau
Sous macOS, Windows et Linux, les notifications système sont souvent signalées par le clignotement de l'icône dans la barre d'état système (system tray). Une autre approche consiste à afficher un badge rouge sur l'icône. Nous allons illustrer ici la méthode du clignotement de l'icône.
Initialisation sous Windows/macOS/Linux
L'interface java.awt.SystemTray est généralement utilisée pour interagir avec la barre d'état système. L'initialisation de Koin et l'obtention de l'instance SystemTray peuvent être faites globalement :
lateinit var koin: Koin
val tray: SystemTray = SystemTray.getSystemTray()
Dans la fonction main :
fun main() {
koin = KoinInit().init()
// ...
val showIcon: BufferedImage = koin.get(named("showIcon"))
val trayIcon = TrayIcon(showIcon, "Application Name") // Remplacez "Application Name"
tray.add(trayIcon)
application {
// ...
trayIcon.apply {
isImageAutoSize = true
addMouseListener(
// ... gestion des événements souris
)
}
Window(
// ...
) {
// ...
}
}
}
L'initialisation spécifique à la plateforme, incluant le chargement des icônes, est gérée via Koin :
class KoinInit {
fun init(appDeclaration: KoinAppDeclaration = {}): Koin {
return startKoin {
modules(
listOf(
// ... autres modules
platformModule(),
),
)
appDeclaration()
}.koin
}
}
expect fun platformModule(): Module
Le module de plateforme pour les environnements de bureau charge deux images pour le clignotement :
@OptIn(ExperimentalSettingsApi::class)
actual fun platformModule(): Module = module {
// ...
single<bufferedimage>(qualifier = named("showIcon")) {
ImageIO.read(
Thread.currentThread().contextClassLoader
.getResource("tray_icon_normal.png") // Assurez-vous que le chemin est correct
)
}
single<bufferedimage>(qualifier = named("hideIcon")) {
ImageIO.read(
Thread.currentThread().contextClassLoader
.getResource("tray_icon_notification.png") // Assurez-vous que le chemin est correct
)
}
}
</bufferedimage></bufferedimage>
Un Timer est utilisé pour alterner entre les icônes et créer l'effet de clignotement. Si l'option de badge rouge est préférée, le timer ne sera pas nécessaire.
val notificationTimer = Timer(500, object : ActionListener {
var showState = true
val normalIcon: BufferedImage = koin.get(named("showIcon"))
val notificationIcon: BufferedImage = koin.get(named("hideIcon"))
override fun actionPerformed(e: java.awt.event.ActionEvent?) {
tray.trayIcons.firstOrNull()?.image = if (showState) normalIcon else notificationIcon
showState = !showState
}
})
Envoi et suppression de notifications sous Windows/macOS/Linux
Pour envoyer une notification, le timer est démarré :
actual fun sendAppNotification(title: String, content: String) {
// Le titre et le contenu peuvent être utilisés pour une notification plus complexe si le système le supporte
// Ici, nous nous concentrons sur le clignotement de l'icône
notificationTimer.start()
}
Pour supprimer les notifications, le timer est arrêté et l'icône normale est restaurée :
actual fun clearAppNotification() {
notificationTimer.stop()
val normalIcon: BufferedImage = koin.get(named("showIcon"))
tray.trayIcons.firstOrNull()?.image = normalIcon
}
Code source
Le projet complet est disponible ici : Tomoyo