20230915_TIL

이상훈·2023년 9월 14일

TIL

목록 보기
42/83

2023년 9월 11일 18조 팀프로젝트 발표

build.gradle

plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-parcelize'
}

android {
namespace 'com.example.contactapp'
compileSdk 33

defaultConfig {
    applicationId "com.example.contactapp"
    minSdk 24
    targetSdk 33
    versionCode 1
    versionName "1.0"

    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
    jvmTarget = '1.8'
}

viewBinding {
    enabled = true
}

buildFeatures {
    viewBinding = true
}

}

dependencies {
implementation 'androidx.core:core-ktx:1.8.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.5.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
implementation 'androidx.viewpager2:viewpager2:1.0.0'
implementation 'com.google.android.engage:engage-core:1.2.0'
implementation 'org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
implementation 'de.hdodenhof:circleimageview:3.1.0' // 원형 이미지뷰 만들기
implementation 'com.github.bumptech.glide:glide:4.16.0' // glide
annotationProcessor 'com.github.bumptech.glide:compiler:4.11.0'

}

AndroidManifest.xml

<uses-feature
    android:name="android.hardware.telephony"
    android:required="false" />

<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
<uses-permission android:name="android.permission.USE_FULL_SCREEN_INTENT" />
<uses-permission
    android:name="android.permission.READ_EXTERNAL_STORAGE"
    android:maxSdkVersion="32" />
<uses-permission
    android:name="android.permission.WRITE_EXTERNAL_STORAGE"
    android:maxSdkVersion="32"
    tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.READ_CONTACTS" />
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
<uses-permission android:name="android.permission.CALL_PHONE" />

<application
    android:allowBackup="true"
    android:dataExtractionRules="@xml/data_extraction_rules"
    android:fullBackupContent="@xml/backup_rules"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:roundIcon="@mipmap/ic_launcher_round"
    android:supportsRtl="true"
    android:theme="@style/AppTheme"
    tools:targetApi="31">
    <activity
        android:name=".SplashActivity"
        android:exported="true" >
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />

            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
    <activity
        android:name=".main.MainActivity"
        android:exported="true" />
    <activity
        android:name=".signUp.SignUpActivity"
        android:exported="true" />
    <activity
        android:name=".signIn.SignInActivity"
        android:exported="true">

    </activity>
</application>

SplashActivity.kt

package com.example.contactapp

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.bumptech.glide.Glide
import com.example.contactapp.databinding.ActivitySplashBinding
import com.example.contactapp.signIn.SignInActivity

class SplashActivity : AppCompatActivity() {

lateinit var binding: ActivitySplashBinding
override fun onCreate(savedInstanceState: Bundle?) {
    binding = ActivitySplashBinding.inflate(layoutInflater)
    super.onCreate(savedInstanceState)
    setContentView(binding.root)

    val splashImage = binding.imageSplash

    Glide.with(this).load(R.raw.splash_background).into(splashImage)

    splashImage.setOnClickListener {
        startActivity(Intent(this, SignInActivity::class.java))
    }
}

}

Constants.kt

package com.example.contactapp.contact

import android.content.Context
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import com.example.contactapp.R
import java.io.IOException

object Constants{
const val ITEM_OBJECT = "item_object"

fun convertToUri (drawableImg: Int): Uri = Uri.parse("android.resource://" + R::class.java.`package`?.name + "/" + drawableImg)

fun  convertToBitmap(context: Context, uri: Uri): Bitmap? {
    try {
        var inputStream = context.contentResolver.openInputStream(uri)
        return BitmapFactory.decodeStream(inputStream)
    } catch(e: IOException) {
        e.printStackTrace()
    }
    return null
}

}

ContactListFragment.kt

package com.example.contactapp.contact

import android.os.Bundle
import android.provider.ContactsContract
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.GridLayoutManager
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.example.contactapp.R
import com.example.contactapp.contact.Constants.ITEM_OBJECT
import com.example.contactapp.contact.Constants.convertToUri
import com.example.contactapp.contact.ContactModelDB.dataList
import com.example.contactapp.databinding.FragmentContactListBinding
import com.example.contactapp.detail.DetailFragment

class ContactListFragment : Fragment() {
private var _binding: FragmentContactListBinding? = null
private val binding get() = _binding!!

private val recyclerViewAdapter by lazy {
    ContactRecyclerViewAdapter(requireContext(), dataList)
}

private val contactRealList by lazy {
    getContacts()
}

private val recyclerViewRealAdapter by lazy {
    ContactRecyclerViewAdapter(requireContext(), contactRealList)
}

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
    _binding = FragmentContactListBinding.inflate(inflater, container, false)
    return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    initView()
}

private fun initView() = with(binding) {
    recyclerViewContact.layoutManager = LinearLayoutManager(requireContext())
    recyclerViewAdapter.isGridLayout(false)
    recyclerViewContact.adapter = recyclerViewAdapter

    val itemHelperCallback = ContactListItemHelper(requireContext(), this@ContactListFragment)
    val itemTouchHelper = ItemTouchHelper(itemHelperCallback)
    itemTouchHelper.attachToRecyclerView(recyclerViewContact)

    recyclerViewAdapter.itemLikeClick = object : ContactRecyclerViewAdapter.ItemClick {
        override fun onClick(view: View, position: Int) {
            updateContactList(dataList[position], position)
        }
    }

    recyclerViewAdapter.itemClick = object : ContactRecyclerViewAdapter.ItemClick {
        override fun onClick(view: View, position: Int) {
            bundleToDetailFragment(dataList[position])
        }
    }

    recyclerViewRealAdapter.itemClick = object : ContactRecyclerViewAdapter.ItemClick {
        override fun onClick(view: View, position: Int) {
            bundleToDetailFragment(contactRealList[position])
        }
    }
}

fun addContactList(contact: ContactModel) {
    recyclerViewAdapter.addItem(contact)
}

fun updateContactList(contact: ContactModel, position: Int) {
    recyclerViewAdapter.updateItem(contact, position)
}

fun updateSwipeItem(viewHolder: RecyclerView.ViewHolder) {

// recyclerViewAdapter.notifyItemChanged(viewHolder.adapterPosition)
recyclerViewAdapter.notifyDataSetChanged()
}

fun bundleToDetailFragment(contact: ContactModel) {
    val detailFragment = DetailFragment()
    val bundle = bundleOf(ITEM_OBJECT to contact)

    detailFragment.arguments = bundle

// detailFragment.arguments = Bundle().apply {
// bundleOf(ITEM_OBJECT to dataList[position])
// }
requireActivity().supportFragmentManager.beginTransaction()
.add(R.id.mainConstrainLayout, detailFragment)
.addToBackStack(null)
.commit()
}

fun showGridView() = with(binding) {
    recyclerViewContact.layoutManager = GridLayoutManager(requireContext(), 3)
    recyclerViewAdapter.isGridLayout(true)
    recyclerViewContact.adapter = recyclerViewAdapter

}

fun showRecyclerView(num: Int) = with(binding) {
    recyclerViewContact.layoutManager = LinearLayoutManager(requireContext())
    recyclerViewAdapter.isGridLayout(false)
    when(num) {
        0 -> recyclerViewContact.adapter = recyclerViewAdapter
        1 -> recyclerViewContact.adapter = recyclerViewRealAdapter
    }
}

private fun getContacts(): MutableList<ContactModel> {
    val contactList = mutableListOf<ContactModel>()
    val cursor = requireContext().contentResolver.query(
        ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
        arrayOf<String>(
            ContactsContract.CommonDataKinds.Phone.PHOTO_URI,
            ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
            ContactsContract.CommonDataKinds.Phone.NUMBER
        ),
        null,
        null,
        null
    )
    if(cursor != null) {

// var count = 0
// while(cursor.moveToNext() && count < 15) {
// count++ //임의로 15개까지만 출력
while(cursor.moveToNext()) {
contactList.add(ContactModel(convertToUri(R.drawable.ic_empty_user), cursor.getString(1), "", cursor.getString(2), "", "", 0))
}
}
return contactList
}
override fun onDestroyView() {
super.onDestroyView()
_binding = null //구글 권장 메모리 누수 방지
}
}

ContactListItemHelper.kt

package com.example.contactapp.contact

import android.content.Context
import android.content.Intent
import android.graphics.Canvas
import android.net.Uri
import androidx.recyclerview.widget.ItemTouchHelper
import androidx.recyclerview.widget.RecyclerView

class ContactListItemHelper(private val context: Context, private val fragment: ContactListFragment): ItemTouchHelper.Callback() {
override fun isItemViewSwipeEnabled(): Boolean {
return true
}

override fun isLongPressDragEnabled(): Boolean {
    return false
}

override fun onChildDraw(
    canvas: Canvas, recyclerView: RecyclerView,
    viewHolder: RecyclerView.ViewHolder,
    dX: Float, dY: Float, actionState: Int, isCurrentlyActive: Boolean
) {
    if (actionState == ItemTouchHelper.ACTION_STATE_SWIPE) {
        val view = (viewHolder as ContactRecyclerViewAdapter.RecyclerHolder).linearLayout
        getDefaultUIUtil().onDraw(canvas, recyclerView, view, dX, dY, actionState, isCurrentlyActive)
    }
}

override fun clearView(
    recyclerView: RecyclerView,
    viewHolder: RecyclerView.ViewHolder
) {
    super.clearView(recyclerView, viewHolder)
    getDefaultUIUtil().clearView((viewHolder as ContactRecyclerViewAdapter.RecyclerHolder).linearLayout)
}

override fun getMovementFlags(
    recyclerView: RecyclerView,
    viewHolder: RecyclerView.ViewHolder
): Int {
    return makeMovementFlags(0, ItemTouchHelper.RIGHT)
}

override fun onMove(
    recyclerView: RecyclerView,
    viewHolder: RecyclerView.ViewHolder,
    target: RecyclerView.ViewHolder
): Boolean = false

override fun onSwiped(viewHolder: RecyclerView.ViewHolder, direction: Int) {
    val contactViewHolder = viewHolder as ContactRecyclerViewAdapter.RecyclerHolder
    val phoneNumber = contactViewHolder.contactNumber

    if(direction == ItemTouchHelper.RIGHT) {
        val callUriSwipedPerson = Uri.parse("tel:${phoneNumber}")
        context.startActivity(Intent(Intent.ACTION_CALL, callUriSwipedPerson))
        fragment.updateSwipeItem(contactViewHolder)
    }
}

}

ContactModel.kt

package com.example.contactapp.contact

import android.net.Uri
import android.os.Parcelable
import com.example.contactapp.R
import com.example.contactapp.contact.Constants.convertToUri
import kotlinx.parcelize.Parcelize

@Parcelize
data class ContactModel(
val profile: Uri,
val name: String,
val locale: String,
val phoneNum: String,
val email: String,
val ability: String,
val isHeart: Int
) : Parcelable

object ContactModelDB {
var dataList = mutableListOf()
init {
dataList.apply{
add(ContactModel(convertToUri(R.drawable.img_kds), "김두식", "문산", "010-1111-1111", "kds@gmail.com", "비행", 0))
add(ContactModel(convertToUri(R.drawable.img_lmh), "이미현", "", "010-3333-3333", "lmh@gmail.com", "초인적 오감", 0))
add(ContactModel(convertToUri(R.drawable.img_ljm), "이재만", "", "010-6666-6666", "ljm@gmail.com", "괴력, 스피드", 0))
add(ContactModel(convertToUri(R.drawable.img_jjw), "장주원", "구룡포", "010-2222-2222", "jjw@gmail.com", "무한 재생", 0))
add(ContactModel(convertToUri(R.drawable.img_jgd), "전계도", "", "010-4444-4444", "jgd@gmail.com", "전기", 0))
add(ContactModel(convertToUri(R.drawable.img_jys), "전영석", "봉평", "010-8888-8888", "jys@gmail.com", "전기", 0))
add(ContactModel(convertToUri(R.drawable.img_jsj), "정상진", "진천", "010-7777-7777", "jsj@gmail.com", "괴력", 0))
add(ContactModel(convertToUri(R.drawable.img_hsh), "홍성화", "나주", "010-9999-9999", "hsh@gmail.com", "투시", 0))
}
}
}

ContactRecyclerViewAdapter.kt

package com.example.contactapp.contact

import android.content.Context
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.LinearLayout
import androidx.recyclerview.widget.RecyclerView
import com.example.contactapp.R
import com.example.contactapp.contact.Constants.convertToBitmap
import com.example.contactapp.databinding.ContactGridviewItemBinding
import com.example.contactapp.databinding.ContactRecyclerviewItemBinding

class ContactRecyclerViewAdapter (private val context: Context, private val contactList: MutableList) : RecyclerView.Adapter<RecyclerView.ViewHolder>(){
companion object {
const val ITEM_VIEW_TYPE_GRID = 0
const val ITEM_VIEW_TYPE_LTR = 1
const val ITEM_VIEW_TYPE_RTL = 2
}

interface ItemClick {
    fun onClick(view : View, position : Int)
}

var itemClick : ItemClick? = null
var itemLikeClick: ItemClick? = null

private var isGridLayout: Boolean = false
fun isGridLayout(bool: Boolean) {
    this.isGridLayout = bool
}

override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
    return when(viewType) {
        ITEM_VIEW_TYPE_GRID -> {
            val binding = ContactGridviewItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
            GridHolder(binding)
        }
        else -> {
            val binding = ContactRecyclerviewItemBinding.inflate(LayoutInflater.from(parent.context), parent, false)
            RecyclerHolder(binding)
        }
    }
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
    when(holder) {
        is GridHolder -> holder.bind(contactList[position])
        is RecyclerHolder -> {
            holder.bind(contactList[position])
            if(position % 2 == 0) {
                holder.linearLayout.layoutDirection = View.LAYOUT_DIRECTION_LTR
            } else {
                holder.linearLayout.layoutDirection = View.LAYOUT_DIRECTION_RTL
            }
        }
    }
}

override fun getItemCount(): Int = contactList.size

override fun getItemViewType(position: Int): Int {
    return if(isGridLayout) {
        ITEM_VIEW_TYPE_GRID
    } else {
        if(position % 2 == 0) {
            ITEM_VIEW_TYPE_LTR
        } else {
            ITEM_VIEW_TYPE_RTL
        }
    }
}

inner class GridHolder(private val binding: ContactGridviewItemBinding) : RecyclerView.ViewHolder(binding.root) {
    fun bind(contact: ContactModel) = with(binding) {
        itemView.setOnClickListener {
            itemClick?.onClick(it, adapterPosition)
        }
        imgProfile.setImageBitmap(convertToBitmap(context, contact.profile))
        txtName.text = contact.name
        txtAbility.text = contact.ability
    }
}

inner class RecyclerHolder(private val binding: ContactRecyclerviewItemBinding) : RecyclerView.ViewHolder(binding.root) {
    lateinit var linearLayout: LinearLayout
    lateinit var contactNumber: String
    fun bind(contact: ContactModel) = with(binding) {
        linearLayout = contactLinearLayout
        contactNumber = contact.phoneNum

        itemView.setOnClickListener {
            itemClick?.onClick(it, adapterPosition)
        }

        imgLike.setOnClickListener {
            itemLikeClick?.onClick(it, adapterPosition)
        }

        imgProfile.setImageBitmap(convertToBitmap(context, contact.profile))
        val localeTxt = if(contact.locale.isNotEmpty()) " (${contact.locale}) " else contact.locale
        val abilityTxt = if(contact.ability.isNotEmpty()) "- ${contact.ability}" else contact.ability
        txtInfo.text = "${contact.name}$localeTxt$abilityTxt"
        imgLike.setImageResource(if(contact.isHeart == 1) R.drawable.ic_full_heart else R.drawable.ic_empty_heart)
    }
}

fun addItem(contact: ContactModel) {
    contactList.add(contact)
    contactList.sortWith(compareBy ({-it.isHeart}, {it.name}))

// notifyItemChanged(contactList.size - 1)
notifyDataSetChanged()
}

fun updateItem(contact: ContactModel, position: Int) {
    contactList[position] = if(contact.isHeart == 1) contact.copy(isHeart = 0) else contact.copy(isHeart = 1)
    contactList.sortWith(compareBy ({-it.isHeart}, {it.name}))

// notifyItemChanged(position)
notifyDataSetChanged()
}
}

DetailFragment.kt

package com.example.contactapp.detail

import android.content.Intent
import android.net.Uri
import android.os.Bundle
import androidx.fragment.app.Fragment
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.content.res.ResourcesCompat
import com.example.contactapp.contact.Constants.ITEM_OBJECT
import com.example.contactapp.contact.Constants.convertToBitmap
import com.example.contactapp.contact.ContactModel
import com.example.contactapp.databinding.FragmentDetailBinding

class DetailFragment : Fragment() {
private var _binding: FragmentDetailBinding? = null
private val binding get() = _binding!!

private val data: ContactModel? by lazy {
    arguments?.getParcelable<ContactModel>(ITEM_OBJECT)
}
lateinit var receiveData: ContactModel

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?, savedInstanceState: Bundle?
): View? {
    _binding = FragmentDetailBinding.inflate(inflater, container, false)
    return binding.root
}

override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
    super.onViewCreated(view, savedInstanceState)
    initView()
}

private fun initView() = with(binding) {
    val receiveData = data ?: receiveData
    imgPhoto.setImageBitmap(convertToBitmap(requireContext(), receiveData.profile))
    tvName.text = receiveData.name
    tvPhoneNumber.text = receiveData.phoneNum
    tvEmail.text = receiveData.email
    tvLocale.text = receiveData.locale
    tvAbility.text = receiveData.ability

    /**
     *  data 값이 null이면 receiveData를 받아오니까, 이 경우가 마이페이지 데이터가 보이는 경우이므로
     *  메세지, 콜 버튼을 사라지게 했습니다!
     */
    if (data == null) {
        btnMessageButton.visibility = View.GONE
        btnCallButton.visibility = View.GONE
    } // 마이페이지 일 때, 메세지 및 콜 버튼 사라지는 기능 완료

    /**
     *  디테일 페이지에서 버튼을 누르면 전화와 문자를 보내는 기능 추가입니다.
     */
    btnCallButton.setOnClickListener {
        val phoneNumber = tvPhoneNumber.text
        val callUriSwipedPerson = Uri.parse("tel:${phoneNumber}")
        startActivity(Intent(Intent.ACTION_CALL, callUriSwipedPerson))
    }

    btnMessageButton.setOnClickListener {
        val phoneNumber = tvPhoneNumber.text
        val sendUriSwipedPerson = Uri.parse("smsto:${phoneNumber}")
        startActivity(Intent(Intent.ACTION_SENDTO, sendUriSwipedPerson))
    } // 디테일 페이지 버튼 기능 완료
}

fun setData(contact: ContactModel) {
    receiveData = contact
}

override fun onDestroyView() {
    super.onDestroyView()
    _binding = null //구글 권장 메모리 누수 방지
}

}

MainActivity.kt

package com.example.contactapp.main

import android.app.Activity
import android.app.NotificationChannel
import android.app.NotificationManager
import android.app.PendingIntent
import android.content.Intent
import android.content.pm.PackageManager
import android.graphics.Color
import android.graphics.drawable.ColorDrawable
import android.net.Uri
import android.os.Build
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.provider.MediaStore
import android.view.View
import android.widget.Toast
import androidx.activity.result.contract.ActivityResultContracts
import androidx.appcompat.app.AlertDialog
import androidx.core.app.ActivityCompat
import androidx.core.app.NotificationCompat
import androidx.core.content.ContextCompat
import androidx.viewpager2.widget.ViewPager2
import com.example.contactapp.R
import com.example.contactapp.contact.Constants.convertToBitmap
import com.example.contactapp.contact.Constants.convertToUri
import com.example.contactapp.contact.ContactListFragment
import com.example.contactapp.contact.ContactModel
import com.example.contactapp.databinding.ActivityMainBinding
import com.example.contactapp.databinding.DialogAddContactBinding
import com.example.contactapp.detail.DetailFragment
import com.google.android.material.tabs.TabLayoutMediator
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch

class MainActivity : AppCompatActivity() {
private lateinit var binding: ActivityMainBinding
private lateinit var dialogBinding: DialogAddContactBinding
private val viewPager2Adapter by lazy {
MainViewPagerAdapter(this@MainActivity)
}
private val contactListFragment by lazy {
viewPager2Adapter.getFragment(0) as? ContactListFragment
}

private var galleryUri: Uri = convertToUri(R.drawable.ic_empty_user)
private val galleryLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
    if(result.resultCode == Activity.RESULT_OK) {
        galleryUri = result.data?.data ?: convertToUri(R.drawable.ic_empty_user)
        dialogBinding.imgProfile.setImageBitmap(convertToBitmap(this, galleryUri))
    }
}

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    binding = ActivityMainBinding.inflate(layoutInflater)
    setContentView(binding.root)
    checkPermission()
}

private fun initView() = with(binding) {
    val checkName = intent.getStringExtra("userName") ?: "name"
    val checkEmailAddress = intent.getStringExtra("userEmailAddress") ?: "emailaddress"
    val checkTel = intent.getStringExtra("userTel") ?: "tel"
    val checkLocale = intent.getStringExtra("userLocale") ?: "locale"
    val checkAbility = intent.getStringExtra("userAbility") ?: "ability"

    val detailFragment = viewPager2Adapter.getFragment(1) as? DetailFragment
    detailFragment?.setData(ContactModel(convertToUri(R.drawable.ic_empty_user), checkName, checkLocale, checkTel, checkEmailAddress, checkAbility, 0))

    viewPager2.adapter = viewPager2Adapter

    viewPager2.registerOnPageChangeCallback(object : ViewPager2.OnPageChangeCallback() {
        override fun onPageSelected(position: Int) {
            super.onPageSelected(position)
            if (viewPager2Adapter.getFragment(position) is ContactListFragment) {
                imgGridView.visibility = View.VISIBLE
                imgListView.visibility = View.VISIBLE
            } else {
                imgGridView.visibility = View.INVISIBLE
                imgListView.visibility = View.INVISIBLE
            }
        }
    })

    TabLayoutMediator(tabLayout, viewPager2) { tab, pos ->
        tab.setText(viewPager2Adapter.getTitle(pos))
    }.attach()

    imgGridView.setOnClickListener {
        contactListFragment?.showGridView()
    }

    imgListView.setOnClickListener {
        contactListFragment?.showRecyclerView(0)
    }

    imgListView.setOnLongClickListener {
        contactListFragment?.showRecyclerView(1)
        true
    }

    btnFab.setOnClickListener { //fab 클릭 리스너
        showCustomDialog()
    }
}

private fun showCustomDialog() {
    dialogBinding = DialogAddContactBinding.inflate(layoutInflater)
    val buildDialog = AlertDialog.Builder(this)
        .setView(dialogBinding.root)
        .create()

    buildDialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))
    buildDialog.show()

    dialogBinding.imgAddProfile.setOnClickListener {
        val intent = Intent(Intent.ACTION_PICK).setDataAndType(MediaStore.Images.Media.EXTERNAL_CONTENT_URI, "image/*")
        galleryLauncher.launch(intent)
    }

    dialogBinding.btnCancel.setOnClickListener {
        buildDialog.dismiss()
    }

    dialogBinding.btnAdd.setOnClickListener {
        val nameEdt = dialogBinding.edtName.text.toString()
        val phoneEdt = dialogBinding.edtPhone.text.toString()
        val emailEdt = dialogBinding.edtEmail.text.toString()
        val localeEdt = dialogBinding.edtLocale.text.toString()
        val abilityEdt = dialogBinding.edtAbility.text.toString()
        if (nameEdt.isBlank() || phoneEdt.isBlank() || emailEdt.isBlank() || localeEdt.isBlank()) {
            Toast.makeText(this, getString(R.string.dialog_no_info_toast), Toast.LENGTH_SHORT)
                .show()
        } else {
            contactListFragment?.addContactList(ContactModel(galleryUri, nameEdt, localeEdt, phoneEdt, emailEdt, abilityEdt, 0))
            var alarmTime = 0
            when(dialogBinding.chipGroup.checkedChipId) {
                R.id.chip_off -> alarmTime = 0
                R.id.chip_5min -> alarmTime = 5 //임시로 5초
                R.id.chip_10min -> alarmTime = 10 //임시로 10초
                R.id.chip_30min -> alarmTime = 15 //임시로 15초
            }
            if(alarmTime != 0) {
                Toast.makeText(this@MainActivity, "${nameEdt}님께 연락할 수 있도록 ${alarmTime}분 이후 알람", Toast.LENGTH_SHORT).show()
                CoroutineScope(Dispatchers.Default).launch {
                    delay(alarmTime * 1000L)
                    setNotification(nameEdt)
                }
            }
            buildDialog.dismiss()
            galleryUri = convertToUri(R.drawable.ic_empty_user)
        }
    }
}

private fun setNotification(nameEdt: String) {
    val notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager
    val channelId = "channel_id"
    val builder: NotificationCompat.Builder
    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { // Android 8.0 이상
        val channel = NotificationChannel(
            channelId,
            "channel_name",
            NotificationManager.IMPORTANCE_DEFAULT
        ).apply {
            // 채널에 다양한 정보 설정
            description = "channel_description"
            setShowBadge(true)
        }
        notificationManager.createNotificationChannel(channel)
        builder = NotificationCompat.Builder(this, channelId)
    } else { // Android 8.0 이하
        builder = NotificationCompat.Builder(this)
    }

    val fullScreenIntent = Intent(this, MainActivity::class.java)
    val fullScreenPendingIntent = PendingIntent.getActivity(this, 0, fullScreenIntent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)

    builder.run {
        setSmallIcon(R.drawable.ic_call)
        setContentTitle(getString(R.string.noti_title))
        setContentText(nameEdt + getString(R.string.noti_message))
        setWhen(System.currentTimeMillis())
        priority = NotificationCompat.PRIORITY_DEFAULT
        setFullScreenIntent(fullScreenPendingIntent, true)
    }
    notificationManager.notify(1, builder.build())
}

private fun checkPermission() {
    if (ContextCompat.checkSelfPermission(this, "android.permission.READ_EXTERNAL_STORAGE") != PackageManager.PERMISSION_GRANTED
        || ContextCompat.checkSelfPermission(this, "android.permission.WRITE_EXTERNAL_STORAGE") != PackageManager.PERMISSION_GRANTED
        || ContextCompat.checkSelfPermission(this, "android.permission.READ_CONTACTS") != PackageManager.PERMISSION_GRANTED
        || ContextCompat.checkSelfPermission(this, "android.permission.CALL_PHONE") != PackageManager.PERMISSION_GRANTED
    ) {
        ActivityCompat.requestPermissions(
            this,
            arrayOf<String>(
                "android.permission.READ_EXTERNAL_STORAGE",
                "android.permission.WRITE_EXTERNAL_STORAGE",
                "android.permission.READ_CONTACTS",
                "android.permission.CALL_PHONE"
            ),
            100
        )
    } else {
        initView()
    }
}

override fun onRequestPermissionsResult(
    requestCode: Int,
    permissions: Array<out String>,
    grantResults: IntArray
) {
    super.onRequestPermissionsResult(requestCode, permissions, grantResults)
    if (grantResults.isNotEmpty()
        && grantResults[0] == PackageManager.PERMISSION_GRANTED
        && grantResults[1] == PackageManager.PERMISSION_GRANTED
        && grantResults[2] == PackageManager.PERMISSION_GRANTED
        && grantResults[3] == PackageManager.PERMISSION_GRANTED
    ) {
        initView()
    } else {
        finish()
    }
}

}

MainTabs.kt

package com.example.contactapp.main

import androidx.fragment.app.Fragment

data class MainTabs(
val fragment: Fragment,
val title: Int
)

MainViewPagerAdapter.kt

package com.example.contactapp.main

import android.content.Context
import android.os.Bundle
import androidx.core.os.bundleOf
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentActivity
import androidx.viewpager2.adapter.FragmentStateAdapter
import com.example.contactapp.R
import com.example.contactapp.contact.ContactListFragment
import com.example.contactapp.databinding.ActivityMainBinding
import com.example.contactapp.detail.DetailFragment

class MainViewPagerAdapter(fragmentActivity: FragmentActivity): FragmentStateAdapter(fragmentActivity) {
private var fragments: ArrayList = ArrayList()

init {
    fragments.add(MainTabs(ContactListFragment(), R.string.main_tab_title_contact))
    fragments.add(MainTabs(DetailFragment(), R.string.main_tab_title_mypage))
}

fun getFragment(position: Int): Fragment {
    return fragments[position].fragment
}

fun getTitle(position: Int): Int {
    return fragments[position].title
}

override fun getItemCount(): Int = fragments.size

override fun createFragment(position: Int): Fragment {



    return fragments[position].fragment
}

}

SignInActivity.kt

package com.example.contactapp.signIn

import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.widget.Button
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import com.bumptech.glide.Glide
import com.example.contactapp.R
import com.example.contactapp.main.MainActivity
import com.example.contactapp.signUp.SignUpActivity

class SignInActivity : AppCompatActivity() {

private lateinit var editTextName: EditText
private lateinit var editTextLocale: EditText
private lateinit var buttonLogin: TextView
private lateinit var buttonSignUp: TextView

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_sign_in)

    /*Glide 배경 움짤 추가 */
    val loginGlide = findViewById<ImageView>(R.id.login_glide)
    Glide.with(this).load(R.raw.login_background).into(loginGlide)
    //

    editTextName = findViewById(R.id.et_login_name)
    editTextLocale = findViewById(R.id.et_login_locale)
    buttonLogin = findViewById(R.id.buttonLogin)
    buttonSignUp = findViewById(R.id.buttonSignUp)

    buttonLogin.setOnClickListener {

        val checkName = intent.getStringExtra("userName") ?: "name"
        val checkEmailAddress = intent.getStringExtra("userEmailAddress") ?: "emailaddress"
        val checkTel = intent.getStringExtra("userTel") ?: "tel"
        val checkLocale = intent.getStringExtra("userLocale") ?: "locale"
        val checkAbility = intent.getStringExtra("userAbility") ?: "ability"

        val loginName = editTextName.text.toString()   // 이름, 암호명으로 로그인 변경 작업
        val loginLocale = editTextLocale.text.toString()

        if (loginName.isEmpty() || loginLocale.isEmpty()) {
            Toast.makeText(this, "이름/암호명 모두 입력해주세요.", Toast.LENGTH_SHORT).show()
        } else if((loginName == checkName) && (loginLocale == checkLocale)){
            Toast.makeText(this, R.string.successLogin, Toast.LENGTH_SHORT).show()
            val intent = Intent(this, MainActivity::class.java)
            intent.putExtra("userName", checkName)
            intent.putExtra("userEmailAddress", checkEmailAddress)
            intent.putExtra("userTel", checkTel)
            intent.putExtra("userAbility", checkAbility)
            intent.putExtra("userLocale", checkLocale)
            startActivity(intent)
            finish()
        } else{
            Toast.makeText(this, R.string.checkNameLocale, Toast.LENGTH_SHORT).show()
        }
    }

    buttonSignUp.setOnClickListener {
        startActivity(Intent(this, SignUpActivity::class.java))
    }
}

}

SignUpActivity.kt

package com.example.contactapp.signUp

import android.content.Intent
import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.widget.Button
import android.widget.EditText
import android.widget.ImageView
import android.widget.TextView
import android.widget.Toast
import androidx.core.content.res.ResourcesCompat
import com.example.contactapp.R
import com.example.contactapp.signIn.SignInActivity
import java.util.regex.Pattern

class SignUpActivity : AppCompatActivity() {

private val emailaddressPattern = Pattern.compile("^[A-Za-z0-9+_.-]+@(.+)\$")
private val pwPattern = Pattern.compile("^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[!@#\$%^&+=]).{8,15}\$")
private val localePattern = Pattern.compile("^[가-힣a-zA-Z]*\$")
private val namePattern = Pattern.compile("^[가-힣a-zA-Z]*\$")
private val telPattern = Pattern.compile("^[0-9]{10,11}\$")
private var imgSet: Int = R.drawable.logo1 // 기본 값으로 초기화

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_sign_up)

    val et_name = findViewById<EditText>(R.id.et_name)
    val et_emailaddress = findViewById<EditText>(R.id.et_emailaddress)
    val et_pw = findViewById<EditText>(R.id.et_pw)
    val et_locale = findViewById<EditText>(R.id.et_locale)
    val et_ability = findViewById<EditText>(R.id.et_ability)
    val et_tel = findViewById<EditText>(R.id.et_tel)
    val btn_signUp = findViewById<TextView>(R.id.btn_signupOk)
    val btn_signCancel = findViewById<TextView>(R.id.btn_signupcancel)




    et_emailaddress.addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {
            val emailaddress = s.toString()
            val valid = emailaddressPattern.matcher(emailaddress).matches()
            if (!valid) {
                et_emailaddress.error = getString(R.string._5_10)
            }
        }

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
    })

    et_pw.addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {
            val pw = s.toString()
            val valid = pwPattern.matcher(pw).matches()
            if (!valid) {
                et_pw.error = getString(R.string._8_15)
            }
        }

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
    })

    et_locale.addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {
            val locale = s.toString()
            val valid = localePattern.matcher(locale).matches()
            if (!valid) {
                et_locale.error = getString(R.string.kor)
            }
        }

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
    })

    et_tel.addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {
            val tel = s.toString()
            val valid = telPattern.matcher(tel).matches()
            if (!valid) {
                et_tel.error = getString(R.string._10_11)
            }
        }

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
    })

    et_name.addTextChangedListener(object : TextWatcher {
        override fun afterTextChanged(s: Editable?) {
            val name = s.toString()
            val valid = namePattern.matcher(name).matches()
            if (!valid) {
                et_name.error = getString(R.string.kor)
            }
        }

        override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
        override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
    })

    //이미지 초기화

// val iv_logo = findViewById(R.id.imageView)
// iv_logo.setOnClickListener {
// imgSet = when ((1..6).random()) {
// 1 -> R.drawable.logo1
// 2 -> R.drawable.logo2
// 3 -> R.drawable.logo3
// 4 -> R.drawable.logo4
// 5 -> R.drawable.logo5
// else -> R.drawable.logo1
// }
//
// iv_logo.setImageDrawable(ResourcesCompat.getDrawable(resources, imgSet, null))
//
// }

    btn_signUp.setOnClickListener{
        val name = et_name.text.toString()
        val emailaddress = et_emailaddress.text.toString()
        val pw = et_pw.text.toString()
        val locale = et_locale.text.toString()
        val ability = et_ability.text.toString()
        val tel = et_tel.text.toString()

        if (name.isBlank() || emailaddress.isBlank() || pw.isBlank() || locale.isBlank() || tel.isBlank()) {
            Toast.makeText(this, getString(R.string.info), Toast.LENGTH_SHORT).show()
            return@setOnClickListener
        }


        val nameValid = namePattern.matcher(name).matches()
        val emailaddressValid = emailaddressPattern.matcher(emailaddress).matches()
        val pwValid = pwPattern.matcher(pw).matches()
        val localeValid = localePattern.matcher(locale).matches()
        val telValid = telPattern.matcher(tel).matches()

        if (!nameValid) {
            et_name.error = getString(R.string.kor)
            return@setOnClickListener
        }

        if (!emailaddressValid) {
            et_emailaddress.error = getString(R.string._5_10)
            return@setOnClickListener
        }

        if (!pwValid) {
            et_pw.error = getString(R.string._8_15)
            return@setOnClickListener
        }

        if (!localeValid) {
            et_locale.error = getString(R.string.kor)
            return@setOnClickListener
        }

        if (!telValid) {
            et_tel.error = getString(R.string._10_11)
            return@setOnClickListener
        }

        val intent = Intent(this, SignInActivity::class.java)

        //수정하겠습니다.
        intent.putExtra("userName", name)
        intent.putExtra("userEmailAddress", emailaddress)
        intent.putExtra("userTel", tel)
        intent.putExtra("userLocale", locale)
        intent.putExtra("userAbility", ability)
        startActivity(intent)
        finish()
    }

    btn_signCancel.setOnClickListener {
        Toast.makeText(this@SignUpActivity, "취소 되었습니다.", Toast.LENGTH_SHORT).show()
        val intent = Intent(this@SignUpActivity, SignInActivity::class.java)
        startActivity(intent)
        finish()
    }


}

}

colors.xml

#FF000000 #FFFFFFFF #D9D9D9 #A6A6A6 #3A75F4 #FFFED36A #FAEAC4 #FF455A64 #FF20222C #1F63AA

strings.xml

ContactApp
<string name="main_fab_btn_description">FabBtn to add contact</string>
<string name="main_tab_title_contact">CONTACT</string>
<string name="main_tab_title_mypage">MY PAGE</string>
<string name="main_toolbar_title">무빙 출연진 연락처</string>

<string name="mypage_todo_edittext_hint">할일 추가하기</string>
<string name="mypage_todo_textview_title">할일 목록</string>
<string name="button_done">Log In</string>
<string name="button_done2">SIGN UP</string>
<string name="butotn_input">입력</string>
<string name="mypage_overlap_todo">이미 등록된 할일이 목록에 존재합니다.</string>
<string name="mypage_edittext_null">할일을 추가하시려면 텍스트를 입력해주세요!</string>

<string name="intent_userImage">userImage</string>
<string name="intent_userNameText">userName</string>
<string name="intent_userPositionText">userPosition</string>
<string name="intent_userTelText">userTel</string>


<string name="_5_10">5~10자의 영어(대소문자)와 숫자만 입력하세요.</string>
<string name="_8_15">8~15자의 영어(대소문자), 숫자, 특수문자를 포함하세요.</string>
<string name="_10_11">10~11자의 숫자만 입력하세요.</string>
<string name="kor">한글 또는 영어만 입력하세요.</string>
<string name="info">입력되지 않은 정보가 있습니다</string>

<string name="signup_cancel1">취소 되었습니다.</string>

//detail
<string name="warningBack">작성 중인 내용이 삭제될 수도 있습니다.</string>
<string name="warningNull">입력된 값이 없습니다.</string>
<string name="noticeComment">댓글을 입력하세요.</string>
<string name="comment">댓글</string>

//signIn
<string name="noticeTeam">Welcome Back!</string>
<string name="name">Full Name</string>
<string name="emailaddress">Email</string>
<string name="password">Password</string>
<string name="tel">TelPhone</string>
<string name="logIn">LOGIN</string>
<string name="signUp">SIGN UP</string>
<string name="checkNameLocale">이름/암호명을 확인해주세요</string>
<string name="successLogin">로그인 성공</string>

//writePage
<string name="subject">제목</string>
<string name="inputContents">내용을 입력해주세요</string>
<string name="cancel">취소</string>

//main
<string name="nameColon">이름 : </string>
<string name="position">Ability</string>
<string name="positionColon">직책 : </string>
<string name="teamMember">팀 멤버</string>
<string name="teamNoticeBoard">팀 게시판</string>

//signUp
<string name="notice2Team">SIGN UP</string>
<string name="inputName">이름을 입력하세요.</string>
<string name="inputID">이메일을 입력하세요.</string>
<string name="inputPassword">비밀번호를 입력하세요.</string>
<string name="inputPosition">(입력 안해도 됨)</string>
<string name="inputTel">전화번호를 입력하세요.</string>
<string name="locale">Locale</string>
<string name="inputLocale">지역명</string>

//dialog
<string name="dialog_name_edt">이름*</string>
<string name="dialog_phone_edt">전화번호*</string>
<string name="dialog_email_edt">이메일*</string>
<string name="dialog_locale_edt">지역*</string>
<string name="dialog_ability_edt">능력</string>
<string name="dialog_chip_off">OFF</string>
<string name="dialog_chip_5_minutes">5분 뒤 알림</string>
<string name="dialog_chip_10_minutes">10분 뒤 알림</string>
<string name="dialog_chip_30_minutes">30분 뒤 알림</string>
<string name="dialog_cancel_btn">취소</string>
<string name="dialog_add_btn">등록</string>
<string name="dialog_no_info_toast">* 표시가 있는 항목은 전부 입력해주세요.</string>

<string name="noti_title">연락처 알림</string>
<string name="noti_message">에게 연락을 할 시간입니다.</string>

themes.xml

<style name="Theme.ContactApp" parent="Base.Theme.ContactApp" />

<style name="AppTheme" parent="Theme.MaterialComponents.Light.NoActionBar">
    <item name="colorPrimaryDark">@color/contact_lightYellow</item>
</style>

<style name="btnFab">
    <item name="cornerSize">50%</item>
</style>

pressed_chip.xml

activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/mainConstrainLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/contact_black"
tools:context=".main.MainActivity">

<androidx.appcompat.widget.Toolbar
    android:id="@+id/toolBar"
    android:layout_width="match_parent"
    android:layout_height="?attr/actionBarSize"
    android:background="@color/contact_yellow"
    android:padding="16dp"
    app:layout_constraintBottom_toTopOf="@id/viewPager2"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent">

    <TextView
        android:layout_width="wrap_content"
        android:layout_height="match_parent"
        android:text="@string/main_toolbar_title"
        android:textColor="@color/white"
        android:textStyle="bold" />

    <ImageView
        android:id="@+id/img_listView"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_gravity="end"
        android:layout_marginEnd="16dp"
        android:src="@drawable/ic_list_view" />

    <ImageView
        android:id="@+id/img_gridView"
        android:layout_width="25dp"
        android:layout_height="25dp"
        android:layout_gravity="end"
        android:layout_marginEnd="10dp"
        android:src="@drawable/ic_grid_view" />
</androidx.appcompat.widget.Toolbar>

<androidx.viewpager2.widget.ViewPager2
    android:id="@+id/viewPager2"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    app:layout_constraintBottom_toTopOf="@id/tabLayout"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/toolBar" />

<com.google.android.material.tabs.TabLayout
    android:id="@+id/tabLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@color/contact_yellow"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@id/viewPager2"
    app:tabIndicatorColor="@color/contact_darkGray"
    app:tabIndicatorFullWidth="true"
    app:tabSelectedTextColor="@color/contact_darkGray"
    app:tabTextColor="@color/black" />

<com.google.android.material.floatingactionbutton.FloatingActionButton
    android:id="@+id/btn_fab"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="20dp"
    android:contentDescription="@string/main_fab_btn_description"
    android:src="@drawable/ic_add_contact"
    app:backgroundTint="@color/contact_yellow"
    app:layout_constraintBottom_toTopOf="@id/tabLayout"
    app:layout_constraintEnd_toEndOf="parent"
    app:shapeAppearanceOverlay="@style/btnFab" />

</androidx.constraintlayout.widget.ConstraintLayout>

activity_sign_in.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#212832"
android:orientation="vertical"
android:padding="0dp">

<ImageView
    android:id="@+id/login_glide"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:scaleType="centerCrop"
    app:layout_constraintTop_toTopOf="parent" />

<ScrollView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent">

        <ImageView
            android:id="@+id/logo_login"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginTop="0dp"
            android:gravity="center"
            android:padding="20dp"
            android:src="@drawable/login_moving_logo"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/txt_login_name"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="start"
            android:layout_marginHorizontal="20dp"
            android:layout_marginTop="26dp"
            android:text="이름"
            android:textColor="#8CAAB9"
            android:textSize="18sp"
            android:textStyle="bold"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/logo_login" />

        <EditText
            android:id="@+id/et_login_name"
            android:layout_width="match_parent"
            android:layout_height="55dp"
            android:layout_marginHorizontal="20dp"
            android:background="#4D455A64"
            android:hint="@string/inputName"
            android:inputType="text"
            android:padding="16dp"
            android:textColor="#FFFFFF"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/txt_login_name" />

        <TextView
            android:id="@+id/txt_login_locale"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_gravity="start"
            android:layout_marginHorizontal="20dp"
            android:layout_marginTop="26dp"
            android:text="암호명"
            android:textColor="#8CAAB9"
            android:textSize="18sp"
            android:textStyle="bold"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/et_login_name" />

        <EditText
            android:id="@+id/et_login_locale"
            android:layout_width="match_parent"
            android:layout_height="55dp"
            android:layout_marginHorizontal="20dp"
            android:layout_marginBottom="32dp"
            android:background="#4D455A64"
            android:hint="암호명을 입력해주세요."
            android:inputType="textPassword"
            android:padding="16dp"
            android:textColor="#FFFFFF"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/txt_login_locale" />

        <TextView
            android:id="@+id/buttonLogin"
            android:layout_width="match_parent"
            android:layout_height="55dp"
            android:layout_marginHorizontal="20dp"
            android:layout_marginTop="60dp"
            android:background="#99FED36A"
            android:gravity="center"
            android:text="@string/logIn"
            android:textSize="20dp"
            android:textStyle="bold"
            app:layout_constraintBottom_toTopOf="@id/buttonSignUp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/et_login_locale" />

        <TextView
            android:id="@+id/buttonSignUp"
            android:layout_width="match_parent"
            android:layout_height="55dp"
            android:layout_marginHorizontal="20dp"
            android:layout_marginTop="26dp"
            android:layout_marginBottom="26dp"
            android:background="#991F63AA"
            android:gravity="center"
            android:text="@string/signUp"
            android:textSize="20dp"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@+id/buttonLogin" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

</androidx.constraintlayout.widget.ConstraintLayout>

activity_sign_up.xml

<androidx.constraintlayout.widget.ConstraintLayout
    android:layout_width="match_parent"
    android:layout_height="wrap_content">

    <TextView
        android:id="@+id/textView"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:gravity="center"
        android:text="@string/notice2Team"
        android:textColor="#FFFFFF"
        android:textSize="35sp"
        android:textStyle="bold"
        tools:layout_editor_absoluteX="-24dp"
        tools:layout_editor_absoluteY="16dp" />

    <TextView
        android:id="@+id/tv_name"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="24dp"
        android:layout_marginEnd="24dp"
        android:text="@string/name"
        android:textColor="#8CAAB9"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/textView" />

    <EditText
        android:id="@+id/et_name"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginStart="24dp"
        android:layout_marginTop="4dp"
        android:layout_marginEnd="24dp"
        android:background="#455A64"
        android:ems="10"
        android:hint="@string/inputName"
        android:inputType="text"
        android:padding="12dp"
        android:textColor="#FFFFFF"
        android:textSize="18sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_name" />

    <TextView
        android:id="@+id/tv_locale"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="24dp"
        android:text="@string/locale"
        android:textColor="#8CAAB9"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/et_pw" />

    <EditText
        android:id="@+id/et_locale"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginStart="24dp"
        android:layout_marginTop="3dp"
        android:layout_marginEnd="24dp"
        android:background="#455A64"
        android:ems="10"
        android:hint="@string/inputLocale"
        android:inputType="text"
        android:padding="12dp"
        android:textColor="#FFFFFF"
        android:textSize="18sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="1.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_locale" />

    <TextView
        android:id="@+id/tv_ability"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="24dp"
        android:text="@string/position"
        android:textColor="#8CAAB9"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/et_locale" />

    <EditText
        android:id="@+id/et_ability"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginStart="24dp"
        android:layout_marginTop="3dp"
        android:layout_marginEnd="24dp"
        android:background="#455A64"
        android:ems="10"
        android:hint="@string/inputPosition"
        android:inputType="text"
        android:padding="12dp"
        android:textColor="#FFFFFF"
        android:textSize="18sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_ability" />

    <TextView
        android:id="@+id/tv_emailaddress"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="24dp"
        android:text="@string/emailaddress"
        android:textColor="#8CAAB9"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/et_name" />

    <EditText
        android:id="@+id/et_emailaddress"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginStart="24dp"
        android:layout_marginTop="3dp"
        android:layout_marginEnd="24dp"
        android:background="#455A64"
        android:ems="10"
        android:hint="@string/inputID"
        android:inputType="text"
        android:padding="12dp"
        android:textColor="#FFFFFF"
        android:textSize="18sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_emailaddress" />

    <TextView
        android:id="@+id/tv_pw"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="20dp"
        android:layout_marginEnd="24dp"
        android:text="@string/password"
        android:textColor="#8CAAB9"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/et_emailaddress" />

    <EditText
        android:id="@+id/et_pw"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginStart="24dp"
        android:layout_marginTop="3dp"
        android:layout_marginEnd="24dp"
        android:background="#455A64"
        android:ems="10"
        android:hint="@string/inputPassword"
        android:inputType="textPassword"
        android:padding="12dp"
        android:textColor="#FFFFFF"
        android:textSize="18sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_pw" />

    <TextView
        android:id="@+id/tv_tel"
        android:layout_width="0dp"
        android:layout_height="wrap_content"
        android:layout_marginStart="24dp"
        android:layout_marginTop="16dp"
        android:layout_marginEnd="24dp"
        android:text="@string/tel"
        android:textColor="#8CAAB9"
        android:textSize="18sp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/et_ability" />

    <EditText
        android:id="@+id/et_tel"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginStart="24dp"
        android:layout_marginTop="3dp"
        android:layout_marginEnd="24dp"
        android:background="#455A64"
        android:ems="10"
        android:hint="@string/inputTel"
        android:inputType="number"
        android:padding="12dp"
        android:textColor="#FFFFFF"
        android:textSize="18sp"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/tv_tel" />

    <TextView
        android:id="@+id/btn_signupOk"
        android:layout_width="match_parent"
        android:layout_height="50dp"
        android:layout_marginStart="24dp"
        android:layout_marginTop="26dp"
        android:layout_marginEnd="24dp"
        android:background="#FED36A"


        android:gravity="center"
        android:text="@string/button_done2"

        android:textSize="20dp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintHorizontal_bias="0.0"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toBottomOf="@+id/et_tel" />

    <TextView
        android:id="@+id/btn_signupcancel"
        android:layout_width="0dp"
        android:layout_height="50dp"
        android:layout_marginTop="26dp"
        android:background="@color/login_blue"

        android:gravity="center"
        android:text="Already have an account? LOGIN"
        android:textSize="20dp"
        android:textStyle="bold"
        app:layout_constraintEnd_toEndOf="@+id/btn_signupOk"

        app:layout_constraintStart_toStartOf="@+id/btn_signupOk"
        app:layout_constraintTop_toBottomOf="@+id/btn_signupOk" />

</androidx.constraintlayout.widget.ConstraintLayout>

activity_splash.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".SplashActivity">

<ImageView
    android:id="@+id/image_splash"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    android:scaleType="centerCrop"/>

</androidx.constraintlayout.widget.ConstraintLayout>

contact_gridview_item.xml

<de.hdodenhof.circleimageview.CircleImageView
    android:id="@+id/img_profile"
    android:layout_width="100dp"
    android:layout_height="100dp"
    android:scaleType="centerCrop"
    android:src="@drawable/ic_empty_user"
    apps:civ_border_color="@color/white"
    apps:civ_border_width="1dp" />

<TextView
    android:id="@+id/txt_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_marginTop="5dp"
    android:textColor="@color/white"
    android:textSize="16sp" />

<TextView
    android:id="@+id/txt_ability"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_gravity="center"
    android:layout_marginTop="5dp"
    android:textColor="@color/white"
    android:textSize="12sp" />

contact_recyclerview_item.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:apps="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingHorizontal="16dp"
android:paddingVertical="8dp">

<LinearLayout
    android:id="@+id/back_linearLayout"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:background="@drawable/border_item_background_yellow"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:padding="16dp"
    apps:layout_constraintBottom_toBottomOf="@id/contact_linearLayout"
    apps:layout_constraintEnd_toEndOf="@id/contact_linearLayout"
    apps:layout_constraintStart_toStartOf="@id/contact_linearLayout"
    apps:layout_constraintTop_toTopOf="@id/contact_linearLayout">

    <ImageView
        android:id="@+id/img_call"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:adjustViewBounds="true"
        android:scaleType="centerCrop"
        android:src="@drawable/ic_call" />

</LinearLayout>

<LinearLayout
    android:id="@+id/contact_linearLayout"
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    android:background="@drawable/border_item_background_black"
    android:gravity="center_vertical"
    android:orientation="horizontal"
    android:padding="16dp"
    apps:layout_constraintBottom_toBottomOf="parent"
    apps:layout_constraintEnd_toEndOf="parent"
    apps:layout_constraintStart_toStartOf="parent"
    apps:layout_constraintTop_toTopOf="parent">

    <de.hdodenhof.circleimageview.CircleImageView
        android:id="@+id/img_profile"
        android:layout_width="50dp"
        android:layout_height="50dp"
        android:scaleType="centerCrop"
        android:src="@drawable/ic_empty_user"
        apps:civ_border_color="@color/white"
        apps:civ_border_width="1dp" />

    <TextView
        android:id="@+id/txt_info"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_marginStart="16dp"
        android:ellipsize="end"
        android:maxLines="1"
        android:textColor="@color/white"
        android:textSize="18sp" />

    <View
        android:layout_width="0dp"
        android:layout_height="0dp"
        android:layout_weight="1" />

    <ImageView
        android:id="@+id/img_like"
        android:layout_width="35dp"
        android:layout_height="35dp" />
</LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

dialog_add_contact.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@color/contact_black"
android:paddingHorizontal="20dp"
android:paddingVertical="40dp">

<ScrollView
    android:layout_width="match_parent"
    android:layout_height="wrap_content"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintTop_toTopOf="parent"
    app:layout_constraintBottom_toBottomOf="parent">
    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">
        <de.hdodenhof.circleimageview.CircleImageView
            android:id="@+id/img_profile"
            android:layout_width="150dp"
            android:layout_height="150dp"
            android:src="@drawable/ic_empty_user"
            app:civ_border_color="@color/white"
            app:civ_border_width="3dp"
            app:layout_constraintBottom_toTopOf="@id/edt_name"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <de.hdodenhof.circleimageview.CircleImageView
            android:id="@+id/img_add_profile"
            android:layout_width="50dp"
            android:layout_height="50dp"
            android:layout_margin="5dp"
            android:src="@drawable/ic_add_profile"
            app:layout_constraintBottom_toBottomOf="@id/img_profile"
            app:layout_constraintEnd_toEndOf="@id/img_profile" />

        <EditText
            android:id="@+id/edt_name"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="30dp"
            android:background="@drawable/dialog_edt_rectangle"
            android:hint="@string/dialog_name_edt"
            android:padding="10dp"
            android:textColor="@color/white"
            android:textColorHint="@color/lightGray"
            app:layout_constraintBottom_toTopOf="@id/edt_phone"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/img_profile" />

        <EditText
            android:id="@+id/edt_phone"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:background="@drawable/dialog_edt_rectangle"
            android:hint="@string/dialog_phone_edt"
            android:inputType="numberDecimal"
            android:padding="10dp"
            android:textColor="@color/white"
            android:textColorHint="@color/lightGray"
            app:layout_constraintBottom_toTopOf="@id/edt_email"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/edt_name" />

        <EditText
            android:id="@+id/edt_email"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:background="@drawable/dialog_edt_rectangle"
            android:hint="@string/dialog_email_edt"
            android:padding="10dp"
            android:textColor="@color/white"
            android:textColorHint="@color/lightGray"
            app:layout_constraintBottom_toTopOf="@id/edt_locale"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/edt_phone" />

        <EditText
            android:id="@+id/edt_locale"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:background="@drawable/dialog_edt_rectangle"
            android:hint="@string/dialog_locale_edt"
            android:padding="10dp"
            android:textColor="@color/white"
            android:textColorHint="@color/lightGray"
            app:layout_constraintBottom_toTopOf="@id/edt_ability"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/edt_email" />

        <EditText
            android:id="@+id/edt_ability"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            android:background="@drawable/dialog_edt_rectangle"
            android:hint="@string/dialog_ability_edt"
            android:padding="10dp"
            android:textColor="@color/white"
            android:textColorHint="@color/lightGray"
            app:layout_constraintBottom_toTopOf="@id/chip_group"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/edt_locale" />

        <com.google.android.material.chip.ChipGroup
            android:id="@+id/chip_group"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="15dp"
            app:checkedChip="@id/chip_off"
            app:layout_constraintBottom_toTopOf="@id/btn_cancel"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/edt_ability"
            app:singleLine="false"
            app:singleSelection="true">

            <com.google.android.material.chip.Chip
                android:id="@+id/chip_off"
                style="@style/Widget.MaterialComponents.Chip.Choice"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/dialog_chip_off"
                android:textColor="@color/black"
                app:chipBackgroundColor="@color/pressed_chip" />

            <com.google.android.material.chip.Chip
                android:id="@+id/chip_5min"
                style="@style/Widget.MaterialComponents.Chip.Choice"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/dialog_chip_5_minutes"
                android:textColor="@color/black"
                app:chipBackgroundColor="@color/pressed_chip" />

            <com.google.android.material.chip.Chip
                android:id="@+id/chip_10min"
                style="@style/Widget.MaterialComponents.Chip.Choice"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/dialog_chip_10_minutes"
                android:textColor="@color/black"
                app:chipBackgroundColor="@color/pressed_chip" />

            <com.google.android.material.chip.Chip
                android:id="@+id/chip_30min"
                style="@style/Widget.MaterialComponents.Chip.Choice"
                android:layout_width="wrap_content"
                android:layout_height="wrap_content"
                android:text="@string/dialog_chip_30_minutes"
                android:textColor="@color/black"
                app:chipBackgroundColor="@color/pressed_chip" />

        </com.google.android.material.chip.ChipGroup>

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/btn_cancel"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginTop="20dp"
            android:layout_marginEnd="10dp"
            android:background="@color/contact_yellow"
            android:padding="10dp"
            android:text="@string/dialog_cancel_btn"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toStartOf="@id/btn_add"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/chip_group" />

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/btn_add"
            android:layout_width="0dp"
            android:layout_height="wrap_content"
            android:layout_marginStart="10dp"
            android:background="@color/contact_yellow"
            android:text="@string/dialog_add_btn"
            android:textStyle="bold"
            app:layout_constraintBottom_toBottomOf="@id/btn_cancel"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toEndOf="@id/btn_cancel"
            app:layout_constraintTop_toTopOf="@id/btn_cancel" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

</androidx.constraintlayout.widget.ConstraintLayout>

fragment_contact_list.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".contact.ContactListFragment">

<androidx.recyclerview.widget.RecyclerView
    android:id="@+id/recyclerView_contact"
    android:layout_width="match_parent"
    android:layout_height="0dp"
    android:layout_marginTop="8dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

fragment_detail.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#20222C"
tools:context="detail.DetailFragment">

<ScrollView
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    app:layout_constraintTop_toTopOf="parent">

    <androidx.constraintlayout.widget.ConstraintLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content">

        <!-- 디테일, 마이페이지  -->
        <de.hdodenhof.circleimageview.CircleImageView
            android:id="@+id/img_photo"
            android:layout_width="266dp"
            android:layout_height="266dp"
            android:layout_marginTop="50dp"
            android:src="@drawable/ic_empty_user"
            app:civ_border_color="@color/white"
            app:civ_border_width="5dp"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />

        <TextView
            android:id="@+id/btn_messageButton"
            android:layout_width="0dp"
            android:layout_height="45dp"
            android:layout_marginTop="50dp"
            android:layout_marginEnd="10dp"
            android:background="@color/contact_yellow"
            android:gravity="center"
            android:text="MESSAGE"
            android:textStyle="bold"
            android:visibility="visible"
            app:layout_constraintEnd_toStartOf="@id/btn_callButton"
            app:layout_constraintStart_toStartOf="@id/iv_name"
            app:layout_constraintTop_toBottomOf="@id/img_photo" />

        <TextView
            android:id="@+id/btn_callButton"
            android:layout_width="0dp"
            android:layout_height="45dp"
            android:layout_marginStart="10dp"
            android:layout_marginTop="50dp"
            android:background="@color/contact_yellow"
            android:gravity="center"
            android:text="CALL"
            android:textStyle="bold"
            android:visibility="visible"
            app:layout_constraintEnd_toEndOf="@id/iv_name"
            app:layout_constraintStart_toEndOf="@id/btn_messageButton"
            app:layout_constraintTop_toBottomOf="@id/img_photo" />

        <!--네임 -->
        <ImageView
            android:id="@+id/iv_name"
            android:layout_width="match_parent"
            android:layout_height="55dp"
            android:layout_marginHorizontal="20dp"
            android:layout_marginTop="26dp"
            android:background="#455A64"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/btn_messageButton" />

        <ImageView
            android:id="@+id/icon_name"
            android:layout_width="24dp"
            android:layout_height="24dp"
            android:layout_marginStart="14dp"
            android:src="@drawable/detail_name"
            app:layout_constraintBottom_toBottomOf="@id/iv_name"
            app:layout_constraintStart_toStartOf="@id/iv_name"
            app:layout_constraintTop_toTopOf="@id/iv_name" />

        <TextView
            android:id="@+id/tv_name"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="14dp"
            android:gravity="center_vertical"
            android:text="이름"
            android:textColor="@color/white"
            android:textSize="20dp"
            app:layout_constraintBottom_toBottomOf="@id/iv_name"
            app:layout_constraintStart_toEndOf="@id/icon_name"
            app:layout_constraintTop_toTopOf="@id/iv_name" />

        <!-- 번호-->
        <ImageView
            android:id="@+id/iv_phoneNumber"
            android:layout_width="match_parent"
            android:layout_height="55dp"
            android:layout_marginHorizontal="20dp"
            android:layout_marginTop="26dp"
            android:background="#455A64"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/iv_name" />

        <ImageView
            android:id="@+id/icon_phoneNumber"
            android:layout_width="24dp"
            android:layout_height="24dp"
            android:layout_marginStart="14dp"
            android:src="@drawable/detail_phone"
            app:layout_constraintBottom_toBottomOf="@id/iv_phoneNumber"
            app:layout_constraintStart_toStartOf="@id/iv_phoneNumber"
            app:layout_constraintTop_toTopOf="@id/iv_phoneNumber" />

        <TextView
            android:id="@+id/tv_phoneNumber"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="14dp"
            android:gravity="center_vertical"
            android:text="010-0000-0000"
            android:textColor="@color/white"
            android:textSize="20dp"
            app:layout_constraintBottom_toBottomOf="@id/iv_phoneNumber"
            app:layout_constraintStart_toEndOf="@id/icon_phoneNumber"
            app:layout_constraintTop_toTopOf="@id/iv_phoneNumber" />

        <!-- 이메일-->
        <ImageView
            android:id="@+id/iv_email"
            android:layout_width="match_parent"
            android:layout_height="55dp"
            android:layout_marginHorizontal="20dp"
            android:layout_marginTop="26dp"
            android:background="#455A64"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/iv_phoneNumber" />

        <ImageView
            android:id="@+id/icon_email"
            android:layout_width="24dp"
            android:layout_height="24dp"
            android:layout_marginStart="14dp"
            android:src="@drawable/detail_email"
            app:layout_constraintBottom_toBottomOf="@id/iv_email"
            app:layout_constraintStart_toStartOf="@id/iv_email"
            app:layout_constraintTop_toTopOf="@id/iv_email" />

        <TextView
            android:id="@+id/tv_email"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="14dp"
            android:gravity="center_vertical"
            android:text="Test@naver.com"
            android:textColor="@color/white"
            android:textSize="20dp"
            app:layout_constraintBottom_toBottomOf="@id/iv_email"
            app:layout_constraintStart_toEndOf="@id/icon_email"
            app:layout_constraintTop_toTopOf="@id/iv_email" />

        <!-- locale-->
        <TextView
            android:id="@+id/iv_locale"
            android:layout_width="match_parent"
            android:layout_height="55dp"
            android:layout_marginHorizontal="20dp"
            android:layout_marginTop="26dp"
            android:background="#455A64"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/iv_email" />

        <ImageView
            android:id="@+id/icon_locale"
            android:layout_width="24dp"
            android:layout_height="24dp"
            android:layout_marginStart="14dp"
            android:src="@drawable/detail_check"
            app:layout_constraintBottom_toBottomOf="@id/iv_locale"
            app:layout_constraintStart_toStartOf="@id/iv_locale"
            app:layout_constraintTop_toTopOf="@id/iv_locale" />

        <TextView
            android:id="@+id/tv_locale"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="14dp"
            android:gravity="center_vertical"
            android:text="Locale"
            android:textColor="@color/white"
            android:textSize="20dp"
            app:layout_constraintBottom_toBottomOf="@id/iv_locale"
            app:layout_constraintStart_toEndOf="@id/icon_locale"
            app:layout_constraintTop_toTopOf="@id/iv_locale" />

        <!-- ability -->

        <TextView
            android:id="@+id/iv_ability"
            android:layout_width="match_parent"
            android:layout_height="55dp"
            android:layout_marginHorizontal="20dp"
            android:layout_marginTop="26dp"
            android:layout_marginBottom="60dp"
            android:background="#455A64"
            app:layout_constraintBottom_toBottomOf="parent"
            app:layout_constraintEnd_toEndOf="parent"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toBottomOf="@id/iv_locale" />

        <ImageView
            android:id="@+id/icon_ability"
            android:layout_width="24dp"
            android:layout_height="24dp"
            android:layout_marginStart="14dp"
            android:src="@drawable/detail_thunder"
            app:layout_constraintBottom_toBottomOf="@id/iv_ability"
            app:layout_constraintStart_toStartOf="@id/iv_ability"
            app:layout_constraintTop_toTopOf="@id/iv_ability" />

        <TextView
            android:id="@+id/tv_ability"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_marginStart="14dp"
            android:gravity="center_vertical"
            android:text="Ability"
            android:textColor="@color/white"
            android:textSize="20dp"
            app:layout_constraintBottom_toBottomOf="@id/iv_ability"
            app:layout_constraintStart_toEndOf="@id/icon_ability"
            app:layout_constraintTop_toTopOf="@id/iv_ability" />
    </androidx.constraintlayout.widget.ConstraintLayout>
</ScrollView>

</androidx.constraintlayout.widget.ConstraintLayout>

Android 앱개발 심화

10-1. SharedPreferences

1. Preference란?

· 프로그램의 설정 정보(사용자의 옵션 선택 사항이나 프로그램의 구성 정보)를 영구적으로 저장하는 용도로 사용
· XML 포맷의 텍스트 파일에 키-값 세트로 정보를 저장.
· SharedPreferences 클래스
◦Preferences의 데이터(키-값 세트)를 관리하는 클래스
◦응용 프로그램 내의 액티비티 간에 공유하며, 한쪽 액티비티에서 수정 시 다른 액티비티에서도 수정된 값을 읽을 수 있다.
◦응용 프로그램의 고유한 정보이므로 외부에서는 읽을 수 없다.

2. 공유 환경설정의 핸들 가져오기

• getSharedPreferences (name, mode)

◦여러개의 Shared Preference 파일들을 사용하는 경우
◦name : 프레퍼런스 데이터를 저장할 XML 파일의 이름이다.
◦mode : 파일의 공유 모드
■ MODE_PRIVATE : 생성된 XML 파일은 호출한 애플리케이션 내에서만 읽기 쓰기가 가능
■ MODE_WORLD_READABLE, MODE_WORLD_WRITEABLE은 보안상 이유로 API level 17에서 deprecated 됨

val sharedPref = activity?.getSharedPreferences(
getString(R.string.preference_file_key), Context.MODE_PRIVATE)

• SharedPreferences 클래스

◦프레퍼런스의 데이터(키-값 세트)를 관리하는 클래스
◦응용 프로그램 내의 액티비티 간에 공유하며, 한쪽 액티비티에서 수정 시 다른 액티비티에서도 수정된 값을 읽을 수 있다.
◦응용 프로그램의 고유한 정보이므로 외부에서는 읽을 수 없다.

• XML 포맷의 텍스트 파일에 키-값 세트로 정보를 저장
• 프로그램의 설정 정보(사용자의 옵션 선택 사항이나 프로그램의 구성 정보)를 영구적으로 저장하는 용도로 사용

• getPreferences

◦ 한 개의 Shared Preference 파일을 사용하는 경우
◦ Activity 클래스에 정의된 메소드이므로, Activity 인스턴스를 통해 접근 가능
◦생성한 액티비티 전용이므로 같은 패키지의 다른 액티비티는 읽을 수 없다.
◦액티비티와 동일한 이름의 XML 파일 생성

val sharedPref = activity?.getPreferences(Context.MODE_PRIVATE)

  1. 예제

EditText를 만들어 text 입력 후, 저장 버튼을 누르면 입력한 text를 저장하고,
앱을 다시 열면 저장된 text가 그대로 유지될 수 있도록 해보자.
(SharedPreferences를 쓰지 않는 일반적인 경우, text가 저장되지 않아 사라진다)

  1. 프로젝트 생성

Android Studio에서 기본 프로젝트(with empty activity) 생성하자!

  1. activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">

<EditText
    android:id="@+id/et_hello"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:ems="10"
    android:inputType="textPersonName"
    android:textColor="#000000"
    android:textSize="24sp"
    android:textStyle="bold"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>

  1. MainActivity.kt

package com.android.preference

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import android.widget.Toast
import com.android.preference.databinding.ActivityMainBinding

class MainActivity : AppCompatActivity() {

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

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(binding.root)

    binding.btnSave.setOnClickListener{
        saveData()
        Toast.makeText(this, "Data Saved.", Toast.LENGTH_SHORT).show()
    }
    loadData()
}

private fun saveData() {
    val pref = getSharedPreferences("pref",0)
    val edit = pref.edit() // 수정 모드
    // 1번째 인자는 키, 2번째 인자는 실제 담아둘 값
    edit.putString("name", binding.etHello.text.toString())
    edit.apply() // 저장완료
}

private fun loadData() {
    val pref = getSharedPreferences("pref",0)
    // 1번째 인자는 키, 2번째 인자는 데이터가 존재하지 않을경우의 값
    binding.etHello.setText(pref.getString("name",""))
}

}

10-2. Room

1) Room 개요

• Room
◦ SQLite를 쉽게 사용할 수 있는 데이터베이스 객체 매핑 라이브러리
◦ 쉽게 Query를 사용할 수 있는 API를 제공
◦Query를 컴파일 시간에 검증함
◦Query 결과를 LiveData로 하여 데이터베이스가 변경될 때마다 쉽게 UI를 변경할 수 있음

• SQLite보다 Room을 사용할 것을 권장함

2) Room 주요 3요소

• Room 주요 3요소

◦ @Database: 클래스를 데이터베이스로 지정하는 annotation, RoomDatabase를 상속 받은 클래스여야 함
• Room.databaseBuilder를 이용하여 인스턴스를 생성함
◦ @Entity: 클래스를 테이블 스키마로 지정하는 annotation
◦ @Dao: 클래스를 DAO(Data Access Object)로 지정하는 annotation
• 기본적은 insert, delete, update SQL은 자동으로 만들어주며, 복잡한 SQL은 직접 만들 수 있음

3) gradle 파일 설정

• Room은 안드로이드 아키텍처에 포함되어 있음
• 사용하기 위해 build.gradle 파일의 dependencies에 아래 내용을 추가해야 함
◦Androidx 사용하는 경우를 가정함, Android Studio와 SDK는 최신 버전으로 사용
◦'kotlin-kapt' 플러그인이 추가
◦dependencies 추가

plugins {
....
id 'kotlin-kapt'
}
.....

dependencies {

......

def room_version = "2.5.1"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"

}

4) Entity 생성

• Entity는 테이블 스키마 정의
• CREATE TABLE student_table (student_id INTEGER PRIMARY KEY, name TEXT NOT NULL);
• @Entity data class Student

@Entity(tableName = "student_table") // 테이블 이름을 student_table로 지정함
data class Student (
@PrimaryKey
@ColumnInfo(name = "student_id")
val id: Int,
val name: String
)

5) DAO 생성

• DAO는 interface나 abstract class로 정의되어야 함
• Annotation에 SQL 쿼리를 정의하고 그 쿼리를 위한 메소드를 선언
• 가능한 annotation으로 @Insert, @Update, @Delete, @Query가 있음

@Query("SELECT * from table") fun getAllData() : List

• @Insert, @Update, @Delete는 SQL 쿼리를 작성하지 않아도 컴파일러가 자동으로 생성함
◦@Insert나 @Update는 key가 중복되는 경우 처리를 위해 onConflict를 지정할 수 있음
• OnConflictStrategy.ABORT: key 충돌시 종료
• OnConflictStrategy.IGNORE: key 충돌 무시
• OnConflictStrategy.REPLACE: key 충돌시 새로운 데이터로 변경

◦@Update나 @Delete는 primary key에 해당되는 튜플을 찾아서 변경/삭제 함

• @Query로 리턴되는 데이터의 타입을 LiveData<>로 하면, 나중에 이 데이터가 업데이트될 때 Observer를 통해 할 수 있음

@Query("SELECT * from table") fun getAllData() : LiveData<List>

• @Query에 SQL을 정의할 때 메소드의 인자를 사용할 수 있음

@Query("SELECT * FROM student_table WHERE name = :sname")
suspend fun getStudentByName(sname: String): List

인자 sname을 SQL에서 :sname으로 사용

• fun 앞에 suspend는 Kotlin coroutine을 사용하는 것임, 나중에 이 메소드를 부를 때는 runBlocking {} 내에서 호출해야 함
• LiveData는 비동기적으로 동작하기 때문에 coroutine으로 할 필요가 없음

@Dao
interface MyDAO {
@Insert(onConflict = OnConflictStrategy.REPLACE) // INSERT, key 충돌이 나면 새 데이터로 교체
suspend fun insertStudent(student: Student)

@Query("SELECT * FROM student_table")
fun getAllStudents(): LiveData<List<Student>>        // LiveData<> 사용

@Query("SELECT * FROM student_table WHERE name = :sname")   
suspend fun getStudentByName(sname: String): List<Student>

@Delete
suspend fun deleteStudent(student: Student); // primary key is used to find the student

// ...

}

• @Query("SELECT from table") fun getAllData() : LiveData<List>
@Query("SELECT
FROM student_table WHERE name = :sname")
suspend fun getStudentByName(sname: String): List

• 인자 sname을 SQL에서 :sname으로 사용

6) Database 생성

• RoomDatabase를 상속하여 자신의 Room 클래스를 만들어야 함
• 포함되는 Entity들과 데이터베이스 버전(version)을 @Database annotation에 지정함
◦ version이 기존에 저장되어 있는 데이터베이스보다 높으면, 데이터베이스를 open할 때 migration을 수행하게 됨
◦ Migration 수행 방법은 RoomDatabase 객체의 addMigration() 메소드를 통해 알려줌

• DAO를 가져올 수 있는 getter 메소드를 만듬
◦ 실제 메소드 정의는 자동으로 생성됨

• Room 클래스의 인스턴스는 하나만 있으면 되므로 Singleton 패턴을 사용
• Room 클래스의 객체 생성은 Room.databaseBuilder()를 이용함

@Database(entities = [Student::class, ClassInfo::class, Enrollment::class, Teacher::class], version = 1)
abstract class MyDatabase : RoomDatabase() {
abstract fun getMyDao() : MyDAO

companion object {
    private var INSTANCE: MyDatabase? = null
    private val MIGRATION_1_2 = object : Migration(1, 2) {
        override fun migrate(database: SupportSQLiteDatabase) { 생략 }
    }

    private val MIGRATION_2_3 = object : Migration(2, 3) {
        override fun migrate(database: SupportSQLiteDatabase) { 생략 }
    }
    fun getDatabase(context: Context) : MyDatabase {
        if (INSTANCE == null) {
            INSTANCE = Room.databaseBuilder(
                context, MyDatabase::class.java, "school_database")
                .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                .build()
        }
        return INSTANCE as MyDatabase
    }
}

}

7) Migration

• 앞에서 MyRoomDatabase 객체 생성 후 addMigrations() 메소드를 호출하여 Migration 방법을 지정했음
◦ 여러 개의 migration 지정 가능

Room.databaseBuilder(...).addMigrations(MIGRATION_1_2, MIGRATION_2_3)

private val MIGRATION_1_2 = object : Migration(1, 2) { // version 1 -> 2
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
}
}

private val MIGRATION_2_3 = object : Migration(2, 3) { // version 2 -> 3
override fun migrate(database: SupportSQLiteDatabase) {
database.execSQL("ALTER TABLE class_table ADD COLUMN last_update INTEGER")
}
}

8) UI와 연결

• 안드로이드 아키텍처에 따라 Repository와 ViewModel을 사용하길 권장하지만
◦참고: https://codelabs.developers.google.com/codelabs/android-room-with-a-view-kotlin/
◦여기에서는 Room과 LiveData 사용만 다룸

• RoomDatabase 객체에서 DAO 객체를 받아오고, 이 DAO 객체의 메소드를 호출하여 데이터베이스를 접근함

myDao = MyDatabase.getDatabase(this).getMyDao()
runBlocking { // (주의) UI를 블록할 수 있는 DAO 메소드를 UI 스레드에서 바로 호출하면 안됨
myDao.insertStudent(Student(1, "james")) // suspend 지정되어 있음
}
val allStudents = myDao.getAllStudents() // LiveData는 Observer를 통해 비동기적으로 데이터를 가져옴

9) UI와 연결 - LiveData

• LiveData<> 타입으로 리턴되는 DAO 메소드 경우
◦observe() 메소드를 이용하여 Observer를 지정
◦데이터가 변경될 때마다 자동으로 Observer의 onChanged()가 호출됨

• LiveData<>를 리턴하는 DAO 메소드는 Observer를 통해 비동기적으로 데이터를 받기 때문에, UI 스레드에서 직접 호출해도 문제 없음

val allStudents = myDao.getAllStudents()
allStudents.observe(this) { // Observer::onChanged() 는 SAM 이기 때문에 lambda로 대체
val str = StringBuilder().apply {
for ((id, name) in it) {
append(id)
append("-")
append(name)
append("\n")
}
}.toString()
binding.textStudentList.text = str
}

10) Room 예제

build.gradle(:app)

plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'kotlin-kapt'
}

android {
namespace 'com.android.roomexample'
compileSdk 33

defaultConfig {
    applicationId "com.android.roomexample"
    minSdk 31
    targetSdk 33
    versionCode 1
    versionName "1.0"

    testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
    release {
        minifyEnabled false
        proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
    }
}
compileOptions {
    sourceCompatibility JavaVersion.VERSION_1_8
    targetCompatibility JavaVersion.VERSION_1_8
}
kotlinOptions {
    jvmTarget = '1.8'
}
viewBinding {
    enabled = true
}

}

dependencies {

implementation 'androidx.core:core-ktx:1.10.0'
implementation 'androidx.appcompat:appcompat:1.6.1'
implementation 'com.google.android.material:material:1.9.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.4'
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'

def room_version = "2.5.1"
implementation "androidx.room:room-runtime:$room_version"
annotationProcessor "androidx.room:room-compiler:$room_version"
kapt "androidx.room:room-compiler:$room_version"
// optional - Kotlin Extensions and Coroutines support for Room
implementation "androidx.room:room-ktx:$room_version"
// optional - Test helpers
testImplementation "androidx.room:room-testing:$room_version"

}

activity_main.xml

<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:padding="5dp"
tools:context=".MainActivity">

<EditText
    android:id="@+id/edit_student_id"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:ems="10"
    android:hint="ID"
    android:inputType="number"
    app:layout_constraintEnd_toStartOf="@+id/query_student"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintHorizontal_chainStyle="spread_inside"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />

<EditText
    android:id="@+id/edit_student_name"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:ems="10"
    android:hint="student name"
    android:inputType="textPersonName"
    app:layout_constraintEnd_toStartOf="@+id/add_student"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintHorizontal_chainStyle="spread_inside"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/edit_student_id" />

<Button
    android:id="@+id/add_student"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Add Student"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintStart_toEndOf="@+id/edit_student_name"
    app:layout_constraintTop_toBottomOf="@+id/query_student" />

<Button
    android:id="@+id/query_student"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Query Student"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintHorizontal_bias="0.5"
    app:layout_constraintStart_toEndOf="@+id/edit_student_id"
    app:layout_constraintTop_toTopOf="parent" />

<TextView
    android:id="@+id/textView"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="16dp"
    android:text="Result of Query Student"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/edit_student_name" />

<TextView
    android:id="@+id/textView2"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_marginTop="24dp"
    android:text="Student List"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/text_query_student" />

<TextView
    android:id="@+id/text_query_student"
    android:layout_width="0dp"
    android:layout_height="100sp"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/textView" />

<TextView
    android:id="@+id/text_student_list"
    android:layout_width="0dp"
    android:layout_height="0dp"
    android:layout_marginBottom="8dp"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toBottomOf="@+id/textView2" />

</androidx.constraintlayout.widget.ConstraintLayout>

MyDatabase

package com.android.roomexample

import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.migration.Migration
import androidx.sqlite.db.SupportSQLiteDatabase

@Database(entities = [Student::class],
exportSchema = false, version = 1)
abstract class MyDatabase : RoomDatabase() {
abstract fun getMyDao() : MyDAO

companion object {
    private var INSTANCE: MyDatabase? = null
    private val MIGRATION_1_2 = object : Migration(1, 2) {
        override fun migrate(database: SupportSQLiteDatabase) {
        }
    }

    private val MIGRATION_2_3 = object : Migration(2, 3) {
        override fun migrate(database: SupportSQLiteDatabase) {
            database.execSQL("ALTER TABLE student_table ADD COLUMN last_update INTEGER")
        }
    }

    fun getDatabase(context: Context) : MyDatabase {
        if (INSTANCE == null) {
            INSTANCE = Room.databaseBuilder(
                context, MyDatabase::class.java, "school_database")
                .addMigrations(MIGRATION_1_2, MIGRATION_2_3)
                .build()
            // for in-memory database
            /*INSTANCE = Room.inMemoryDatabaseBuilder(
                context, MyDatabase::class.java
            ).build()*/
        }
        return INSTANCE as MyDatabase
    }
}

}

MyDao

package com.android.roomexample

import androidx.lifecycle.LiveData
import androidx.room.*

@Dao
interface MyDAO {
@Insert(onConflict = OnConflictStrategy.REPLACE) // INSERT, key 충돌이 나면 새 데이터로 교체
suspend fun insertStudent(student: Student)

@Query("SELECT * FROM student_table")
fun getAllStudents(): LiveData<List<Student>>        // LiveData<> 사용

@Query("SELECT * FROM student_table WHERE name = :sname")   // 메소드 인자를 SQL문에서 :을 붙여 사용
suspend fun getStudentByName(sname: String): List<Student>

@Delete
suspend fun deleteStudent(student: Student); // primary key is used to find the student

}

MyEntity

package com.android.roomexample

import androidx.room.*

@Entity(tableName = "student_table") // 테이블 이름을 student_table로 지정함
data class Student (
@PrimaryKey @ColumnInfo(name = "student_id") val id: Int,
val name: String
)

MainActivity.kt

package com.android.roomexample

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import com.android.roomexample.databinding.ActivityMainBinding
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MainActivity : AppCompatActivity() {

private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
lateinit var myDao: MyDAO

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(binding.root)

    myDao = MyDatabase.getDatabase(this).getMyDao()

    val allStudents = myDao.getAllStudents()
    allStudents.observe(this) {
        val str = StringBuilder().apply {
            for ((id, name) in it) {
                append(id)
                append("-")
                append(name)
                append("\n")
            }
        }.toString()
        binding.textStudentList.text = str
    }

    binding.addStudent.setOnClickListener {
        val id = binding.editStudentId.text.toString().toInt()
        val name = binding.editStudentName.text.toString()
        if (id > 0 && name.isNotEmpty()) {
            CoroutineScope(Dispatchers.IO).launch {
                myDao.insertStudent(Student(id, name))
            }
        }

        binding.editStudentId.text = null
        binding.editStudentName.text = null
    }

    binding.queryStudent.setOnClickListener {
        val name = binding.editStudentName.text.toString()
        CoroutineScope(Dispatchers.IO).launch {

            val results = myDao.getStudentByName(name)

            if (results.isNotEmpty()) {
                val str = StringBuilder().apply {
                    results.forEach { student ->
                        append(student.id)
                        append("-")
                        append(student.name)
                    }
                }
                withContext(Dispatchers.Main) {
                    binding.textQueryStudent.text = str
                }
            } else {
                withContext(Dispatchers.Main) {
                    binding.textQueryStudent.text = ""
                }
            }
        }
    }
}

}

// AndroidManifest.xml

<uses-permission android:name="android.permission.INTERNET"/>

<application
    android:allowBackup="true"
    android:dataExtractionRules="@xml/data_extraction_rules"
    android:fullBackupContent="@xml/backup_rules"
    android:icon="@mipmap/ic_launcher"
    android:label="@string/app_name"
    android:supportsRtl="true"
    android:theme="@style/Theme.MiseYa"
    android:usesCleartextTraffic="true"
    tools:targetApi="31">
    <activity
        android:name=".MainActivity"
        android:exported="true">
        <intent-filter>
            <action android:name="android.intent.action.MAIN" />
            <category android:name="android.intent.category.LAUNCHER" />
        </intent-filter>
    </activity>
</application>

// DustDTO.kt
package com.android.miseya.data

import com.google.gson.annotations.SerializedName

data class Dust(val response: DustResponse)

data class DustResponse(
@SerializedName("body")
val dustBody: DustBody,
@SerializedName("header")
val dustHeader: DustHeader
)

data class DustBody(
val totalCount: Int,
@SerializedName("items")
val dustItem: MutableList?,
val pageNo: Int,
val numOfRows: Int
)

data class DustHeader(
val resultCode: String,
val resultMsg: String
)

data class DustItem(
val so2Grade: String,
val coFlag: String?,
val khaiValue: String,
val so2Value: String,
val coValue: String,
val pm25Flag: String?,
val pm10Flag: String?,
val o3Grade: String,
val pm10Value: String,
val khaiGrade: String,
val pm25Value: String,
val sidoName: String,
val no2Flag: String?,
val no2Grade: String,
val o3Flag: String?,
val pm25Grade: String,
val so2Flag: String?,
val dataTime: String,
val coGrade: String,
val no2Value: String,
val stationName: String,
val pm10Grade: String,
val o3Value: String
)

// NetWorkClient.kt
package com.android.miseya.retrofit

import com.android.miseya.BuildConfig
import okhttp3.OkHttpClient
import okhttp3.logging.HttpLoggingInterceptor
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.util.concurrent.TimeUnit

object NetWorkClient {

private const val DUST_BASE_URL = "http://apis.data.go.kr/B552584/ArpltnInforInqireSvc/"


private fun createOkHttpClient(): OkHttpClient {
    val interceptor = HttpLoggingInterceptor()

    if (BuildConfig.DEBUG)
        interceptor.level = HttpLoggingInterceptor.Level.BODY
    else
        interceptor.level = HttpLoggingInterceptor.Level.NONE

    return OkHttpClient.Builder()
        .connectTimeout(20, TimeUnit.SECONDS)
        .readTimeout(20, TimeUnit.SECONDS)
        .writeTimeout(20, TimeUnit.SECONDS)
        .addNetworkInterceptor(interceptor)
        .build()
    }

private val dustRetrofit = Retrofit.Builder()
    .baseUrl(DUST_BASE_URL)
    .addConverterFactory(GsonConverterFactory.create())
    .client(createOkHttpClient())
    .build()

val dustNetwork: NetWorkInterface = dustRetrofit.create(NetWorkInterface::class.java)

}

// NetWorkInterface.kt
package com.android.miseya.retrofit

import com.android.miseya.data.Dust
import retrofit2.http.GET
import retrofit2.http.QueryMap

interface NetWorkInterface {
@GET("getCtprvnRltmMesureDnsty") // 시도별 실시간 측정정보 조회 주소
suspend fun getDust(@QueryMap param: HashMap<String, String>): Dust
}

// MainActivity.kt
package com.android.miseya

import android.graphics.Color
import android.os.Bundle
import android.util.Log
import androidx.appcompat.app.AppCompatActivity
import androidx.lifecycle.lifecycleScope
import com.android.miseya.data.DustItem
import com.android.miseya.databinding.ActivityMainBinding
import com.android.miseya.retrofit.NetWorkClient
import com.skydoves.powerspinner.IconSpinnerAdapter
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class MainActivity : AppCompatActivity() {
private val binding by lazy { ActivityMainBinding.inflate(layoutInflater) }
var items = mutableListOf()

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(binding.root)


    binding.spinnerViewSido.setOnSpinnerItemSelectedListener<String> { _, _, _, text ->
        communicateNetWork(setUpDustParameter(text))
    }

    binding.spinnerViewGoo.setOnSpinnerItemSelectedListener<String> { _, _, _, text ->

        Log.d("miseya", "selectedItem: spinnerViewGoo selected >  $text")
        var selectedItem = items.filter { f -> f.stationName == text }
        Log.d("miseya", "selectedItem: sidoName > " + selectedItem[0].sidoName)
        Log.d("miseya", "selectedItem: pm10Value > " + selectedItem[0].pm10Value)

        binding.tvCityname.text = selectedItem[0].sidoName + "  " + selectedItem[0].stationName
        binding.tvDate.text = selectedItem[0].dataTime
        binding.tvP10value.text = selectedItem[0].pm10Value + " ㎍/㎥"

        when (getGrade(selectedItem[0].pm10Value)) {
            1 -> {
                binding.mainBg.setBackgroundColor(Color.parseColor("#9ED2EC"))
                binding.ivFace.setImageResource(R.drawable.mise1)
                binding.tvP10grade.text = "좋음"
            }

            2 -> {
                binding.mainBg.setBackgroundColor(Color.parseColor("#D6A478"))
                binding.ivFace.setImageResource(R.drawable.mise2)
                binding.tvP10grade.text = "보통"
            }

            3 -> {
                binding.mainBg.setBackgroundColor(Color.parseColor("#DF7766"))
                binding.ivFace.setImageResource(R.drawable.mise3)
                binding.tvP10grade.text = "나쁨"
            }

            4 -> {
                binding.mainBg.setBackgroundColor(Color.parseColor("#BB3320"))
                binding.ivFace.setImageResource(R.drawable.mise4)
                binding.tvP10grade.text = "매우나쁨"
            }
        }
    }
}

private fun communicateNetWork(param: HashMap<String, String>) = lifecycleScope.launch() {
    val responseData = NetWorkClient.dustNetWork.getDust(param)
    Log.d("Parsing Dust ::", responseData.toString())

    val adapter = IconSpinnerAdapter(binding.spinnerViewGoo)
    items = responseData.response.dustBody.dustItem!!

    val goo = ArrayList<String>()
    items.forEach {
        Log.d("add Item :", it.stationName)
        goo.add(it.stationName)
    }

    runOnUiThread {
        binding.spinnerViewGoo.setItems(goo)
    }

}

private fun setUpDustParameter(sido: String): HashMap<String, String> {
    val authKey =
        "YBZ(....)SVg1pEC39CVbmsA=="

    return hashMapOf(
        "serviceKey" to authKey,
        "returnType" to "json",
        "numOfRows" to "100",
        "pageNo" to "1",
        "sidoName" to sido,
        "ver" to "1.0"
    )
}

fun getGrade(value: String): Int {
    val mValue = value.toInt()
    var grade = 1
    grade = if (mValue >= 0 && mValue <= 30) {
        1
    } else if (mValue >= 31 && mValue <= 80) {
        2
    } else if (mValue >= 81 && mValue <= 100) {
        3
    } else 4
    return grade
}

}

// colors.xml

#C51162 #AD1457 #C51162 #2B292B #424242 #212121 #8effffff #b2ffffff #ddffffff #edf8f8f8 #ffffffff #57A8D8 #FBC02D #FFD600 #FBC02D #FFA000 #FFA726 #FF6D00 #81C784 #388E3C #81D4fA #0091EA #AA00FF #7200CA #0091EA

// strings.xml

MiseYa

전국
서울
부산
대구
인천
광주
대전
울산
경기
강원
충북
충남
전북
전남
경북
경남
제주
세종

// themes.xml

<!-- Base application theme. -->
<style name="Theme.MiseYa" parent="Theme.Material3.DayNight.NoActionBar">
    <!-- Customize your light theme here. -->
    <!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>

<style name="Theme._12_MiseYa" parent="Base.Theme._12_MiseYa" />

// build.gradle.kts (:app)
plugins {
id("com.android.application")
id("org.jetbrains.kotlin.android")
}

android {
namespace = "com.android.miseya"
compileSdk = 33

defaultConfig {
    applicationId = "com.android.miseya"
    minSdk = 24
    targetSdk = 33
    versionCode = 1
    versionName = "1.0"

    testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
}

buildTypes {
    release {
        isMinifyEnabled = false
        proguardFiles(
            getDefaultProguardFile("proguard-android-optimize.txt"),
            "proguard-rules.pro"
        )
    }
}
compileOptions {
    sourceCompatibility = JavaVersion.VERSION_1_8
    targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
    jvmTarget = "1.8"
}
buildFeatures {
    viewBinding = true
    dataBinding = true
}

}

dependencies {

implementation("androidx.core:core-ktx:1.9.0")
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.9.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")


implementation("com.google.code.gson:gson:2.10.1")
implementation("com.squareup.retrofit2:retrofit:2.9.0")
implementation("com.squareup.retrofit2:converter-gson:2.9.0")
implementation("com.squareup.okhttp3:okhttp:4.10.0")
implementation("com.squareup.okhttp3:logging-interceptor:4.10.0")

implementation("com.github.skydoves:powerspinner:1.2.6")

}

LifeCycle

MainActivity.kt

package com.example.lifecycle

import android.os.Bundle
import android.util.Log
import android.view.View
import android.widget.Button
import androidx.appcompat.app.AppCompatActivity
import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentTransaction

class MainActivity : AppCompatActivity() {

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    setContentView(R.layout.activity_main)
    Log.d("MainActivity", "onCreate 호출")

    // 버튼 클릭 시 토스트 메시지 출력
    val showToastButton = findViewById<Button>(R.id.showToastButton)
    showToastButton.setOnClickListener {
        showToast("Hello World")
    }

    // Fragment 추가 버튼
    val addFragmentButton = findViewById<Button>(R.id.addFragmentButton)
    addFragmentButton.setOnClickListener {
        val fragment = MyFragment()
        supportFragmentManager.beginTransaction()
            .add(R.id.fragmentContainer, fragment, "myFragment")
            .commit()
    }

    // Fragment 제거 버튼
    val removeFragmentButton = findViewById<Button>(R.id.removeFragmentButton)
    removeFragmentButton.setOnClickListener {
        val fragment = supportFragmentManager.findFragmentByTag("myFragment")
        if (fragment != null) {
            supportFragmentManager.beginTransaction()
                .remove(fragment)
                .commit()
        }
    }
}

override fun onStart() {
    super.onStart()
    Log.d("MainActivity", "onStart 호출")
}

override fun onResume() {
    super.onResume()
    Log.d("MainActivity", "onResume 호출")
}

override fun onPause() {
    super.onPause()
    Log.d("MainActivity", "onPause 호출")
}

override fun onStop() {
    super.onStop()
    Log.d("MainActivity", "onStop 호출")
}

override fun onDestroy() {
    super.onDestroy()
    Log.d("MainActivity", "onDestroy 호출")
}

}

MyFragment.kt

package com.example.lifecycle

import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment

class MyFragment : Fragment() {

override fun onCreate(savedInstanceState: Bundle?) {
    super.onCreate(savedInstanceState)
    Log.d("MyFragment", "onCreate 호출")
}

override fun onCreateView(
    inflater: LayoutInflater, container: ViewGroup?,
    savedInstanceState: Bundle?
): View? {
    return inflater.inflate(R.layout.fragment_my, container, false)
}

override fun onStart() {
    super.onStart()
    Log.d("MyFragment", "onStart 호출")
}

override fun onResume() {
    super.onResume()
    Log.d("MyFragment", "onResume 호출")
}

override fun onPause() {
    super.onPause()
    Log.d("MyFragment", "onPause 호출")
}

override fun onStop() {
    super.onStop()
    Log.d("MyFragment", "onStop 호출")
}

override fun onDestroyView() {
    super.onDestroyView()
    Log.d("MyFragment", "onDestroyView 호출")
}

override fun onDestroy() {
    super.onDestroy()
    Log.d("MyFragment", "onDestroy 호출")
}

}

ExtensionFunctions.kt

package com.example.lifecycle

import android.content.Context
import android.widget.Toast

fun Context.showToast(message: String, duration: Int = Toast.LENGTH_SHORT) {
Toast.makeText(this, message, duration).show()
}

activity_main.xml

<!-- Fragment를 추가하거나 제거하는 버튼 -->
<Button
    android:id="@+id/addFragmentButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Add Fragment" />

<Button
    android:id="@+id/removeFragmentButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Remove Fragment" />

<!-- 확장 함수로 토스트 메시지를 출력하는 버튼 -->
<Button
    android:id="@+id/showToastButton"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Show Toast Message" />

<FrameLayout
    android:id="@+id/fragmentContainer"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:layout_gravity="center"/>
# fragment_my.xml
<!-- Fragment 내용을 정의하는 레이아웃 -->
<TextView
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:text="Fragment Content"
    android:textSize="18sp" />
profile
열심히 하자

0개의 댓글