--- id: android-notification-patterns title: Android Notification — 채널 / 권한 / 스타일 category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [android, notification, fcm, vibe-coding] tech_stack: { language: "Kotlin / Android", applicable_to: ["Android"] } applied_in: [] aliases: [NotificationChannel, FCM, push, BigText, MessagingStyle, POST_NOTIFICATIONS] --- # Android Notification > Android 8+ = **NotificationChannel 필수**. Android 13+ = POST_NOTIFICATIONS 권한. **Style (BigText/Inbox/Messaging) 으로 풍부**. FCM 으로 push. ## 📖 핵심 개념 - Channel: 카테고리 (chat / promo / system). 사용자가 채널별 설정. - Importance: HIGH (heads-up), DEFAULT, LOW, MIN. - Style: 단순 / BigText / BigPicture / Messaging / Media. - Action: 인라인 버튼 + 답장 입력. ## 💻 코드 패턴 ### Channel 생성 (앱 시작) ```kotlin class App : Application() { override fun onCreate() { super.onCreate() val nm = getSystemService()!! nm.createNotificationChannels(listOf( NotificationChannel("chat", "Chat", NotificationManager.IMPORTANCE_HIGH).apply { description = "New messages" enableVibration(true) setSound(soundUri, audioAttrs) }, NotificationChannel("promo", "Promotions", NotificationManager.IMPORTANCE_LOW), )) } } ``` ### 권한 (13+) ```kotlin private val launcher = registerForActivityResult(ActivityResultContracts.RequestPermission()) { granted -> } if (Build.VERSION.SDK_INT >= 33 && ContextCompat.checkSelfPermission(this, Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) { launcher.launch(Manifest.permission.POST_NOTIFICATIONS) } ``` ```xml ``` ### 단순 notification ```kotlin val pi = PendingIntent.getActivity(ctx, 0, Intent(ctx, MainActivity::class.java), PendingIntent.FLAG_IMMUTABLE) val notif = NotificationCompat.Builder(ctx, "chat") .setSmallIcon(R.drawable.ic_chat) .setContentTitle("Alice") .setContentText("Hi there") .setContentIntent(pi) .setAutoCancel(true) .build() NotificationManagerCompat.from(ctx).notify(notifId, notif) ``` ### BigText (긴 본문) ```kotlin .setStyle(NotificationCompat.BigTextStyle().bigText(longBody)) ``` ### MessagingStyle (chat) ```kotlin val you = Person.Builder().setName("You").build() val alice = Person.Builder().setName("Alice").setIcon(...).build() val style = NotificationCompat.MessagingStyle(you) .setConversationTitle("Alice") .addMessage("Hi", System.currentTimeMillis() - 60_000, alice) .addMessage("Hello", System.currentTimeMillis(), you) builder.setStyle(style) ``` ### Action + RemoteInput (답장) ```kotlin val replyKey = "key_reply" val remoteInput = RemoteInput.Builder(replyKey).setLabel("Reply").build() val replyPI = PendingIntent.getBroadcast(ctx, 0, Intent(ctx, ReplyReceiver::class.java).putExtra("convId", convId), PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_MUTABLE) val replyAction = NotificationCompat.Action.Builder(R.drawable.ic_reply, "Reply", replyPI) .addRemoteInput(remoteInput) .setSemanticAction(NotificationCompat.Action.SEMANTIC_ACTION_REPLY) .build() builder.addAction(replyAction) ``` ```kotlin class ReplyReceiver : BroadcastReceiver() { override fun onReceive(context: Context, intent: Intent) { val text = RemoteInput.getResultsFromIntent(intent)?.getCharSequence("key_reply").toString() // send + update notification } } ``` ### FCM (push) ```kotlin class MyFcmService : FirebaseMessagingService() { override fun onMessageReceived(msg: RemoteMessage) { val data = msg.data showNotification(data["title"] ?: "", data["body"] ?: "") } override fun onNewToken(token: String) { // 서버에 등록 } } ``` ```xml ``` ### Group + summary ```kotlin val groupKey = "messages-group" val notif1 = NotificationCompat.Builder(ctx, "chat").setGroup(groupKey).build() val notif2 = NotificationCompat.Builder(ctx, "chat").setGroup(groupKey).build() val summary = NotificationCompat.Builder(ctx, "chat") .setGroup(groupKey).setGroupSummary(true) .setSmallIcon(R.drawable.ic_chat) .setContentTitle("3 new messages") .build() ``` ### Heads-up (떠 있는 알림) ```kotlin NotificationChannel(..., IMPORTANCE_HIGH) // 채널이 HIGH .setPriority(NotificationCompat.PRIORITY_HIGH) .setCategory(NotificationCompat.CATEGORY_MESSAGE) .setFullScreenIntent(pi, true) // 전화 같은 거 (lock screen 도) ``` ### Image / Avatar ```kotlin .setLargeIcon(bitmap) .setStyle(NotificationCompat.BigPictureStyle().bigPicture(bigBitmap)) ``` ## 🤔 의사결정 기준 | 종류 | 채널 / 스타일 | |---|---| | 채팅 | HIGH + MessagingStyle + reply action | | 시스템 (sync 완료) | LOW + 단순 | | 마케팅 / promo | LOW + dismissable | | 진행 중 (다운로드) | LOW + progress | | 알람 / 콜 | HIGH + fullScreenIntent | ## ❌ 안티패턴 - **Channel 안 만들고 notify**: 8+ 에서 안 보임. - **권한 미요청 (13+)**: silently 차단. - **모든 채널 HIGH**: 사용자 끄기. 적절히. - **PendingIntent FLAG_IMMUTABLE 누락 (12+)**: SecurityException. - **Group summary 누락 + 많은 알림**: 사용자 화남. - **Image inline base64 큼**: 메모리. - **`setOngoing(true)` 잘못 쓰면 dismiss 못 함**. - **Channel 삭제 + 재생성**: 사용자 설정 잃음. ## 🤖 LLM 활용 힌트 - Channel + 권한 (13+) + Style + Action 4종. - FCM + onMessageReceived → showNotification. - Group key + summary. ## 🔗 관련 문서 - [[Android_Foreground_Service_Patterns]] - [[iOS_Push_Notifications]] - [[Native_Battery_Network_Profiling]]