티스토리 뷰

1. 안드로이드 알람 어플 만들어보기 with Kotlin

기본 어플로 있는 알람 어플을 비슷하게 만들어서 사용자가 설정한 시간에 맞춰서 알림이 울리도록 하는 어플을 만들어보았습니다. 아직은 사용 패턴에 대해서 완벽하게는 기억할 수 없어서 포스팅을 통해 정리하고 기록해둘 목적으로 글을 작성합니다.

주요 기능

  • 특정 시간을 설정하여 알람을 등록할 수 있음
  • 만약 지정한 시간이 현재 시간 이전의 시간이라면 다음 날 부터 알람
  • 알람 지정시 매일 지정한 시간 마다 알람

사용 기술

  • AlarmManager
    • Real Time
    • Elapsed Time (기기 부팅 시간을 기준으로 알람)
  • Notification
  • Broadcast receiver
  • TimePickerDialog
  • SystemService

2. 기본 레이아웃 구성

레이아웃의 경우에는 현재 시간 분 을 표시하는 텍스트뷰, 오전/오후 텍스트뷰, 알람 켜기 버튼, 시간 재설정 버튼 으로 구성되어있습니다. 시간 재설정 버튼에서는 TimePickerDialog 를 사용해서 현재시간을 선택할 수 있고 SharedPreferences 에 선택한 시간을 저장합니다.

알람 켜기 버튼은 새로 설정한 시간의 알림을 켜거나 끌 수 있습니다. 이때 전에 설정해둔 알람의 경우에는 취소시키고 새로운 알람으로 등록 시키게 됩니다.

2.1. 시간 재설정 버튼

    private fun initChangeAlarmTimeButton() {
        val changeAlarmButton = findViewById<Button>(R.id.changeAlarmTimeButton)
        changeAlarmButton.setOnClickListener {
            // 현재 시간을 가져오기 위해 캘린더 인스터늣 사
            val calendar = Calendar.getInstance()
            // TimePickDialog 띄워줘서 시간을 설정을 하게끔 하고, 그 시간을 가져와서
            TimePickerDialog(this, { picker, hour, minute ->


                // 데이터를 저장
                val model = saveAlarmModel(hour, minute, false)
                // 뷰를 업데이트
                renderView(model)

                // 기존에 있던 알람을 삭제한다.
                cancelAlarm()

            }, calendar.get(Calendar.HOUR_OF_DAY), calendar.get(Calendar.MINUTE), false)
                .show()

        }
    }

현재 시간을 재설정 하기위해서 캘린더 인스턴스를 가져와 사용합니다. 픽한 시간을 가져와서 우리가 정의해준 모델 클래스로 만들어서 저장하고 이를 통해 뷰를 업데이트 하는 과정을 거칩니다. 이미 알람이 존재하는 경우에는 취소해주어야 하기 때문에 cancelAlarm 도 구현합니다.

get에서 우리는 시간과 분이 필요하기 때문에 위 코드와 같이 설정해주고, 24시간 형식이 아닌 오전/오후 형식을 사용할 것이기 때문에 마지막 false를 줍니다. 모든 설정이 끝났으면 시간 선택 다이얼로그를 띄워주면 되겠습니다.

2.2. 알람 데이터 모델 클래스

알람 데이터를 보관할 모델 클래스입니다.

data class AlarmDisplayModel(
    val hour: Int, // 0~23
    val minute: Int,
    var onOff: Boolean
) {

    fun makeDataForDB(): String {
        return "$hour:$minute"
    }

    // 형식에 맞게 시:분 가져오기.
    val timeText: String
        get() {
            val h = "%02d".format(if (hour < 12) hour else hour - 12)
            val m = "%02d".format(minute)

            return "$h:$m"
        }

    // am pm 가져오기.
    val ampmText: String
        get() {
            return if (hour < 12) "AM" else "PM"
        }

    val onOffText: String
    get(){
        return if(onOff) "알람 끄기" else "알람 켜기"
    }
}

시간, 분, on/off 여부를 가지며 :를 포함한 시간 문자열을 반환해주는 프로퍼티와, am/pm 및 on/off 에 따른 문자열을 반환해주는 프로퍼티를 가지고 있습니다. 해당 클래스를 사용해서 뷰 랜더링을 하게됩니다.

3. SharedPreferences

해당 모델을 사용해서 shared preferences에 저장하게 되고 DB에 저장할 형식을 가져와서 문자열로 put 합니다.

    private fun saveAlarmModel(hour: Int, minute: Int, onOff: Boolean): AlarmDisplayModel {
        val model = AlarmDisplayModel(
            hour = hour,
            minute = minute,
            onOff = onOff
        )

        // time 에 대한 db 파일 생성
        val sharedPreferences = getSharedPreferences(M_SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE)

        // edit 모드로 열어서 작업 (값 저장)
        with(sharedPreferences.edit()) {
            putString(M_ALARM_KEY, model.makeDataForDB())
            putBoolean(M_ONOFF_KEY, model.onOff)
            commit()
        }

        return model
    }

on off 여부는 불리언이기 때문에 따로 지정할 필요없이 Boolean으로 put 해주시면 되겠습니다. 이후 커밋을 통해 저장합니다.

뷰 랜더링

    private fun renderView(model: AlarmDisplayModel) {
        // 최초 실행 또는 시간 재설정 시 들어옴

        findViewById<TextView>(R.id.ampmTextView).apply{
            text = model.ampmText
        }
        findViewById<TextView>(R.id.timeTextView).apply{
            text = model.timeText
        }
        findViewById<Button>(R.id.onOffButton).apply{
            text = model.onOffText
            tag = model
        }
    }

거창하게 랜더한다 했지만 사실 그냥 모델에 있는 데이터를 불러와서 그저 업데이트해서 뿌려주는 것입니다. 알맞은 텍스트를 넣어주고, 버튼의 경우 꺼져있으면 켜기 켜있으면 끄기 가 뜨도록 버튼 텍스트를 변경해줍니다.

tag의 경우에는 Tags can also be used to store data within a view without resorting to another data structure. 라는 주석을 참고해보면 데이터를 저장하는 데 활용할 수 있어서 tag에 모델을 넣어두고 이후에 사용하도록 하겠습니다.

3.1. 앱 시작시 데이터 가져오기

앱 시작시에 SharedPreference에 저장된 데이터를 가져와서 알맞게 알람 모델을 생성하고 예외처리를 해줍니다. 팬딩해둔 인텐트를 가져와서 (인텐트가 가져와지면 알람이 설정되어있는 것) 알람이 설정되어있지 않은데 데이터는 켜져있는 경우 데이터에 끔으로 설정해줍니다.

또는 알람은 켜져있는데 데이터는 꺼져있는 경우에는 알림을 취소합니다. 서로 어긋나는 데이터가 있으면 조정해줘서 알맞게 어플이 동작해주는 과정이라고 생각하시면 되겠습니다.

    private fun fetchDataFromSharedPreferences(): AlarmDisplayModel {
        val sharedPreferences = getSharedPreferences(M_SHARED_PREFERENCE_NAME, Context.MODE_PRIVATE)

        // DB 에서 데이터 가져오기
        val timeDBValue = sharedPreferences.getString(M_ALARM_KEY, "09:30") ?: "09:30"
        val onOffDBValue = sharedPreferences.getBoolean(M_ONOFF_KEY, false)

        // 시:분 형식으로 가져온 데이터 스플릿
        val alarmData = timeDBValue.split(":")

        val alarmModel = AlarmDisplayModel(alarmData[0].toInt(), alarmData[1].toInt(), onOffDBValue)

        // 보정 조정 예외처 (브로드 캐스트 가져오기)
        val pendingIntent = PendingIntent.getBroadcast(
            this,
            M_ALARM_REQUEST_CODE,
            Intent(this, AlarmReceiver::class.java),
            PendingIntent.FLAG_NO_CREATE) // 있으면 가져오고 없으면 안만든다. (null)

        if ((pendingIntent == null) and alarmModel.onOff){
            //알람은 꺼져있는데, 데이터는 켜져있는 경우
            alarmModel.onOff = false

        } else if((pendingIntent != null) and alarmModel.onOff.not()){
            // 알람은 켜져있는데 데이터는 꺼져있는 경우.
            // 알람을 취소함
            pendingIntent.cancel()
        }
        return alarmModel
    }
   

4. 알람 켜기 끄기 버튼 설정

다음은 알람을 켜고 끄는 버튼을 설정하는 코드입니다. 위에서 저는 해당 버튼의 태그에 데이터 모델을 담아둔 것을 기억하시나요? 이제 해당 테크에 저장된 데이터를 사용할 때가 되었습니다. 해당 태그가 알람 모델일 경우 계속진행하며 아닐 경우 바로 반환합니다.

저장된 데이터는 기본적으로 위에서 false를 주어 저장했습니다. 그러므로 모델이 true로 되어있는 상태에서 버튼을 누르게 되었을 경우에는 알람을 등록해주면 되는것이지요. 캘린더 인스턴스를 알람 데이터 시간 기준으로 생성합니다.

    private fun initOnOffButton() {
        val onOffButton = findViewById<Button>(R.id.onOffButton)
        onOffButton.setOnClickListener {
            // 저장한 데이터를 확인한다
            val model = it.tag as? AlarmDisplayModel ?: return@setOnClickListener// 형변환 실패하는 경우에는 null
            val newModel = saveAlarmModel(model.hour,model.minute, model.onOff.not()) // on off 스위칭
            renderView(newModel)

            // 온/오프 에 따라 작업을 처리한다
            if (newModel.onOff){
                // 온 -> 알람을 등록
                val calender = Calendar.getInstance().apply {
                    set(Calendar.HOUR_OF_DAY, newModel.hour)
                    set(Calendar.MINUTE, newModel.minute)
                    // 지나간 시간의 경우 다음날 알람으로 울리도록
                    if (before(Calendar.getInstance())){
                        add(Calendar.DATE, 1) // 하루 더하기
                    }
                }

                //알람 매니저 가져오기.
                val alarmManager = getSystemService(Context.ALARM_SERVICE) as AlarmManager

                val intent = Intent(this, AlarmReceiver::class.java)
                val pendingIntent = PendingIntent.getBroadcast(
                    this,
                    M_ALARM_REQUEST_CODE,
                    intent,
                    PendingIntent.FLAG_UPDATE_CURRENT) // 있으면 새로 만든거로 업데이트

                alarmManager.setInexactRepeating( // 정시에 반복
                    AlarmManager.RTC_WAKEUP, // RTC_WAKEUP : 실제 시간 기준으로 wakeup , ELAPSED_REALTIME_WAKEUP : 부팅 시간 기준으로 wakeup
                    calender.timeInMillis, // 언제 알람이 발동할지.
                    AlarmManager.INTERVAL_DAY, // 하루에 한번씩.
                    pendingIntent
                )
            } else{
                // 오프 -> 알람을 제거
                cancelAlarm()
            }
        }
    }

이때 만약 설정 시간이 현재 시간 이전의 시간으로 설정된 경우는 다음 날 부터 울리도록 해줍니다.

다음으로 시스템 서비스를 사용하여 알람을 등록 해줍니다. 알람 서비스를 알람 매니저로 가져와서 사용할 것이며 PendingIntent 브로드캐스트 인텐트를 설정하고 알람 메니저에 해당 인텐트를 팬딩, 그리고 설정해둔 알람 시간을 바탕으로 알라매니저에 설정해줍니다.

setInexactRepeating 은 정시에 반복하는 알람을 등록하는 것이며 그에 따른 속성 RTC_WAKEUP 의 경우 기기 부팅 시간이 아닌 실제 시간을 바탕으로 알람이 울리도록 합니다. 이 경우 시간대 영역이 차이나는 나라의 경우 옳바르게 작동하지 않을 가능성이 있으나, 현재 우리는 정식 출시하는 것이 아니기에 해당 속성을 사용할 수 있습니다.

알람을 끄는 경우에는 알람을 취소해주면 되겠습니다.

4.1. 알람 취소 시키기

    private fun cancelAlarm(){
        // 기존에 있던 알람을 삭제한다.
        val pendingIntent = PendingIntent.getBroadcast(
            this,
            M_ALARM_REQUEST_CODE,
            Intent(this, AlarmReceiver::class.java),
            PendingIntent.FLAG_NO_CREATE) // 있으면 가져오고 없으면 안만든다. (null)

        pendingIntent?.cancel() // 기존 알람 삭제
    }

알람 취소의 경우 팬딩 인텐트를 가져와서 (기존 요청 코드로 가져옵니다.) 취소하도록 코드를 작성했습니다.

4.2. static 상수 사용하기 (companion object)

    companion object {
        // static 영역 (상수 지정)
        private const val M_SHARED_PREFERENCE_NAME = "time"
        private const val M_ALARM_KEY = "alarm"
        private const val M_ONOFF_KEY = "onOff"
        private val M_ALARM_REQUEST_CODE = 1000
    }

반복되는 키로 사용되는 값들을 companion object 로 정의해서 사용하도록 해서 코드 종속성을 낮춰주었습니다.

<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android"
    android:shape="oval">
    <size
        android:width="250dp"
        android:height="250dp" />
    <solid android:color="@color/background_black" />
    <stroke
        android:width="1dp"
        android:color="@color/white" />
</shape>

알람 시간을 디스플레이할 때 뷰를 사용해서 원형 테두리로 감싸주기 위해 drawable 자원을 사용했습니다. 이는 원형 shape 를 사용해서 간단하게 구현가능합니다.

app:layout_constraintDimensionRatio="H,1:1"

이때 디멘션 레시오 속성을 사용해서 1:1 비율로 지정해주면 보다 나은 뷰의 모습을 설정할 수 있습니다. 🤭

해당 프로젝트의 전체 소스는 저의 깃허브 저장소에 업로드 되어 있으며, 필요하신 분을 위해 공개해두도록 하겠습니다.

2021.06.07 - [Android/App] - [Android] 오늘의 명언 어플 만들기 (Firebase remote config)

2021.06.03 - [Android/App] - [Android] Push 알림 수신기 앱 만들어보기 with Kotlin (Firebase)

2021.05.31 - [Android/App] - [Android] 심플 웹 브라우저 만들어보기 with Kotlin

댓글
최근에 올라온 글
최근에 달린 댓글
네이버 이웃추가
«   2024/03   »
1 2
3 4 5 6 7 8 9
10 11 12 13 14 15 16
17 18 19 20 21 22 23
24 25 26 27 28 29 30
31
글 보관함