[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