Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

부산대 Android_이지은 1주차 과제 #33

Open
wants to merge 13 commits into
base: jieunyume
Choose a base branch
from
Open
27 changes: 27 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -1 +1,28 @@
# android-contacts
## 기능 목록
### step1
- 연락처 정보 입력하기
- 이름: 필수-Toast 메세지(이름을 입력해주세요.)
- 전화번호
- 필수-Toast 메세지(전화번호를 입력해주세요.)
- 숫자만 가능-Toast 메세지(전화번호는 숫자로만 입력해주세요.)
- 메일
- 더보기
- 클릭하면 입력 폼이 확장된다.
- 생일: 캘린더
- 성별: 여자, 남자 중에 선택한다.
- 메모
- 저장하기
- Toast(저장이 완료되었습니다.)
- 취소하기
- Toast(취소되었습니다.)

### step2
- 연락처 목록
- 이름 앞글자가 적힌 사진과 이름 정보가 나타난다.
- 스크롤이 가능하다.
- 연락처 추가
- 추가 도중 취소하면 확인 팝업이 나타난다.
- 추가가 완료되면 Toast(저장이 완료되었습니다.)
- 연락처 상세 보기
- 이름과 전화번호 정보가 나타난다.
1 change: 1 addition & 0 deletions app/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ dependencies {
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("com.google.android.material:material:1.11.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.activity:activity:1.9.0")
testImplementation("junit:junit:4.13.2")
androidTestImplementation("androidx.test.ext:junit:1.1.5")
androidTestImplementation("androidx.test.espresso:espresso-core:3.5.1")
Expand Down
10 changes: 8 additions & 2 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,13 @@
android:theme="@style/Theme.Contacts"
tools:targetApi="31">
<activity
android:name=".MainActivity"
android:name=".ContactDetailActivity"
android:exported="false" />
<activity
android:name=".ContactAddActivity"
android:exported="false" />
<activity
android:name=".ContactListActivity"
android:exported="true">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
Expand All @@ -23,4 +29,4 @@
</activity>
</application>

</manifest>
</manifest>
98 changes: 98 additions & 0 deletions app/src/main/java/campus/tech/kakao/contacts/ContactAddActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
package campus.tech.kakao.contacts

import android.app.DatePickerDialog
import android.content.Intent
import android.os.Bundle
import android.view.View
import android.widget.Button
import android.widget.EditText
import android.widget.RadioGroup
import android.widget.Toast
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.widget.LinearLayoutCompat
import java.util.Calendar

class ContactAddActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)

val name_input: EditText = findViewById(R.id.name_input)
val phone_number_input: EditText = findViewById(R.id.phone_number_input)
val email_input: EditText = findViewById(R.id.email_input)

val birth_date_input: EditText = findViewById(R.id.birth_date_input)
val birth_date_radiogroup: RadioGroup = findViewById(R.id.birth_date_radiogroup)

val gender_input: EditText = findViewById(R.id.gender_input)
val memo_input: EditText = findViewById(R.id.memo_input)

val save_button: Button = findViewById(R.id.save_button)
val cancel_button: Button = findViewById(R.id.cancel_button)
val see_more_button: LinearLayoutCompat = findViewById(R.id.see_more_button)
val see_more_input_form: LinearLayoutCompat = findViewById(R.id.see_more_input_form)
Comment on lines +20 to +33
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기본적으로 코틀린은 Kotlin coding convention 을 따르게 됩니다.
코딩 컨벤션을 따르게 된다면 다른 리뷰어가 지은님의 코드를 보게될 때 비교적 쉽게 코드를 읽게 도와줍니다.
(https://velog.io/@productuidev/coding-conventionstyle-guide)

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

또한 ViewBinding 이라는 것이 있어요.
findViewById 의 번거로움을 획기적으로 줄여줄 뿐더러, null safety, type safety 하다는 장점을 가지기도 합니다.

ViewBInding 을 사용하신다면 조금 더 코드작성이 수월해질 것으로 예상됩니다.


see_more_input_form.visibility = View.GONE
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

View 의 extension 함수 중 isVisible 이라는 놈이 존재합니다.

이것을 사용한다면 아래처럼 조금 더 간편하게 visibility 를 컨트롤할 수 있을 것입니다.

seeMoreInputForm.isVisible = false 

birth_date_input.setClickable(false)
birth_date_input.setFocusable(false)
gender_input.setClickable(false)
gender_input.setFocusable(false)

see_more_button.setOnClickListener{
see_more_button.visibility = View.GONE
see_more_input_form.visibility = View.VISIBLE
}

birth_date_input. setOnClickListener{
var calendar = Calendar.getInstance()
var year = calendar.get(Calendar.YEAR)
var month = calendar.get(Calendar.MONTH)
var day = calendar.get(Calendar.DAY_OF_MONTH)
this.let { it1 ->
DatePickerDialog(it1, { _, year, month, day ->
run {
birth_date_input.setText(year.toString() + "." + (month + 1).toString() + "." + day.toString())
}
}, year, month, day)
}?.show()
Comment on lines +51 to +57
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

여기서의 let 은 사용하지 않는 것이 좋을 것 같아요. apply, let, run, also, with 등을 범위 지정 함수라고 하는데, 코틀린 코드에 익숙해지기 전까지는 사용하지 않는 것이 조금 더 성장에 도움이 될 것 같아요.

만약 사용하고 싶다면, 도움이 되는 레퍼런스 공유드려요.
(https://kotlinworld.com/255)

}

birth_date_radiogroup.run {
setOnCheckedChangeListener { group, checkedId ->
when(checkedId){
R.id.woman_radiobutton -> {gender_input.setText("여성")}
R.id.man_radiobutton -> {gender_input.setText("남성")}
}
}
}

save_button.setOnClickListener {
if (name_input.text.isNullOrEmpty()) {
Toast.makeText(this, "이름을 입력해주세요.", Toast.LENGTH_SHORT).show()
}
else if (phone_number_input.text.isNullOrEmpty()) {
Toast.makeText(this, "전화번호를 입력해주세요.", Toast.LENGTH_SHORT).show()
} else if (!phone_number_input.text.all { Character.isDigit(it) }){
Toast.makeText(this, "전화번호는 숫자로만 입력해주세요.", Toast.LENGTH_SHORT).show()
}
else{
Toast.makeText(this, "저장이 완료되었습니다.", Toast.LENGTH_SHORT).show()
val contact: Contact = Contact(
name_input.text.toString(),
phone_number_input.text.toString(),
email_input.text.toString(),
birth_date_input.text.toString(),
gender_input.text.toString(),
memo_input.text.toString()
)
val intent: Intent = Intent()
intent.putExtra("contactInfo", contact)
setResult(RESULT_OK, intent)
finish()
}
Comment on lines +70 to +92
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이렇게 if 문이 반복해서 나올 경우에는 when 절으로 처리하는 것이 조금 더 명확합니다.

when {
  name_input.text.isNullOrEmpty() -> {   
    Toast.makeText(this, "이름을 입력해주세요.", Toast.LENGTH_SHORT).show()
  }
  phone_number_input.text.isNullOrEmpty() -> {
    Toast.makeText(this, "전화번호를 입력해주세요.", Toast.LENGTH_SHORT).show()
  }
  !phone_number_input.text.all { Character.isDigit(it) } -> {
    Toast.makeText(this, "전화번호는 숫자로만 입력해주세요.", Toast.LENGTH_SHORT).show()
  }
  else -> {
    // do else things...
  }
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

또한 각 when 절에서 직접 수식값을 사용하기보다, 명확한 boolean 형태의 값을 지역변수로 선언하여 사용하면 리뷰어가 리뷰하기 조금 더 수월해집니다. 추후 코드 관리 측면에서도 이해하기 편하구요.

val isNameEmpty = name_input.text.isNullOrEmpty()
val isPhoneNumberEmpty = phone_number_input.text.isNullOrEmpty()
val isPhoneNumberNotDigits = !phone_number_input.text.all { Character.isDigit(it) }

when {
  isNameEmpty-> {   
    Toast.makeText(this, "이름을 입력해주세요.", Toast.LENGTH_SHORT).show()
  }
  isPhoneNumberEmpty-> {
    Toast.makeText(this, "전화번호를 입력해주세요.", Toast.LENGTH_SHORT).show()
  }
  isPhoneNumberNotDigits -> {
    Toast.makeText(this, "전화번호는 숫자로만 입력해주세요.", Toast.LENGTH_SHORT).show()
  }
  else -> {
    // do else things...
  }
}

}
cancel_button.setOnClickListener {
Toast.makeText(this, "취소되었습니다.", Toast.LENGTH_SHORT).show()
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package campus.tech.kakao.contacts

import android.os.Bundle
import android.widget.TextView
import androidx.activity.enableEdgeToEdge
import androidx.appcompat.app.AppCompatActivity
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat

class ContactDetailActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_contact_detail)

val contact = intent.getSerializableExtra("contactDetail") as Contact
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이부분 또한 아쉬웠어요. Contact 정보가 필요하다면, Contact 를 Parcelable 하게 만드는 것이 조금 더 좋아보입니다.

또한 kotlin-parcelize 를 사용한다면 조금 더 Contact 를 parcelable 하게 만들 수 있습니다.

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 parcelable 을 쓰지 않더라도, contact 내부의 값을 모두 따로따로 String 으로 주고받아도 좋을 것 같아요. 예를들어,

val name = intent.getStringExtra("name")
val phoneNumber = intent.getStringExtra("phoneNumber")
val email = intent.getStringExtra("email")
// 이하 생략

val contact = Context(
  name = name,
  phoneNumber = phoneNumber,
  email = email,
 // 이하 생략
)


val nameTextView: TextView = findViewById<TextView>(R.id.contact_name)
val phoneNumberTextView: TextView = findViewById(R.id.contact_phone_number)

nameTextView.text = contact.name
phoneNumberTextView.text = contact.phoneNumber
}
}
115 changes: 115 additions & 0 deletions app/src/main/java/campus/tech/kakao/contacts/ContactListActivity.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
package campus.tech.kakao.contacts

import android.content.Intent
import android.os.Bundle
import android.util.Log
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import android.widget.ImageView
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import androidx.core.content.ContextCompat.startActivity
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import java.io.Serializable

class ContactListActivity : AppCompatActivity() {
private lateinit var contactList: MutableList<Contact>
private lateinit var adapter: ContactRecyclerAdapter
private lateinit var message: TextView
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_contact_list)

contactList = mutableListOf<Contact>()

val contactRecyclerView: RecyclerView = findViewById<RecyclerView>(R.id.contact_list_recyclerview)
message = findViewById<TextView>(R.id.add_message)

adapter = ContactRecyclerAdapter(
contactList = contactList,
inflater = LayoutInflater.from(this@ContactListActivity),
activity = this@ContactListActivity
)
contactRecyclerView.layoutManager = LinearLayoutManager(this@ContactListActivity)
contactRecyclerView.adapter = adapter

val addButton: ImageView = findViewById<ImageView>(R.id.add_button)
addButton.setOnClickListener{
val intent: Intent = Intent(this@ContactListActivity, ContactAddActivity::class.java)
startActivityForResult(intent, 1)
}
}

override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
when(requestCode){
1 -> {
when(resultCode){
AppCompatActivity.RESULT_OK -> {
val contact: Contact? = data?.getSerializableExtra("contactInfo") as Contact?
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

오호 흥미로운 주제네요. kotlin 의 safe cast 와 nullable cast 의 충돌인데, 한번 해당 링크 확인해보면 좋을 것 같네요 :)

contact?.let {
contactList.add(it)
adapter.notifyItemInserted(contactList.size-1)
}
message.visibility=View.GONE
}
}
}
}
super.onActivityResult(requestCode, resultCode, data)
}
}

class Contact(
val name: String,
val phoneNumber: String,
val email: String?,
birthDate: String?,
gender: String?,
memo: String?
) : Serializable

class ContactRecyclerAdapter(
private val contactList: MutableList<Contact>,
private val inflater: LayoutInflater,
private val activity: ContactListActivity
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

activity 를 변수로 넘기는 것은 금기시해야할 것 중 하나입니다. inflater 또한 파라미터로 받을 필요는 없을 것 같구요.
아이템을 클릭했을 때 이동해야한다면, 아이템이 클릭되었음을 알려주는 listener 를 정의하고 그것을 파라미터로 받는 것이 좋습니다.

interface OnContactClickListener {
    fun onContactClicked(position: Int)
}

) : RecyclerView.Adapter<RecyclerView.ViewHolder>() {

inner class ViewHolder(val itemView: View) : RecyclerView.ViewHolder(itemView) {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요거 굳이 inner class 로 사용하는 이유가 있을까요?

val nameInitialTextView: TextView
val nameTextView: TextView
init {
nameInitialTextView = itemView.findViewById(R.id.name_initial_text)
nameTextView = itemView.findViewById(R.id.name_text)

itemView.setOnClickListener {
val position = adapterPosition
if (position != RecyclerView.NO_POSITION) {
val contact = contactList[position]
val intent = Intent(activity, ContactDetailActivity::class.java)
intent.putExtra("contactDetail", contact)
activity.startActivity(intent)
}
}
}
}


override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): RecyclerView.ViewHolder {
return ViewHolder(inflater.inflate(R.layout.item_contact, parent, false))
}

override fun getItemCount(): Int {
return contactList.size
}

override fun onBindViewHolder(holder: RecyclerView.ViewHolder, position: Int) {
val contact = contactList.get(position)
(holder as ViewHolder).nameInitialTextView.text = contact.name.substring(0 until 1)
(holder as ViewHolder).nameTextView.text = contact.name
}

}


11 changes: 0 additions & 11 deletions app/src/main/java/campus/tech/kakao/contacts/MainActivity.kt

This file was deleted.

Binary file added app/src/main/res/drawable/add_icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/arrow_down.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added app/src/main/res/drawable/contacts_book.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
66 changes: 66 additions & 0 deletions app/src/main/res/layout/activity_contact_detail.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.appcompat.widget.LinearLayoutCompat 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/main"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".ContactAddActivity"
android:orientation="vertical"
android:gravity="center_horizontal"
android:layout_weight="1"
android:background="@color/white">

<ImageView
android:layout_width="80dp"
android:layout_height="80dp"
android:src="@drawable/contacts_book"/>
<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="이름"
android:textSize="25dp"
android:textColor="@color/black"/>
<TextView
android:id="@+id/contact_name"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="이름"
android:layout_marginLeft="10dp"
android:textSize="25dp"
android:textColor="@color/black"/>

</androidx.appcompat.widget.LinearLayoutCompat>


<androidx.appcompat.widget.LinearLayoutCompat
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="10dp">
<TextView
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="전화번호"
android:textSize="25dp"
android:textColor="@color/black"/>
<TextView
android:id="@+id/contact_phone_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:hint="전화번호"
android:layout_marginLeft="10dp"
android:textSize="25dp"
android:textColor="@color/black"/>

</androidx.appcompat.widget.LinearLayoutCompat>

</androidx.appcompat.widget.LinearLayoutCompat>





Loading