[Do it! 깡샘의 안드로이드 앱 프로그래밍 with 코틀린] 다이얼로그와 알림 이용하기
안드로이드 앱 프로그래밍에서 퍼미션 확인 및 요청 방법, 다양한 다이얼로그와 알림 창 사용법, 소리와 진동 알림 설정, 알림 채널 및 객체 구성, 알림 스타일 적용 방법을 설명한다. 주요 내용으로는 퍼미션 허용 확인과 요청, 토스트 메시지, 날짜와 시간 입력 다이얼로그, 알림 창 구성, 소리 및 진동 알림, 알림 채널 설정, 알림 객체 생성, 알림 터치 이벤트 처리, 알림 스타일 설정 등이 포함된다.
Aug 01, 2024
🌼API 레벨 호환성 고려하기
💡API 레벨 호환성
build.gradle
파일에 설정한 API 레벨에서minSdk
가 24고,targetSdk
가 34라고 가정하자.
- 이 경우 해당 앱은 34버전의 API로 앱을 개발하지만, 24버전 기기에서도 오류가 발생하지 않고 동작해야 한다.
- 안드로이드 API 문서에서 클래스를 찾으면 이름 위에
Added in API level x
처럼 정보가 표시된다. 이는 해당 버전에서 추가된 클래스라는 의미다. - 예를들어
Notification.CallStyle
클래스는 버전 31 하위에서는 제공하지 않으므로 이 클래스를 이용해 앱을 개발하면 31버전 하위 기기에서 오류가 발생한다.
- API 레벨 호환성 문제가 발생하는 클래스나 함수를 사용하면 안드로이드 스튜디오에서 경고나 오류 메시지를 표시한다.
- API 레벨 호환성에 문제가 있는 API를 사용한 함수나 클래스 선언부 위에
@RequiresApi
애너테이션을 추가하면 오류가 발생하지 않는다. @RequiresApi
대신@TargetApi
애너테이션을 이용해도 된다.
// API 호환성 애너테이션
@RequiresApi(Build.VERSION_CODES.S)
// API 레벨 30 이상에서만 addCallback() 함수 실행
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S){
val builder: Notification.Builder = Notification.Builder(this, "1")
.setStyle(
Notification.CallStyle.forIncomingCall(caller, declineIntent, answerIntent)
)
}
🌼퍼미션 설정하기
💡퍼미션 설정과 사용 설정
- 퍼미션이랑 앱의 특정 기능에 부여하는 접근 권한을 말한다.
- A앱과 B앱이 있고 A앱이 컴포넌트를 B에서 사용하는 상황을 생각해 보자.
- 만약 A앱의 컴포넌트에 퍼미션을 설정하면 B 앱에서 연동할 때 문제가 발생한다.
- A앱의 개발자가 매니페스트 파일에
<permission>
태그로 퍼미션을 설정하면 B앱에서는 실행되지 않는다. 이때는 B앱의 매니페스트 파일에<uses-permission>
태그로 해당 퍼미션을 이용하겠다고 설정해 줘야 한다. <permission>
: 기능을 보호하려는 앱의 매니페스트 파일에 설정한다.
<permission android:name="com.example.permission.TEST_PERMISSION"
android:label="TEST PERMISSION"
android:description="@string/permission_desc"
android:protectionLevel="dangeroud"/>
<uses-permission>
: 퍼미션으로 보호된 기능을 사용하려는 앱의 매니페스트 파일에 설정한다.<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<permission>
태그와 다음 속성을 이용한다.name
: 퍼미션의 이름이다. 퍼미션을 구별하는 식별자 역할을 한다.label, description
: 퍼미션을 설명한다. 권한 인증 화면에 출력할 정보다.protectionLevel
: 보호 수준이다.normal
: 낮은 수준의 보호다. 사용자에게 권한 허용을 요청하지 않아도 된다.dangerous
: 높은 수준의 보호다. 사용자에게 권한 허용을 요청해야 한다.signature
: 같은 키로 인증한 앱만 실행한다.signatureOrSystem
: 안드로이드 시스템 앱이거나 같은 키로 인증한 앱만 실행한다.
- 매니페스트 파일에
<permission>
을 설정했다고 해서 컴포넌트가 보호되지는 않는다.
<permission>
을 설정한 다음,android:permission
속성으로 이 퍼미션으로 보호하려는 컴포넌트에 적용해야 한다.
<activity android:name=".OneActivity" android:permission="com.example.TEST_PERMISSION">
<intent-filter>
<action android:name="android.intent.action.PICK"/>
</intent-filter>
</activity>
- 시스템이 보호하는 기능은 다음과 같다.
ACCESS_FINE_LOCATION
: 위치 정보 접근ACCESS_NETWORK_STATE
: 네트워크 정보 접근ACCESS_WIFI_STATE
: 와이파이 네트워크 정보 접근BATTERY_STATS
: 배터리 정보 접근BLUETOOTH
: 블루투스 장치에 연결BLUETOOTH_ADMIN
: 블루투스 장치를 검색하고 페어링CAMERA
: 카메라 장치에 접근INTERNET
: 네트워크 연결READ_EXTERNAL_STORAGE
: 외부 저장소에서 파일 읽기WRITE_EXTERNAL_STORAGE
: 외부 저장소에 파일 쓰기READ_PHONE_STATE
: 전화기 정보 접근SEND_SMS
: 문자 메시지 발신RECEIVE_SMS
: 문자 메시지 수신RECEIVE_BOOT_COMPLETED
: 부팅 완료 시 실행VIBRATE
: 진동 울리기
앱을 개발할 때 구글의 기본 앱과 자주 연동하므로, 해당 기능을 사용할 일이 많다.
💡퍼미션 허용 확인
- api 레벨 23 버전부터 퍼미션은 허가제로 바뀌었다. 개발자가
<uses-permission>
으로 선언했더라도 사용자가 권한 화면에서 이를 거부할 수 있게 되었다.
- 그에 따라 매니페스트 파일에
<uses-permission>
을 선언하는 것 뿐만 아니라 앱을 실행할 때 사용자가 퍼미션을 거부했는지 확인하고, 거부했다면 다시 퍼미션을 허용해 달라고 요청해야 한다.
- 사용자가 퍼미션을 허용했는지 확인하려면
checkSelfPermission()
함수를 이용한다.
// 퍼미션 허용 확인 예
val status = ContextCompat.checkSelfPermission(this, "android.permission.ACCESS_FINE_LOCATION")
if(status == PackageManager.PERMISSION_GRANTED){
Log.d("kkang", "permission granted")
}else{
Log.d("kkang", "permission denied")
}
- 만약 퍼미션을 거부한 상태라면
ActivityResultLauncher
를 이용해 퍼미션 허용을 요청해야 한다.
// 퍼미션 허용 요청 확인
val requestPermissionLauncher = registerForActivityResult(
ActivityResultContracts.RequestPermission()
){ isGranted ->
if(isGranted) {
Log.d("kkang", "callback, granted..")
}else{
Log.d("kkang", "callback, denied..")
}
}
// 퍼미션 허용 요청 실행
requestPermissionLauncher.launch("android.permission.ACCESS_FINE_LOCATION")
🌼다양한 다이얼로그
- 다이얼로그란 사용자와 상호 작용하는 대화상자다.
💡토스트 메시지 띄우기
- 토스트는 화면 아래쪽에 잠깐 보였다가 사라지는 문자열이다. 사용자에게 간단한 메시지로 특정한 상황을 알릴 때 사용한다.
// 토스트 출력 예
val toast = Toast.makeText(this, "종료하시려면 한 번 더 누르세요", Toast.LENGTH_SHORT)
toast.show()
- 토스트가 화면에 보이거나 사라지는 순간을 콜백으로 감지해 특정 로직을 수행하게 할 수도 있다.
// 콜백 기능 이용하기
@RequiresApi(Build.VERSION_CODES.R) // API 레벨 호환성 애너테이션
fun showToast() {
val toast = Toast.makeText(this, "종료하시려면 한 번 더 누르세요", Toast.LENGTH_SHORT)
toast.addCallback(
object : Toast.Callback() {
override fun onToastHidden() {
super.onToastHidden()
Log.d("kkang","toast hidden")
}
override fun onToastShown() {
super.onToastShown()
Log.d("kkang","toast shown")
}
})
toast.show()
}
💡날짜 또는 시간 입력받기
- 날짜를 입력받을 떄는 데이트 피커 다이얼로그를, 시간을 입력받을 때는 타임 피커 다이얼로그를 사용한다.
// 데이트 피커 다이얼로그 사용 예
DatePickerDialog(this, object: DatePickerDialog.OnDateSetListener{
override fun onDateSet(p0: DatePicker?, p1: Int, p2: Int, p3: Int){
Log.d("kkang","year : $p1, month : $p2, dayOfMonth : $p3")
}
}, 2024, 4, 6).show()
// 타임 피커 다이얼로그 사용 예
TimePickerDialog(this, object: TimePickerDialog.OnTimeSetListener{
override fun onTimeSet(p0: TimePicker?, p1: Int, p2: Int){
Log.d("kkang","time : $p1, minute: $p2")
}
}, 15, 0, true).show()
💡알림 창 띄우기
- 안드로이드 다이얼로그의 기본은
AlertDialog
다. 알림 창은 제목, 내용, 버튼 영역으로 구성되며 지정하지 않은 영역이 있다면 해당 영역은 나오지 않는다.
- 알림 창의 버튼은 최대 3개까지만 추가할 수 있고, 같은 함수를 여러 번 사용하면 버튼은 중복되어 하나만 나타난다.
// 알림 창 띄우기
AlertDialog.Builder(this).run{
setTitle("test dialog")
setIcon(android.R.drawable.ic_dialog_info)
setMessage("정말 종료하시겠습니까?")
setPositiveButton("OK", null)
setNegativeButton("Cancel", null)
setNeutralButton("More", null)
setPositiveButton("Yes", null)
setNegativeButton("No", null)
show()
}
- 버튼 함수를
setPositiveButton, setNegativeButton, setNeutralButton
으로 구분하는 이유는 이벤트 핸들러에서 버튼 클릭을 구분하기 위해서다.
// 버튼의 이벤트 핸들러 등록
val eventHandler = object : DialogInterface.OnClickListener{
override fun onClick(p0: DialogInterface?, p1: Int) {
if(p1==DialogInterface?, p1: Int) {
Log.d("kkang","positive button click")
}else if(p1==DialogInterface.BUTTON_NEGATIVE){
Log.d("kkang","negative button click")
}
}
}
setPositiveButton("OK",eventHandler)
setNegativeButton("Cancel",eventHandler)
- 목록을 출력하고 선택받는 알림 창을 만들 수도 있다.
// 체크박스를 포함하는 목록을 출력하는 알림 창
val items = arrayOf<String>("사과", "복숭아", "수박", "딸기")
AlertDialog.Builder(this).run{
setTitle("items test")
setIcon(android.R.drawable.ic_dialog_info)
setMultiChoiceItems(items, booleanArrayOf(false, false, false, false), object:
DialogInterface.OnMultiChoiceClickListener{
override fin onClick(p0: DialogInterface?, p1: Int, p2: Boolean){
Log.d("kkang","${items[p1]}이 ${if(p2) "선택되었습니다." else "선택 해제되었습니다."}")
}
})
setCancleable(false) // 사용자가 기기의 뒤로 가기 버튼을 눌렀을 때 닫지 않게 설정
setPositiveButton("닫기", null)
show()
}.setCanceledOnTouchOutside(false) // 알림 창의 바깥을 터치했을 때 닫지 않게 설정
// 라디오 버튼을 포함하는 예
setSingleChoiceItems(items, 1, object:DialogInterface.OnClickListener{
override fin onClick(p0: DialogInterface?, p1: Int){
Log.d("kkang","${items[p1]}이 선택되었습니다.")
}
})
코틀린의 if~else 문은 자바와 다르게 표현식으로 사용할 수 있다.
표현식이란 실행 결과가 발생하는 구문을 의미한다.
🌼소리와 진동 알림
💡소리 알림
- 알림음은 시스템에 등록된 소리를 이용할 수도 있다.
// 소리 얻기
val notification: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val ringTone = RingtoneManager.getRingtone(applicationContext, notification)
ringtone.play()
- 알림음은 앱에서 자체 음원을 준비해서 이용할 수도 있다.
// 음원 재생하기
val player: MediaPlayer = MediaPlayer.create(this, R.raw.fallbackring)
player.start()
💡진동 알림
- 진동을 울리기 위해선 먼저 매니페스트 파일에 퍼미션을 얻어야 한다.
<uses-permission android:name="android.permision.VIBRATE"/>
- 진동은
Vibrator
객체를 얻어서 이용한다. 31 버전부터는VIBRATOR_MANAGER_SEREVICE
로 식별되는VibratorManager
시스템 서비스를 얻고 이 서비스에서Vibrator
를 이용해야 한다.
// 진동 객체 얻기
val vibrator = if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S){
val vibratorManager = this.getSystemService(Context.VIBRATOR_MANAGER_SEREVICE)
as VibratorManager
vibratorManager.defaultVibrator;
}else{
getSystemService(VIBRATOR_SERVICE) as a Vibrator
}
// 기본 세기로 진동 울리기
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.0){
vibrator.vibrate(
VibrationEffect,createOneShot(500,
VibrationEffect.DEFAULT_AMPLITUDE))
}else{
vibrator.vibrate(500)
}
// 패턴대로 반복해서 울리기
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.0){
vibrator.vibrate(VibrationEffect.createWaveform(longArrayOf(500,1000,500,2000),
intArrayOf(0,50,0,200), -1))
}else{
vibrator.vibrate(longArrayOf(500,1000,500,2000), -1)
}
🌼알림 띄우기
💡알림 채널
- 상태 바에 앱의 정보를 출력하는 것을 알림이라고 한다.
- API 레벨 33 버전부터는 앱에서 알림을 띄우기 위해 사용자에게 퍼미션을 요청해야 한다.
<uses-permission android:name="android.permision.POST_NOTIFICATIONS"/>
- API 레벨 26부터 채널이라는 개념이 추가되었는데 채널별로 알림을 설정할 수 있다.
- 알림의 중요도도 매개변수로 받으며,
IMPORTANCE_HIGH, IMPORTANCE_DEFAULT, IMPORTANCE_LOW, IMPORTANCE_MIN
수준으로 낮아진다.
- 그 외 채널의 각종 정보는 함수와 프로퍼티로 설정할 수 있다.
// 알림 빌더 작성
val manager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
val builder = NotificationCompact.Builder
if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.0){
val channelId = "one-channel"
val channelName = "My Channel One"
val channel = NotificationChannel(
channelId,
channelName,
NotificationManager.IMPORTANCE_HIGH
)
// 채널에 다양한 정보 설정
channel.description = "My Channel One Description"
channel.setShowBadge(true)
val uri: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
val audioAttributes = AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
.setUsage(AudioAttributes.USAGE_ALARM)
.build()
channel.setSound(uri, audioAttributes)
channel.enableLights(true)
channel.lightColor = Color.RED
channel.enableVibration(true)
channel.vibrationPattern = longArrayOf(100,200,100,200)
// 채널을 NotificationManager에 등록
manager.createNotificationChannel(channel)
// 채널을 이용해 빌더 생성
builder = NotificationCompat.Builder(thism channelId)
}else{
builder = NotificationCompat.Builder(this)
}
💡알림 객체
- 알림 빌더를 만들었으면 이 빌더를 이용해
Notification
객체를 만들어야 한다. 이 객체에 출력할 이미지, 문자열 등의 정보를 담는다.
- 앱에서 알림이 발생하면 상태 바에 출력되는 이미지를
스몰 아이콘
이라고 한다.
// 알림 객체 설정
builder.setSmallIcon(android.R.drawable.ic_notification_overlay)
builder.setWhen(System.currentTimeMillis())
builder.setContentTitle("Content Title")
builder.setContentText("Content Text")
// 알림 발생
manager.notify(11, builder.build())
// 알림 취소
manager.cancel(11)
// 알림 취소 막기
builder.setAutoCancel(false)
builder.setOngoing(true)
💡알림 구성
- 알림은 앱이 관할하는 화면이 아니므로 앱의 처치 이벤트로 처리할 수 없다. 따라서
onTouchEvent()
로 처리할 수 없다.
- 따라서 알림을 터치했을 때 실행해야 하는 정보를
Notification
객체에 담아두고, 실제 이벤트가 발생하면Notification
객체에 등록된 이벤트 처리 내용을 시스템이 실행하는 구조로 처리한다.
- 사용자가 알림을 터치하면 앱의 액티비티 또는 브로드캐스트 리시버를 실행해야 하는데, 이를 실행하려면
인텐트
를 이용한다.
// 알림 객체에 액티비티 실행 정보 등록
val intent = Intent(this, DetailActivity::class.java)
val pendgingIntent =
PendingIntent.getActivity(this, 10, intent, PendingIntent.FLAG_IMMUTABLE)
builder.setContentIntent(pendingIntent) // 터치 이벤트 등록
클래스 타입의 레퍼런스를 등록할 때
.java
를 추가해야 하는 경우는 자바로 작성된 API를 코틀린에서 이용할 때다.
자바에서는 클래스 타입 레퍼런스를 Class<*>
로 표현하지만 코틀린에서는 KClass<*>
로 표현한다.
따라서 코틀린에서 KClass<*>
로 선언된 API를 이용한다면 .java
가 없어도 된다. 하지만 코틀린에서 자바 API를 이용할 때는 .java
를 추가해서 작성해야 한다.- 알림에는 터치 이벤트 이외에도 액션을 최대 3개까지 추가할 수 있다.
// 액션 등록 함수
open fun addAction(action: Notification.Action!): Notification.Builder
// 액션 빌더 생성자
Builder(icon: Int, title: CharSequence!, intent: PendingIntent!)
// 액션 등록하기
val actionIntent = Intent(this, OneReceiver::class.java)
val actionPendingIntent = PendingIntent.getBroadcast(this, 20, actionIntent, PendingIntent.FLAG_IMMUTABLE)
builder.addAction(
NotificationCompat.Action.Builder(
android.R.drawable.stat_notify_more,
"Action",
actionPendingIntent
).build()
)
- 원격 입력이란 알림에서 사용자 입력을 직접 받는 기법이다.
// 원격 입력
val KEY_TEXT_REPLY = "key_text_reply"
var replyLabel: String = "답장"
var remoteInput: RemoteInput = RemoteInput.Builder(KEY_TEXT_REPLY).run{
setLabel(replyLabel)
build()
}
// 인텐트 준비
val replyIntent = Intent(this, ReplyReceiver::class.java)
val replyPendingIntent = PendingIntent.getBroadcast(this, 30, replyIntent,
Pending.FLAG_MUMTABLE)
// 원격 입력 액션 등록하기
buikder.addAction(
NotificationCompat.Action.Builder(
R.drawable.send,
"답장"
replyPendingIntent
).addRemoteInput(remoteInput).build()
)
// 브로드캐스트 리시버에서 사용자가 입력한 글을 받는 코드
val replyTxt = RemoteInput.getResultsFromIntent(intent)
?.getCharSequence("key_text_reply")
// 알림 갱신
manager.notify(11, builder.build())
- 앱에서 어떤 작업이 이루어지는 데 시간이 걸린다면 알림을 이용해 앱의 진행 상황을 프로그레스에 바로 알려준다.
- 프로그레스 바는 화면을 따로 준비하지 않고 빌더에
setProgress()
함수만 추가하면 자동으로 나온다. - 스레드 같은 프로그램을 이용해 진행값을 계속 바꾸면서 상황을 알려주면 된다.
// 프로그레스 바의 진행값을 증가시키는 스레드
builder.setProgress(100,0,false)
manager.notify(11, builder.build())
thread{
for(i in 1..100){
builder.setProgress(100,i,false)
manager.notify(11,builder.build()
SystemClock.sleep(100)
}
}
💡알림 스타일
- 알림에 큰 이미지를 출력할 때는
BigPictureStyle
을 이용한다.
// 큰 이미지 스타일
val bigPicture = BitmapFactory.decodeResource(resources, R.drawable.test)
val bigStyle = NotificationCompat.BigPictureStyle()
bigStyle.bigPicture(bigPicture)
builder.setStyle(bigStyle)
- 긴 문자열을 출력할 때는
BigTextStyle
을 이용한다.
// 긴 텍스트 스타일
val bigTextStyle = NotificationCompat.BigTextStyle()
bigTextStyle.bigText(resources.getString(R.string.long_text)
builder.setStyle(bigTextStyle)
-
InboxStyle
로 문자열을 목록으로 출력한다.
// 상자 스타일
val style = NotificationCompat.InboxStyle()
style.addLine("1코스 - 수락.불암산코스")
style.addLine("2코스 - 용마.아차산코스")
style.addLine("3코스 - 고덕.일자산코스")
style.addLine("4코스 - 대모.우면산코스")
builder.setStyle(style)
- 메시지 스타일 알림은 여러 사람이 주고받은 메시지를 구분해서 출력할 때 사용한다.
Message
객체로 각각 표현한다.
// Person 객체 생성
val sender1: Person = Person.Builder()
.setName("kkang")
.setIcon(IconCompat.createWithResource(this, R.drawable.person1))
.build()
val sender2: Person = Person.Builder()
.setName("kim")
.setIcon(IconCompat.createWithResource(this, R.drawable.person2))
.build()
// 메시지 객체 생성
val message1 = NotificationCompat.MessagingStyle.Message(
"hello",
System.currentTimeMillis(),
sender1
)
val message2 = NotificationCompat.MessagingStyle.Message(
"world",
System.currentTimeMillis(),
sender2
)
// 메시지 스타일 만들기
val messageStyle = NotificationCompat.MessagingStyle(sender1)
.addMessage(message1)
.addMessage(message2)
builder.setStyle(messageStyle)
🏁결론
안드로이드 앱 프로그래밍에서 퍼미션 확인 및 요청 방법, 다양한 다이얼로그와 알림 창 사용법, 소리와 진동 알림 설정, 알림 채널 및 객체 구성, 알림 스타일 적용 방법을 설명한다. 주요 내용으로는 퍼미션 허용 확인과 요청, 토스트 메시지, 날짜와 시간 입력 다이얼로그, 알림 창 구성, 소리 및 진동 알림, 알림 채널 설정, 알림 객체 생성, 알림 터치 이벤트 처리, 알림 스타일 설정 등이 포함된다.
Share article