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")
를 넣고 먼저 싱크를 돌린뒤 넣으니 에러발생안함
'코틀린 팀플3-연락처 어플 만들기' 카테고리의 다른 글
알람, 권한 부여, 애니매이션, 착발신 기록 가져오기 (0) | 2024.07.24 |
---|---|
연락처 가져오기-레포지토리 패턴&서비스 로케이터 패턴, 권한 부여 (0) | 2024.07.23 |