2023년 9월 11일 18조 팀프로젝트 발표
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'
}
<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>
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))
}
}
}
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
}
}
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 //구글 권장 메모리 누수 방지
}
}
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)
}
}
}
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))
}
}
}
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()
}
}
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 //구글 권장 메모리 누수 방지
}
}
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()
}
}
}
package com.example.contactapp.main
import androidx.fragment.app.Fragment
data class MainTabs(
val fragment: Fragment,
val title: Int
)
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
}
}
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))
}
}
}
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()
}
}
}
<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>
<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>
<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>
<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>
<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>
<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>
<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" />
<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>
<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>
<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>
<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>
· 프로그램의 설정 정보(사용자의 옵션 선택 사항이나 프로그램의 구성 정보)를 영구적으로 저장하는 용도로 사용
· XML 포맷의 텍스트 파일에 키-값 세트로 정보를 저장.
· SharedPreferences 클래스
◦Preferences의 데이터(키-값 세트)를 관리하는 클래스
◦응용 프로그램 내의 액티비티 간에 공유하며, 한쪽 액티비티에서 수정 시 다른 액티비티에서도 수정된 값을 읽을 수 있다.
◦응용 프로그램의 고유한 정보이므로 외부에서는 읽을 수 없다.
• 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)
EditText를 만들어 text 입력 후, 저장 버튼을 누르면 입력한 text를 저장하고,
앱을 다시 열면 저장된 text가 그대로 유지될 수 있도록 해보자.
(SharedPreferences를 쓰지 않는 일반적인 경우, text가 저장되지 않아 사라진다)
Android Studio에서 기본 프로젝트(with empty activity) 생성하자!
<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>
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",""))
}
}
• Room
◦ SQLite를 쉽게 사용할 수 있는 데이터베이스 객체 매핑 라이브러리
◦ 쉽게 Query를 사용할 수 있는 API를 제공
◦Query를 컴파일 시간에 검증함
◦Query 결과를 LiveData로 하여 데이터베이스가 변경될 때마다 쉽게 UI를 변경할 수 있음
• SQLite보다 Room을 사용할 것을 권장함
• Room 주요 3요소
◦ @Database: 클래스를 데이터베이스로 지정하는 annotation, RoomDatabase를 상속 받은 클래스여야 함
• Room.databaseBuilder를 이용하여 인스턴스를 생성함
◦ @Entity: 클래스를 테이블 스키마로 지정하는 annotation
◦ @Dao: 클래스를 DAO(Data Access Object)로 지정하는 annotation
• 기본적은 insert, delete, update SQL은 자동으로 만들어주며, 복잡한 SQL은 직접 만들 수 있음
• 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"
}
• 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
)
• 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으로 사용
• 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
}
}
}
• 앞에서 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")
}
}
• 안드로이드 아키텍처에 따라 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를 통해 비동기적으로 데이터를 가져옴
• 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
}
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"
}
<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>
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
}
}
}
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
}
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
)
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")
}
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 호출")
}
}
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 호출")
}
}
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()
}
<!-- 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" />