코틀린 팀플3-연락처 어플 만들기

서비스 로케이터를 MVVM으로 변경, 알람 기능 확장

songyooho 2024. 7. 26. 21:20

1. Contact - MVVM: 서비스 로케이터 패턴을 사용하던것을 MVVM으로 바꾸었고 데이터소스에 필요한 application은 뷰모델에서 프로바이더가 제공해 주는 방식으로 바꾸었다.

1)ContactRepositoryImple

class ContactRepositoryImpl(private val contactDataSource: ContactDataSource):ContactRepository {

    private val _contacts = MutableSharedFlow<ArrayList<ContactEntity>>()
    private val _callLogs = MutableSharedFlow<ArrayList<CallLogEntity>>()
    private val _mypageContact = MutableSharedFlow<ArrayList<ContactEntity>>()
    private val contacts = _contacts.asSharedFlow()
    private val callLogs = _callLogs.asSharedFlow()
    private val mypageContact = _mypageContact.asSharedFlow()

    override fun getContactList(): Flow<ArrayList<ContactEntity>> = contacts

    override fun getCallLogs(): Flow<ArrayList<CallLogEntity>> = callLogs

    override fun getMypageContact(): Flow<ArrayList<ContactEntity>> =mypageContact

    override suspend fun notNormal() {
        _mypageContact.emit(contactDataSource.ContactEntities.filter { it.tag!=0 } as ArrayList<ContactEntity>)
    }

    override suspend fun search(str: String) {
        val contacts = contactDataSource.ContactEntities
        val result=ArrayList<ContactEntity>()
        for(i in contacts){
            if(str.equals(i.name.slice(0..minOf(str.length-1,i.name.length-1)))
                ||str.equals(i.convertedName.slice(0..minOf(str.length-1,i.name.length-1)))){
                result+=i
            }
        }
        _contacts.emit(result)
    }

    override suspend fun addContact(contactEntity: ContactEntity) {
        contactDataSource.ContactEntities.add(contactEntity)
        contactDataSource.ContactEntities.sortBy { it.name }
        fetchData()
    }
    override suspend fun removeContact(position:Int) {
        contactDataSource.ContactEntities.removeAt(position)
        fetchData()
    }

    override suspend fun modifyContact(position: Int, contactEntity: ContactEntity) {
        contactDataSource.ContactEntities[position] = contactEntity
        fetchData()
    }

    override suspend fun fetchData() {
        _contacts.emit(contactDataSource.ContactEntities)
        _callLogs.emit(contactDataSource.CallLogEntities)
    }



}

-전화기록, 연락처, 즐겨찾기나 차단된 연락처 이 세가지를 sharedFlow로 전달하도록 하였다.

 

2)ContactViewModel

class ContactViewModel(application: Application): AndroidViewModel(application) {

    private val contactRepositoryImpl = ContactRepositoryImpl(ContactDataSource(application))
    val contacts=contactRepositoryImpl.getContactList()
    val callLogs=contactRepositoryImpl.getCallLogs()

    init {
        viewModelScope.launch {
            contactRepositoryImpl.fetchData()
        }
    }

    fun addContact(contact: ContactEntity) {
        viewModelScope.launch {
            contactRepositoryImpl.addContact(contact)
        }

    }

    fun modifyContact(idx:Int, contact:ContactEntity) {
        viewModelScope.launch {
            contactRepositoryImpl.modifyContact(idx,contact)
        }
    }

    fun removeContact(idx:Int){
        viewModelScope.launch {
            contactRepositoryImpl.removeContact(idx)
        }
    }

    fun search(str: String) {
        viewModelScope.launch {
            contactRepositoryImpl.search(str)
        }
    }

    fun fetch(){
        viewModelScope.launch {
            contactRepositoryImpl.fetchData()
        }
    }
}

-기능별로 레포지토리의 메소드를 가져와 실행

 

3)ContactViewModelFactory

class ContactViewModelFactory(private val application: Application): ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if(modelClass.isAssignableFrom(ContactViewModel::class.java)) return ContactViewModel(application) as T
        throw IllegalArgumentException()
    }
}

-뷰모델이 생성될때 application을 줌

 

2. Alarm -MVVM, Room

:MVVM구조를 택하여 설계하였고 Room을 이용해 재시작되더라도 알람이 유지되도록 하였다.

 

1)AlarmEntity

@Entity(tableName = "alarms")
data class AlarmEntity(
    @PrimaryKey(autoGenerate = true)
    val alarmCode:Long,
    val name: String
)

-room의 구성 요소

 

2)AlarmDao

@Dao
interface AlarmDao {
    @Query("select * from alarms")
    fun getAlarms() : List<AlarmEntity>

    @Insert(onConflict = OnConflictStrategy.REPLACE)
    fun addAlarm(item: AlarmEntity)

    @Query("DELETE FROM alarms WHERE alarmCode = :alarmCode")
    fun deleteAlarm(alarmCode: Long)
}

-쿼리

 

3)AlarmDataBase

@Database( entities = [AlarmEntity::class], version = 1)
abstract class AlarmDataBase: RoomDatabase() {
    abstract fun alarmDao(): AlarmDao

    companion object{
        private var INSTANCE: AlarmDataBase? =null
        fun getInstance(context: Context): AlarmDataBase{
            return synchronized(this){
                val newInstance = INSTANCE?: Room.databaseBuilder(context.applicationContext, AlarmDataBase::class.java, "alarm.db").build()
                INSTANCE = newInstance
                newInstance
            }
        }
    }
}

-데이터 베이스로 싱글톤으로 관리

 

4)AlarmReceiver

class AlarmReceiver(): BroadcastReceiver() {

    private lateinit var manager: NotificationManager
    private lateinit var builder: NotificationCompat.Builder

    private val myNotificationID = 1
    private val channelID = "default"

    @SuppressLint("UnspecifiedImmutableFlag")
    override fun onReceive(context: Context?, intent: Intent?) {
        manager=context?.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager

        if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.O){
            manager.createNotificationChannel(
                NotificationChannel(
                    channelID,
                    "default channel",
                    NotificationManager.IMPORTANCE_DEFAULT
                )
            )
        }

        builder = NotificationCompat.Builder(context,channelID)

        val intents = Intent(context, MainActivity::class.java)
        val requestCode = intent?.extras!!.getInt("alarm_requestCode")
        val contextText = intent?.extras!!.getString("alarm_content")

        val pendingIntent =
            if(Build.VERSION.SDK_INT>=Build.VERSION_CODES.S)
                PendingIntent.getActivity(context,requestCode,intents,PendingIntent.FLAG_IMMUTABLE)
            else PendingIntent.getActivity(context,requestCode,intents,PendingIntent.FLAG_UPDATE_CURRENT)
        val notification = builder.setSmallIcon(R.mipmap.ic_launcher)
            .setContentTitle("연락처 알림")
            .setContentText(contextText)
            .setPriority(NotificationCompat.PRIORITY_DEFAULT)
            .setContentIntent(pendingIntent)
            .setAutoCancel(true)
            .build()
        if (ActivityCompat.checkSelfPermission(
                context,
                Manifest.permission.POST_NOTIFICATIONS
            ) != PackageManager.PERMISSION_GRANTED
        ) {
            (context as Activity).requestPermissions(arrayOf(Manifest.permission.POST_NOTIFICATIONS),0)
            return
        }
        manager.notify(myNotificationID,notification)
    }
}

-브로드캐스트리시버로 알림을 띄우는 역할을 한다.

 

5)AlarmCall

class AlarmCall(private val context: Context) {

    private lateinit var pendingIntent: PendingIntent

    fun callAlarm(name:String, alarmCode:Long){
        val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
        val receiverIntent = Intent(context, AlarmReceiver::class.java).apply {
            putExtra("alarm_requestCode",alarmCode.toInt())
            putExtra("alarm_content",name+"에게 연락할 시간입니다.")
        }

        val pendingIntent =
            if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.S)
                PendingIntent.getBroadcast(context, alarmCode.toInt(),receiverIntent,PendingIntent.FLAG_IMMUTABLE)
            else PendingIntent.getBroadcast(context, alarmCode.toInt(),receiverIntent,PendingIntent.FLAG_UPDATE_CURRENT)

        if(Build.VERSION.SDK_INT>=31&&!alarmManager.canScheduleExactAlarms()){
            (context as Activity).requestPermissions(arrayOf(android.Manifest.permission.SCHEDULE_EXACT_ALARM),0)
        }else{
            alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, alarmCode,pendingIntent)
        }
    }


}

-예약된 시간에 맞춰 알람을 보내는 역할을 한다.

 

6)AlarmRepositoryImple

class AlarmRepositoryImpl(private val db: AlarmDataBase):AlarmRepository {

    override suspend fun addAlarm(alarmEntity: AlarmEntity) {
        db.alarmDao().addAlarm(alarmEntity)
    }

    override suspend fun removeAlarm(alarmCode: Long) {
        db.alarmDao().deleteAlarm(alarmCode)
    }
}

-저장된 알람에 대한 레포지토리

 

7)AlarmViewModel

class AlarmViewModel(application: Application):AndroidViewModel(application) {

    private val alarmRepositoryImpl =AlarmRepositoryImpl(AlarmDataBase.getInstance(application))

    fun addAlarm(alarmEntity: AlarmEntity){
        CoroutineScope(Dispatchers.IO).launch {
            alarmRepositoryImpl.addAlarm(alarmEntity)
        }
    }

    fun removeAlarm(alarmCode: Long){
        CoroutineScope(Dispatchers.IO).launch {
            alarmRepositoryImpl.removeAlarm(alarmCode)
        }
    }
}

-알람 뷰모델

 

8)AlarmViewModelFactory

class AlarmViewModelFactory(private val application: Application): ViewModelProvider.Factory {
    @Suppress("UNCHECKED_CAST")
    override fun <T : ViewModel> create(modelClass: Class<T>): T {
        if (modelClass.isAssignableFrom(AlarmViewModel::class.java)) return AlarmViewModel(application) as T
        throw IllegalArgumentException()
    }
}

-db접근을 위해 application을 주는 역할

 

9)RebootAlarmReceiver

class RebootAlarmReceiver: BroadcastReceiver() {

    private val coroutineScope by lazy { CoroutineScope((Dispatchers.IO)) }

    override fun onReceive(context: Context, intent: Intent) {

        val db = AlarmDataBase.getInstance(context)

        if(intent.action.equals("android.intent.action.BOOT_COMPLETED") ){
            AlarmCall(context).run {
                coroutineScope.launch {
                    val db=AlarmDataBase.getInstance(context)
                    val list=db.alarmDao().getAlarms()
                    if(list.isNotEmpty()){
                        for(i in 0..list.size-1){
                            val name=list[i].name
                            val alarmCode=list[i].alarmCode
                            if(alarmCode>System.currentTimeMillis()) callAlarm(name,alarmCode)
                            db.alarmDao().deleteAlarm(alarmCode)
                            println("${name} / ${alarmCode}")
                        }
                    }else println("Receive: none")
                }
            }
        }
    }

}

-재시작시 룸에 저장된 알람을 다시 실행시키는 역할

 

 

3. 트러블슈팅

-알람db에 접근하려는데 에러발생

=>코루틴스코프를 lifecycleScope에서 CoroutineScope(Dispatcher.IO)로 바꿔 해결

 

-gradle에서 dependecies에 kapt(~)가 에러발생하는것

=>플러그인에서

id("kotlin-kapt")

를 넣고 먼저 싱크를 돌린뒤 넣으니 에러발생안함