오늘은 안드로이드에 Notification 기능을 정리해보려고 한다.
Notification 즉, 알람기능의 경우는 개인적으로 모바일에서는 중요한 기능이라고 생각한다.
웹과 달리 모바일 어플리케이션의 경우 모바일의 휴대성이라는 점을 고려해야한다.
알림(Notification) 개요
- 앱의 UI와 별도로 사용자에게 앱과 관련한 정보를 보여주는 기능
- 알림을 터치하여 해답 앱을 열 수 있다.
- 바로 간단한 작업(ex 문자 답하기)을 할 수 있음(Android 7.0부터)
- 보통 단말기 상단 부분에 표시된다.
- 앱 아이콘의 배지로도 표시(Android 8.0부터)
알림(Notification) 표시 권한
- 안드로이드 13 (API 33) 부터 알림 표시를 위해 동적 권한 필요
- Manifest 파일에 POST_NOTIFICATIONS 권한 추가
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
- 동적 권한 요청 코드 추가
requestSinglePermission(Manifest.permission.POST_NOTIFICATIONS)
private fun requestSinglePermission(permission: String) {
if (checkSelfPermission(permission) == PackageManager.PERMISSION_GRANTED)
return
val requestPermLauncher = registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it == false) { // permission is not granted!
AlertDialog.Builder(this).apply {
setTitle("Warning")
setMessage(getString(R.string.no_permission, permission))
}.show()
}
}
if (shouldShowRequestPermissionRationale(permission)) {
// you should explain the reason why this app needs the permission.
AlertDialog.Builder(this).apply {
setTitle("Reason")
setMessage(getString(R.string.req_permission_reason, permission))
setPositiveButton("Allow") { _, _ -> requestPermLauncher.launch(permission) }
setNegativeButton("Deny") { _, _ -> }
}.show()
} else {
// should be called in onCreate()
requestPermLauncher.launch(permission)
}
}
NotificationCompat
- 알림은 안드로이드가 버전에 따라 기능이 계속 추가되어 왔음
- 여기에서는 안드로이드 8.0이상을 기준으로 이야기 하지만
- 이전 버전에서도 동작할 수 있도록 support library의 NotificationCompat, NotificationManagerCompat 사용이 권장됨
- 이미 Support library를 사용하고 있으면 별도로 gradle 수정할 필요 없음
implementation 'androidx.appcompat:appcompat:1.5.0'
알림 채널(Android 8.0 이상)
- Android 8.0 이상의 경우는 알림을 만들기 전에 알림 채널을 먼저 만들어야 함
- 알림 채널은 알림을 그룹하여 알림 활성화나 방식을 변결할 수 있음
- 현재 앱이 실행 중인 안드로이드 버전을 확인하여 8.0 이상인 경우만 채널 생성
private val myNotificationID = 1
private val channelID = "default"
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel() {
val channel = NotificationChannel(
channelID, "default channel",
NotificationManager.IMPORTANCE_DEFAULT
)
channel.description = "description text of this channel."
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
알림 생성
NotificationCompat.Builder 객체에서 알림에 대한 UI 정보와 작업을 지정
- setSmallIcon() : 작은 아이콘
- setContentTitle() : 제목
- setContentText() : 세부텍스트
NotificationCompate.Builder.build() 호출
- Notification 객체를 반환
NotificationManagerCompat.notify()를 호출해서 시스템에 Notification 객체를 전달
단순 알림 생성, notify()
private var myNotificationID = 1
get() = field++
private fun showNotification() {
val builder = NotificationCompat.Builder(this, channelID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("Notification Title")
.setContentText("Notification body")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
NotificationManagerCompat.from(this)
.notify(myNotificationID, builder.build())
}
알림 중요도
채널 중요도(Android 8.0 이상)
NotificationChannel(channelID, "default channel", NotificationManager.IMPORTANCE_DEFAULT)
알림 우선순위(Android 7.1 이상)
NotificationCompat.Builder(this, channelID).setPriority(NotificationCompat.PRIORITY_DEFAULT)
채널/알림 중요도/우선순위 수준
중요도 | 설명 | 중요도(Android 8.0) 이상 | 우선순위(Android 7.1 이하) |
긴급 | 알림음이 울림, 헤드업 알림표시 | IMPORTANCE_HIGH | PRIORITY_HIGH |
높음 | 알림음이 울림 | IMPORTANCE_DEFAULT | PRIORITY_DEFAULT |
중간 | 알림음이 없음 | IMPORTANCE_LOW | PRIORITY_LOW |
낮음 | 알림음이 없음, 상태 표시줄에 표시 안됨 | IMPORTANCE_MIN | PRIORITY_MIN |
알림에 확장뷰
긴 텍스트를 추가한 확장 뷰를 알림에 넣을 수 있음
builder(this, channelID).setStyle(NotificationCompat.BigTextStyle()
.bigText(resources.getString(R.string.long_notification_body)))
그림 넣은 확장 뷰
val bitmap = BitmapFactory.decodeResource(resources, R.drawable.android_hsu)
val builder = NotificationCompat.Builder(this, channelID)
.setSmallIcon(R.mipmap.ic_launcher)
.setLargeIcon(bitmap)
.setContentTitle("Notification Title")
.setContentText("Notification body")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setStyle(NotificationCompat.BigPictureStyle()
.bigPicture(bitmap)
.bigLargeIcon(null)) // hide largeIcon while expanding
NotificationManagerCompat.from(this)
.notify(myNotificationID, builder.build())
알림에 버튼 추가
val intent = Intent(this, TestActivity::class.java)
val pendingIntent = PendingIntent.getActivity(this, 0, intent,
PendingIntent.FLAG_IMMUTABLE)
val builder = NotificationCompat.Builder(this, channelID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("Notification Title")
.setContentText("Notification body")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.addAction(R.drawable.android_hsu, "Action", pendingIntent)
NotificationManagerCompat.from(this)
.notify(myNotificationID, builder.build())
알림에 프로그래스 바 표시
val progressNotificationID = myNotificationID
val builder = NotificationCompat.Builder(this, channelID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("Progress")
.setContentText("In progress")
.setProgress(100, 0, false)
.setPriority(NotificationCompat.PRIORITY_LOW) // need to change channel importance LOW for Android 8.0 or higher.
NotificationManagerCompat.from(this)
.notify(progressNotificationID, builder.build())
CoroutineScope(Dispatchers.Default).apply {
launch {
for (i in (1..100).step(10)) {
Thread.sleep(1000)
builder.setProgress(100, i, false)
NotificationManagerCompat.from(applicationContext)
.notify(progressNotificationID, builder.build())
}
builder.setContentText("Completed")
.setProgress(0, 0, false)
NotificationManagerCompat.from(applicationContext)
.notify(progressNotificationID, builder.build())
}
}
알림에 액티비티 연결하기
알림을 터치하여 연결된 액티비티가 실행되도록 하는 것
- PendingIntent 사용
- 연결된 액티비티가 일반 액티비티, 알림 전용 액티비티인지에 따라 백스택 관리가 달라짐
일반 액티비티 : 일반적인 앱의 액티비티임
- 사용자가 앱을 사용하면서 액티비티를 시작시키는 것과 유사하게 백스택을 관리
알림 전용 액티비티 : 알림하고만 연결되어 실행 가능한 액티비티로 알림을 확장하는 개념
- 사용자가 다른 방법으로 시작하지는 못하게 함
태스크와 백 스택(Back Stack)
태스크(Task) : 어떤 작업을 하기 위한 액티비티의 그룹
태스크마다 자신의 백스택을 가지고 있음
foreground, background task
foreground task(Task B), background(Task A)
최근 앱 보기에서 선택하거나 앱 아이콘을 눌러서 foreground task로 전환할 수 있음
액티비티를 시작(startActivity)할 때 플래그에 따라 다르게 동작하게 할 수 있음
- A라는 액티비티를 시작한다고 가정, startActivty(A)
- 플래그 없음 : 액티비티 A의 새 인스턴스를 항상 시작함
- FLAG_ACTIVITY_NEW_TASK : 새 태스크로 A를 시작, 하지만 이미 실행 중인 A의 인스턴스가 있다면 새로 만들지 않고 A의 인스턴스가 포함된 태스크를 앞(foreground)으로 가져오고 A의 onNewIntent() 호출
- FLAG_ACTIVITY_CLEAR_TASK : A 의 인스턴스와 관련된 모든 기존 태스크를 제거하고 새로 A의 인스턴스를 시작함, FLAG_ACTIVITY_NEW_TASK와 같이 사용해야 함
- FLAG_ACTIVITY_SINGLE_TOP : A의 인스턴스가 태스크 백스택 탑에 존재하는 경우, 새로 만들지 않고 A의 onNewIntent() 호출
- FLAG_ACTIVTY_CLEAR_TOP : A의 인스턴스가 이미 시작 중인 경우 백스택에서 A의 인스턴스 위에 있는 다른 액티비티 인스턴스를 모두 제거하고 A의 onNewIntent() 호출
- FLAG_ACTIVITY_NEW_TASK와 같이 사용됨
- 플래그 FLAG_ACTIVITY_NEW_TASK 예
알림에 액티비티 연결하기 - 일반 액티비티
알림을 터치하면 일반 액티비티인 SecondActivity가 시작, 이때 MainActivity위에 SecondActivity가 있는 백스택을 생성
AndroidManifest.xml의 SecondActivity 정의 부분
<activity android:name=".SecondActivity" android:parentActivityName=".MainActivity" />
PendingIntent 생성하고 알림 등록
val intent = Intent(this, SecondActivity::class.java)
val pendingIntent = with (TaskStackBuilder.create(this)) {
addNextIntentWithParentStack(intent)
getPendingIntent(0, PendingIntent.FLAG_IMMUTABLE)
}
val builder = NotificationCompat.Builder(this, channelID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("Notification Title")
.setContentText("Notification body")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true) // auto remove this notification when user touches it
NotificationManagerCompat.from(this)
.notify(myNotificationID, builder.build())
알림을 터치하면
알림은 사라지고, SecondActivity가 실행됨
SecondActivity가 실행된 상태에서 Back이나 Up을 누르면 MainActivity가 나옴
사실 MainActivity가 이미 백스택에 있기 때문에 TaskStackBuilder로 백스택을 조작하지 않아도 동일하게 동작
백스택에 없는 다른 액티비티를 SecondActivity의 parentActivity로 하면 달라짐
알림에 액티비티 연결하기 - 알림 전용 액티비티
알림을 터치하면 알림 전용 액티비티인 TempActivity가 시작됨
AndroidManifest.xml의 TempActivity 정의 부분
<activity
android:name=".TempActivity"
android:launchMode="singleTask"
android:taskAffinity=""
android:excludeFromRecents="true" />
<activity
PendingIntent 생성하고 알림 등록
val intent = Intent(this, TempActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK
val pendingIntent = PendingIntent.getActivity(this, 0, intent,
PendingIntent.FLAG_IMMUTABLE)
val builder = NotificationCompat.Builder(this, channelID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("Notification Title")
.setContentText("Notification body")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
.setContentIntent(pendingIntent)
.setAutoCancel(true) // auto remove this notification when user touches it
NotificationManagerCompat.from(this)
.notify(myNotificationID, builder.build())
알림을 터치하면, 알림은 사라지고, TempActivity가 실행됨
최근(Recents) 앱 보기를 눌러보면 TempActivity와 MainActivity가 서로 다른 태스크로 되어 있음
알림을 터치하기 전에 홈 버튼을 누른 후에, 알림을 터치하여 TempActivity를 실행하게 한 후 TempActivity에서 Back을 누르면 백스택에 다른 액티비티가 없기 때문에 홈 화면을 돌아감
추가 실습
액티비티에 버튼을 만들고 버튼을 누를 때 알림을 표시하되, 버튼을 여러 번 눌러도 하나의 알림 만 표시되도록 한다.
알림의 내용에는 액티비티가 시작한 이후 버튼을 누른 횟수가 표시되도록 한다.
버튼을 계속 누르면서 알림을 지우지 않으면 알림에 버튼 누른 수가 계속 업데이트가 된다.
MainActivity
import android.Manifest
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.content.pm.PackageManager
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Button
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.appcompat.app.AlertDialog
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.example.notificationpractice.databinding.ActivityMainBinding
class MainActivity : AppCompatActivity() {
lateinit var binding: ActivityMainBinding
private val channelID = "default"
@RequiresApi(33)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = ActivityMainBinding.inflate(layoutInflater)
setContentView(binding.root)
var check = 1
val button = findViewById<Button>(R.id.button)
button.setOnClickListener {
showNotification(check)
check += 1
}
Manifest.permission.POST_NOTIFICATIONS.requestSinglePermission()
createNotificationChannel()
}
private fun showNotification(check: Int) {
val builder = NotificationCompat.Builder(this, channelID)
.setSmallIcon(R.mipmap.ic_launcher)
.setContentTitle("Notification Lab")
.setContentText("""Notification #$check""")
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
NotificationManagerCompat.from(this)
.notify(1, builder.build())
}
private fun createNotificationChannel() {
val channel = NotificationChannel(
channelID, "default channel",
NotificationManager.IMPORTANCE_DEFAULT
)
channel.description = "description text of this channel."
val notificationManager =
getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager.createNotificationChannel(channel)
}
private fun String.requestSinglePermission() {
if (checkSelfPermission(this) == PackageManager.PERMISSION_GRANTED)
return
val requestPermLauncher =
registerForActivityResult(ActivityResultContracts.RequestPermission()) {
if (it == false) { // permission is not granted!
AlertDialog.Builder(this@MainActivity).apply {
setTitle("Warning")
setMessage("Warning")
}.show()
}
}
if (shouldShowRequestPermissionRationale(this)) {
// you should explain the reason why this app needs the permission.
AlertDialog.Builder(this@MainActivity).apply {
setTitle("Reason")
setMessage("Reason")
setPositiveButton("Allow") { _, _ -> requestPermLauncher.launch(this@requestSinglePermission) }
setNegativeButton("Deny") { _, _ -> }
}.show()
} else {
// should be called in onCreate()
requestPermLauncher.launch(this)
}
}
}
'Android > Study' 카테고리의 다른 글
[Android] MVP 패턴이란? (0) | 2023.01.01 |
---|---|
[Android] MVC 패턴이란? (0) | 2023.01.01 |
[Android] CustomView 추가 실습 (0) | 2022.09.28 |
[Android] CustomView(커스텀 뷰) (0) | 2022.09.07 |
[Android] 안드로이드 취업시 회사에서 요구하는 기본적인 사항 (0) | 2022.09.04 |