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

연락처 가져오기-레포지토리 패턴&서비스 로케이터 패턴, 권한 부여

songyooho 2024. 7. 23. 21:15

1. 설계

-contactMethods에 contact정보를 가져오는 메소드들을 작성

-ContactDataSource는 싱글톤 패턴으로 구현하며 ContactEntities에 연락처 정보를 가지고 있음

-ContactRepositoryImpl에서 데이터 소스를 캡슐화하며 접근 메소드들을 구현

-SeviceLocator은 싱글톤으로 구현하며 ContactRepository에 필요한 Context를 제공

-motionLayout을 이용하여 슬라이드 버튼 구현

 

2. 코드

1)SNS

class SNS(){
    var instagram = ArrayList<String>()
    var github = ArrayList<String>()
    var discord = ArrayList<String>()
}

-SNS정보가 담겨있는 클래스

 

2)ContactEntity

data class ContactEntity(
    //필수 입력 사항
    var name: String,
    var convertedName: String,
    var num: String,
    var tag: Int, //0일반, 1즐겨찾기, 2차단

    //선택 입력 사항
    var img: Uri?,
    var birth: String?,
    var email: String?,
    var sns: SNS?
){
    init {
        sns=SNS()
    }
}

-연락처 정보가 담겨있는 클래스

 

3)contactMethods

fun contactList(context: Context):ArrayList<ContactEntity>{
    val list = ArrayList<ContactEntity>()

    val projection=arrayOf(ContactsContract.CommonDataKinds.Phone.CONTACT_ID, ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
    ContactsContract.CommonDataKinds.Phone.NUMBER,ContactsContract.CommonDataKinds.Photo.PHOTO_URI, ContactsContract.Contacts.STARRED)

    val cursor = context.contentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI,projection,null,null,null)
        ?: return list

    while(cursor.moveToNext()){
        val nameidx=cursor.getColumnIndex(projection[1])
        val numberidx=cursor.getColumnIndex(projection[2])
        val photoidx=cursor.getColumnIndex(projection[3])
        val starredidx=cursor.getColumnIndex(projection[4])

        val name=cursor.getString(nameidx)
        val number=cursor.getString(numberidx)
        val photoUri=cursor.getString(photoidx)?.toUri()
        val starred = cursor.getInt(starredidx)

        list+=ContactEntity(name, convertString(name),number,starred,photoUri,null,null,null)
    }
    return list.sortBy{it.name} as ArrayList<ContactEntity>
}


fun convertString(s:String):String{
    val result=StringBuilder()
    for(i in s){
        if(checkKorean(i)) result.append(getFirstAlphabetKorean(i))
        else result.append(i)
    }
    return result.toString()
}

fun checkKorean(c:Char):Boolean{
    val char=c.toString()
    if(Pattern.matches("^[ㄱ-ㅎ가-힣]*$",char)) return true
    else return false
}

fun getFirstAlphabetKorean(c:Char):Char{
    return ((c.digitToInt()-0xAC00)/28/21).toChar()
}

-연락처 정보를 가져오고 가공하는 역할을 하는 메소드들이 담겨 있는 파일

-초성 검색을 지원하기 위해 한글인 경우 초성들을 뽑아서 함께 저장

 

4)ContactDataSource

class ContactDataSource(application: Application) {
    companion object{
        @Volatile
        private var INSTANCE: ContactDataSource? = null


        fun getContactDataSource(application: Application): ContactDataSource {
            return synchronized(this) {
                val newInstance = INSTANCE ?: ContactDataSource(application)
                INSTANCE = newInstance
                newInstance
            }
        }
    }

    val ContactEntities by lazy { contactList(application) }
}

-싱글톤 패턴으로 구현

-연락처 리스트를 저장해둔다.

 

5)ContactRepository

interface ContactRepository {
    fun getContactList() : ArrayList<ContactEntity>
    fun addContactList(contact: ContactEntity)
    fun modifyContact(idx:Int, contact:ContactEntity)
    fun removeContact(idx:Int)
    fun search(str:String) :ArrayList<ContactEntity>
}

-레포지토리 구현을 위한 인터페이스

 

6)ContactRepositoryImple

class ContactRepositoryImpl(private val contactDataSource: ContactDataSource):ContactRepository {
    override fun getContactList(): ArrayList<ContactEntity> {
        return contactDataSource.ContactEntities
    }

    override fun addContactList(contact: ContactEntity) {
        contactDataSource.ContactEntities.add(contact)
        contactDataSource.ContactEntities.sortBy { it.name }
    }

    override fun modifyContact(idx:Int, contact:ContactEntity) {
        contactDataSource.ContactEntities.set(idx, contact)
    }

    override fun removeContact(idx:Int){
        contactDataSource.ContactEntities.removeAt(idx)
    }

    override fun search(str: String):ArrayList<ContactEntity> {
        val result=ArrayList<ContactEntity>()
        for(i in contactDataSource.ContactEntities){
            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
            }
        }
        return result
    }
}

-데이터 소스를 private로 받아와 외부에서 데이터 소스에 접근을 막음

-리스트받아오기, 수정,추가,삭제,검색을 구현

 

7)MainActivity

class MainActivity : AppCompatActivity() {

    val binding by lazy {
        ActivityMainBinding.inflate(layoutInflater)
    }

    var isGrid = false
    var isContact = true

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        enableEdgeToEdge()
        setContentView(binding.root)
        ViewCompat.setOnApplyWindowInsetsListener(binding.main) { v, insets ->
            val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
            v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
            insets
        }
        val serviceLocator=ServiceLocator.getInstance(application)

        getPermission()

    }

    fun getPermission(){
        if(Build.VERSION.SDK_INT < Build.VERSION_CODES.TIRAMISU) return
        val permissions = arrayOf(android.Manifest.permission.READ_CONTACTS, android.Manifest.permission.CALL_PHONE,
            android.Manifest.permission.POST_NOTIFICATIONS,android.Manifest.permission.SEND_SMS, android.Manifest.permission.INTERNET)
        var flag=false
        for(i in permissions){
            if(checkSelfPermission(i)== PackageManager.PERMISSION_DENIED){
                flag=true
            }
        }
        if(flag) requestPermissions(permissions,0)
        else initView()

    }

    fun initView(){

        with(binding){
            mainLlGridlist.setOnClickListener{
                binding.mainViewWhitebtn.callOnClick()
                isGrid=!isGrid
                lifecycleScope.launch {
                    EventBus.produceEvent(isGrid)
                }
            }

            mainBtnContact.setOnClickListener {
                if(isContact) return@setOnClickListener
                isContact=!isContact
                setFragment(isContact)
            }
            mainBtnMypage.setOnClickListener {
                if(!isContact) return@setOnClickListener
                isContact=!isContact
                setFragment(isContact)
            }

            mainFbtnAdd.setOnClickListener{

            }

            mainFbtnAddalarm.setOnClickListener {

            }
        }
    }

    fun setFragment(isContact: Boolean){
        if(isContact){

        }else{

        }
    }

    override fun onRequestPermissionsResult(
        requestCode: Int,
        permissions: Array<out String>,
        grantResults: IntArray
    ) {
        super.onRequestPermissionsResult(requestCode, permissions, grantResults)
        if(requestCode ==0){
            var flag=true
            for(i in grantResults){
                if(i!=PackageManager.PERMISSION_GRANTED) flag=false
            }
            if(flag) initView()
        }
    }
}

-앱 시작시 권한 체크후 권한을 받고 권한을 모두 수락한 경우에만 리스트가 뜨도록 함. 

-initView()로 초기화 및 클릭 이벤트 설정

 

8)메인 액티비티 레이아웃

<FrameLayout
    android:id="@+id/main_framelayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/buttonBackground"
    android:orientation="horizontal"
    android:padding="2dp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent">

    <androidx.constraintlayout.motion.widget.MotionLayout
        app:layoutDescription="@xml/main_motion"
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <View
            android:id="@+id/main_view_whitebtn"
            app:layout_constraintHorizontal_weight="1"
            android:layout_width="0dp"
            android:layout_height="30sp"
            android:background="@drawable/gridlist_shape"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    </androidx.constraintlayout.motion.widget.MotionLayout>

    <LinearLayout
        android:id="@+id/main_ll_gridlist"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:orientation="horizontal">

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/main_btn_grid"
            android:layout_width="0dp"
            android:layout_height="30sp"
            android:layout_weight="1"
            android:background="@android:color/transparent"
            android:text="@string/main_gridviewbtn"
            android:textAllCaps="false"
            android:textColor="@color/black"
            android:textSize="18sp"
            android:textStyle="bold"
            android:clickable="false"/>

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/main_btn_list"
            android:layout_width="0dp"
            android:layout_height="30sp"
            android:layout_weight="1"
            android:background="@android:color/transparent"
            android:gravity="center"
            android:text="@string/main_listviewbtn"
            android:textAllCaps="false"
            android:textColor="@color/black"
            android:textSize="18sp"
            android:clickable="false"/>
    </LinearLayout>
<MotionScene xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:motion="http://schemas.android.com/apk/res-auto">
    <Transition
        motion:constraintSetStart="@+id/start"
        motion:constraintSetEnd="@+id/end"
        motion:duration="300">

        <OnClick
            motion:targetId="@+id/main_view_whitebtn"
            motion:clickAction="transitionToEnd" />

    </Transition>

    <Transition
        motion:constraintSetStart="@+id/end"
        motion:constraintSetEnd="@+id/start"
        motion:duration="300">
        <OnClick
            motion:targetId="@id/main_view_whitebtn"
            motion:clickAction="transitionToEnd"/>
    </Transition>

    <ConstraintSet android:id="@+id/start">
        <Constraint
            android:id="@+id/main_view_whitebtn"
            android:layout_width="200dp"
            android:layout_height="30sp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintStart_toStartOf="parent"
            motion:layout_constraintTop_toTopOf="parent"/>
    </ConstraintSet>

    <ConstraintSet android:id="@+id/end">
        <Constraint
            android:id="@+id/main_view_whitebtn"
            android:layout_width="200dp"
            android:layout_height="30sp"
            motion:layout_constraintBottom_toBottomOf="parent"
            motion:layout_constraintEnd_toEndOf="parent"
            motion:layout_constraintTop_toTopOf="parent"/>

    </ConstraintSet>
</MotionScene>

-MotionScene에 동작에 대해 정의

-MotionLayout으로 애니메이션 구현