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주차_과제 #10

Open
wants to merge 39 commits into
base: yjy1220
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 33 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
34f79e7
docs: readme.md func
Jun 26, 2024
bce177a
feat: func1 - make form
Jun 26, 2024
175d6e3
feat: func2 - profile image
Jun 26, 2024
0e91fa8
docs: readme
Jun 26, 2024
7aed137
feat: func 3 - input field base
Jun 26, 2024
e0144a9
feat: func4 - necessary phone number
Jun 26, 2024
232d9c3
feat: func5 - more toggle
Jun 26, 2024
4fe166e
feat: func6 - choose birth with calendar
Jun 26, 2024
6181c41
feat: func7 - choose gender
Jun 26, 2024
f5a7482
feat: func8 - cancel
Jun 26, 2024
750f6bd
feat: func9 - save
Jun 26, 2024
e0fa7b5
chore: toggle textview background color
Jun 26, 2024
d98073f
sub01[02] - docs:readme.md
Jun 27, 2024
020f0d6
feat: sub01[02] func1 - add contact
Jun 27, 2024
3cdea64
feat: sub01[02] func2 - main view floating button, text
Jun 27, 2024
a416479
docs: sub01[02] readme.md
Jun 27, 2024
e83af53
feat: sub01[02] func3 - contact object
Jun 27, 2024
538a974
docs: sub01[02] readme.md
Jun 27, 2024
015d5ba
feat: sub01[02] func4 - express Recyclerview with intent
Jun 27, 2024
9bdd6bb
feat: sub01[02] func5 - list view with recyclerview in main
Jun 27, 2024
9643a3e
feat: sub01[02] func6 - scroll & detail click
Jun 27, 2024
7c0d571
feat: sub01[02] func7 - detail view
Jun 27, 2024
f3dbeb2
feat: sub01[02] func8 - detail view condition
Jun 27, 2024
5f1ac66
fix: sub01[02] add activity in manifest.xml
Jun 27, 2024
8471f81
fix: sub01[02] notice adapter about changed data
Jun 27, 2024
3378f9d
docs: sub01[02] readme.md
Jun 27, 2024
34ea9c1
docs: video image
YJY1220 Jun 27, 2024
606fed8
docs : complete README.md
YJY1220 Jun 27, 2024
eee04a8
fix: sub01[02] choose gender default
Jun 27, 2024
8ae4646
fix: sub01[02] detail view with gender default
Jun 27, 2024
c9edb53
fix: sub01[02] pick calendar
Jun 27, 2024
c6dcba1
sub01[02] delete package
Jun 27, 2024
e9e8ec9
docs: complete readme.md
YJY1220 Jun 27, 2024
1910cee
chore : contact_add.xml radiogroup
Jul 1, 2024
e81c6e9
fix: string / private field
Jul 3, 2024
b9e0400
Merge branch 'feat-JiyeYU' of https://github.com/YJY1220/android-cont…
Jul 3, 2024
b40e90a
fix: field instance one-time
Jul 3, 2024
e9cdf63
perf: runtime func
Jul 3, 2024
207f8cf
docs: Update README.md
YJY1220 Jul 5, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
95 changes: 94 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,94 @@
# android-contacts
# KakaoTechCampus 2기 step2
## 1주차 1단계 과제 - 연락처 추가

### 기능 설명
1. 전체 폼 디자인
2. 프로필 이미지 설정
3. 입력 필드 기능
4. 전화번호 필수 입력 값 기능
5. 더보기 토글 기능
6. 생일 선택 캘린더 팝업 기능
7. 성별 선택 기능
8. 취소 기능
9. 저장 기능

---

## 1주차 2단계 과제 - 연락처 목록

### 화면 3가지
> main 화면
> > 오른쪽 하단에 + 블록 버튼이 있는 연락처 조회 및 추가 시작 화면

> 연락처 추가 화면
> > 1단계에서 구현한 화면

> 연락처 세부사항 조회 화면
> > 추가된 연락처를 누룰 시, 해당 연락처 프로필 이미지, 이름, 전화번호 조회 가능

### 작동 방법
1. main 화면에서 오른쪽 하단의 + 버튼을 누르면 '연락처 추가 화면'으로 넘어간다.
2. 해당 '연락처 추가 화면'에서 이름과 전화번호(필수값)을 입력 후 '저장'을 누르면 다시 초기의 main 화면으로 돌아간다. 단, 이때 main 화면에는 추가한 연락처 목록이 조회된다.
3. 해당 연락처 목록을 클릭 시 '연락처 세부사항 조회 화면'으로 넘어간다.
4. 해당 '연락처 세부사항 조회 화면'에는 입력한 이름, 전화번호(필수값)이 있고, 그 외 입력한 값들이 있다면 그 정보들이 추가로 떠 있다.
5. 이후 뒤로가기 버튼을 누르면 다시 main 화면으로 돌아간다.
6. 앱을 종료 시 해당 데이터들은 모두 리셋된다.

### 기능 구현
1. 연락처 추가 기능 구현 - 1단계 구현 내용
2. main 화면 플로팅 버튼 및 텍스트 구현
3. 추가한 연락처 데이터 - contact 객체 - intent로 전달 기능 구현
4. 전달받은 객체 데이터를 Recyclerview로 표현
5. 연락처 추가 시 항목 리스트 조회 기능 구현
6. 추가된 연락처 스크롤 및 세부사항 화면 클릭 기능 구현
7. 연락처 세부 사항 조회 기능 구현
8. 세부 조회 데이터 조건 구현

---

### 전체 디렉토리 구조 (2단계 기준)
```bash
app
├── build
├── src
│ ├── androidTest
│ │ └── java
│ │ └── contacts
│ │ └── .gitkeep
│ ├── main
│ │ ├── java
│ │ │ └── contacts
│ │ │ ├── Contact.kt
│ │ │ ├── ContactAddActivity.kt
│ │ │ ├── ContactDetailActivity.kt
│ │ │ ├── ContactsAdapter.kt
│ │ │ └── MainActivity.kt
│ │ └── res
│ │ ├── drawable
│ │ │ ├── circle_background.xml
│ │ │ ├── ic_add.xml
│ │ │ ├── ic_launcher_background.xml
│ │ │ ├── ic_launcher_foreground.xml
│ │ │ ├── profile_image.png
│ │ │ ├── round_background.xml
│ │ │ └── round_corners.xml
│ │ └── layout
│ │ ├── activity_main.xml
│ │ ├── contact_add.xml
│ │ ├── contact_detail.xml
│ │ └── contact_item.xml
├── .gitignore
├── build.gradle.kts
└── proguard-rules.pro
```
### 구현 화면
<img src="https://github.com/YJY1220/Programmers_prac/assets/93771689/6cd67398-f9af-483a-8f5c-d2f50c154352" width="200" height="400"/>
<img src="https://github.com/YJY1220/Programmers_prac/assets/93771689/bb312d57-7b6d-4c30-ab6a-febfadb8d7cc" width="200" height="400"/>
<img src="https://github.com/YJY1220/Programmers_prac/assets/93771689/f522fb92-7139-469f-b7d2-fffdf0f75d9d" width="200" height="400"/>
<img src="https://github.com/YJY1220/Programmers_prac/assets/93771689/ea04d308-2bb5-4387-9b2c-2e9853cb37ee" width="200" height="400"/>
<img src="https://github.com/YJY1220/Programmers_prac/assets/93771689/b2e5f425-d791-4265-8b07-da94115e3044" width="200" height="400"/>
<img src="https://github.com/YJY1220/Programmers_prac/assets/93771689/f6f155cd-7e36-4c83-9be9-24aa1165c2e6" width="200" height="400"/>
<img src="https://github.com/YJY1220/Programmers_prac/assets/93771689/fcc10402-f863-4a88-9b59-f36be7779d0e" width="200" height="400"/>

### 실행 영상 (전체 실행영상)
[https://youtu.be/re7rHcPNlhE](https://youtu.be/S2kDtQot_4Y)
2 changes: 2 additions & 0 deletions app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".ContactAddActivity"/>
<activity android:name=".ContactDetailActivity"/>
</application>

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

import android.os.Parcel
import android.os.Parcelable

//연락처 데이터 객체 클래스
data class Contact (
val name: String,
val phone: String,
val email: String?,
val birth: String?,
Copy link

Choose a reason for hiding this comment

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

날짜를 좀 더 직접적으로 나타내는 Date나 Kotlin.date.Instant 에 대해서도 알아보면 어떨까 합니다. :)

Copy link
Author

Choose a reason for hiding this comment

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

아하, 날짜를 더 직접적으로 나타내는 방법이 있었는지 몰랐습니다. 더 공부해보겠습니다. 감사합니다!

val gender: String?,
val memo: String?
) : Parcelable { //intent로 전달하는 거
constructor(parcel: Parcel) : this(
parcel.readString() ?: "",
parcel.readString() ?: "",
parcel.readString(),
parcel.readString(),
parcel.readString(),
parcel.readString()
)

//Contact 객체 데이터 내용을 parcel에 쓰기
override fun writeToParcel(parcel: Parcel, flags: Int){
parcel.writeString(name)
parcel.writeString(phone)
parcel.writeString(email)
parcel.writeString(birth)
parcel.writeString(gender)
parcel.writeString(memo)
}

//오류 해결
//Parcelable 필수
override fun describeContents(): Int {
return 0
}

//Parcelable 객체 생성
companion object CREATOR: Parcelable.Creator<Contact> {
override fun createFromParcel(parcel: Parcel): Contact {
return Contact(parcel)
}

//parcelable 인터페이스의 객체 직렬화 시 필수 - newArray method
override fun newArray(size:Int):Array<Contact?>{
return arrayOfNulls(size)
}
}

}
110 changes: 110 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,110 @@
package campus.tech.kakao.contacts

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.app.DatePickerDialog //날짜 선택
import java.text.SimpleDateFormat // 날짜 형식 지정
import java.util.*
import android.widget.*
import android.content.Intent

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

val nameText = findViewById<EditText>(R.id.nameText)
val phoneText = findViewById<EditText>(R.id.phoneText)
val emailText = findViewById<EditText>(R.id.emailText)
val birthText = findViewById<TextView>(R.id.birthText)
val memoText = findViewById<EditText>(R.id.memoText)
val moreLayout = findViewById<LinearLayout>(R.id.moreLayout)
val moreText = findViewById<TextView>(R.id.moreText)
val cancelButton = findViewById<Button>(R.id.cancelButton)
val saveButton = findViewById<Button>(R.id.saveButton)
val genderRadio = findViewById<RadioGroup>(R.id.genderRadio)
val femaleButton = findViewById<RadioButton>(R.id.femaleButton)
val maleButton = findViewById<RadioButton>(R.id.maleButton)
Copy link

Choose a reason for hiding this comment

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

private 필드변수화 필요.

Copy link
Author

Choose a reason for hiding this comment

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

e81c6e9
private 필드변수화 완료했습니다.
여러 개를 한번에 수정하다보니 하나의 커밋에 기능 변경사항이 여러개가 되었는데 다음부터는 하나의 커밋에 하나의 변경사항만 담겠습니다..!


//호출
moreText.setOnClickListener{
toggleMore()
}

birthText.setOnClickListener{
showDatePicker()
}

cancelButton.setOnClickListener {
Toast.makeText(this,"취소되었습니다.", Toast.LENGTH_SHORT).show()
finish() //main 화면으로 돌아가기
}

saveButton.setOnClickListener {
val name = nameText.text.toString().trim()
val phone = phoneText.text.toString().trim()
val email = emailText.text.toString().trim()
val birth = birthText.text.toString().trim()
val memo = memoText.text.toString().trim()

//이름 or 전화번호 미 입력 시 저장 불가
if(name.isEmpty() || phone.isEmpty()){
Toast.makeText(this, "이름과 전화번호는 필수 입력 값입니다.", Toast.LENGTH_SHORT).show()
return@setOnClickListener
}

//성별 선택
//아무런 성별도 선택하지 않은 경우 " " 값이 default 이어야 함
val gender = when(genderRadio.checkedRadioButtonId){
R.id.femaleButton -> "여성"
R.id.maleButton -> "남성"
else -> " "
}

//contact 객체 생성
val contact = Contact(name, phone, email, birth, gender, memo)

//intent로 결과 반환
val resultIntent = Intent()
resultIntent.putExtra("contact", contact)
Copy link

Choose a reason for hiding this comment

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

보통 이런 요소는 Constant라는 object를 파일단위로 생성하고
const val NAV_KEY_POSITION = "contact"를 변수로 세팅하여 사용합니다.
그리고 받는쪽에서도 이것을 사용하는거죠.

Copy link

Choose a reason for hiding this comment

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

객체 자체를 넣기보다는 전화번호부의 키값이 될만한 별도의 id 값이나, 혹은 현재까지 세팅된 것중에서는 phone 번호만 넘기고 넘어가는 다음페이지에서 해당 정보를가지고 쿼리를 하듯이 별도로 Contact를 로드해오는게 이상적으론 더 나은 구현이긴합니다.

Copy link
Author

Choose a reason for hiding this comment

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

넵, 다음엔 그렇게 구현해보겠습니다..! 더 코드가 깔끔해질 것 같습니다. 감사합니다!

setResult(RESULT_OK, resultIntent)

Toast.makeText(this, "저장되었습니다.", Toast.LENGTH_SHORT).show()
Copy link

Choose a reason for hiding this comment

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

"저장되었습니다." String은 추후에 영어등의 다른 언어로 번역해서 쓰일수있으니 strings.xml에서 관리하는게 어떨까요?

Copy link
Author

Choose a reason for hiding this comment

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

e81c6e9
strings.xml에서 관리하도록 변경했습니다!

finish()
}
}

//더보기 토글 기능 - 표시 및 숨기기
private fun toggleMore(){
val moreLayout = findViewById<LinearLayout>(R.id.moreLayout)
val moreText = findViewById<TextView>(R.id.moreText)
Comment on lines +95 to +96
Copy link

Choose a reason for hiding this comment

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

메소드를 실행시킬때마다 비싼 xml 파싱을 통해 뷰를 탐색해내는 findViewBy 를 실행하는 것이 아닌,
액티비티가 열렸을때 one-time만 이것들을 호출하여 필드 인스턴스를 할당하고 그것을 계속 재사용하는게 성능측면에서 더 좋아보입니다.

Copy link
Author

Choose a reason for hiding this comment

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

b40e90a
따로 빼서 한 번만 호출 후 재사용하는 식으로 수정했습니다!


if(moreLayout.visibility == LinearLayout.GONE){
moreLayout.visibility = LinearLayout.VISIBLE
moreText.visibility = TextView.GONE
}
else {
moreLayout.visibility = LinearLayout.GONE
moreText.visibility = TextView.VISIBLE
}
Comment on lines +98 to +105
Copy link

Choose a reason for hiding this comment

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

Activity에서 이런 처리에 대한 책임이 있는것이 문제가 있는건 아닙니다.
걱정하지 말고 이런 로직을 넣으셔도 된다고 생각합니다.

Copy link
Author

Choose a reason for hiding this comment

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

아하 넵, 알겠습니다! 감사합니다!

}

private fun showDatePicker() {
val calendar = Calendar.getInstance()
val datePicker = DatePickerDialog(
this,
{ _, year, month, day ->
val selectedDate = Calendar.getInstance()
selectedDate.set(year, month, day)
val setting = SimpleDateFormat("yyyy.MM.dd", Locale.getDefault())
val formDate = setting.format(selectedDate.time)
findViewById<TextView>(R.id.birthText).text = formDate
Copy link

Choose a reason for hiding this comment

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

런타임 성능에 영향을 줄수있습니다.

Copy link
Author

Choose a reason for hiding this comment

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

e9cdf63
이전 커밋에서 findViewById를 한번만 호출하도록 수정해서 birthText부분도 런타임성능에 영향을 주지 않도록 변경했습니다. 커밋을 두 번 나누어 진행했습니다!

},
calendar.get(Calendar.YEAR),
calendar.get(Calendar.MONTH),
calendar.get(Calendar.DAY_OF_MONTH)
)
datePicker.show()
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
package campus.tech.kakao.contacts

import android.os.Bundle
import androidx.appcompat.app.AppCompatActivity
import android.widget.ImageView
import android.widget.TextView

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

//view 초기화 보는 거
val profileView = findViewById<ImageView>(R.id.profileView)
val nameView = findViewById<TextView>(R.id.nameView)
val phoneView = findViewById<TextView>(R.id.phoneView)
val emailView = findViewById<TextView>(R.id.emailView)
val birthView = findViewById<TextView>(R.id.birthView)
val genderView = findViewById<TextView>(R.id.genderView)
val memoView = findViewById<TextView>(R.id.memoView)

val nameLabelText = findViewById<TextView>(R.id.nameLabelText)
val phoneLabelText = findViewById<TextView>(R.id.phoneLabelText)
val emailLabelText = findViewById<TextView>(R.id.emailLabelText)
val birthLabelText = findViewById<TextView>(R.id.birthLabelText)
val genderLabelText = findViewById<TextView>(R.id.genderLabelText)
val memoLabelText = findViewById<TextView>(R.id.memoLabelText)

//parcel에 적어서 intent로 전달된 contact 객체 가져오기
val contact = intent.getParcelableExtra<Contact>("contact")
Copy link

Choose a reason for hiding this comment

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

Parcelable을 써보는것도 좋지만, 여러 객체중 측정 객체를 식별해낼수있는 key 개념만을 전달하고 받아서 동일한 기능을 처리하는 방식으로 로직을 구성해보면 좋겠습니다.

Copy link
Author

Choose a reason for hiding this comment

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

구현 시에는 하나에만 꽂혀서 해당 로직 방법을 생각해보지 못했었습니다. 다음엔 해당 로직처럼 구성해보겠습니다. 감사합니다!


//연락처 정보 설정하기 - 조건
contact?.let {

//이름 필수조건
nameView.text = it.name
nameView.visibility = TextView.VISIBLE
nameLabelText.visibility = TextView.VISIBLE

//이메일 필수조건
phoneView.text = it.phone
phoneView.visibility = TextView.VISIBLE
phoneLabelText.visibility = TextView.VISIBLE

//email
if (!it.email.isNullOrEmpty()) {
emailView.text = it.email
emailView.visibility = TextView.VISIBLE
emailLabelText.visibility = TextView.VISIBLE
} else {
emailView.visibility = TextView.GONE
emailLabelText.visibility = TextView.GONE
}

//birth
if (!it.birth.isNullOrEmpty()) {
birthView.text = it.birth
birthView.visibility = TextView.VISIBLE
birthLabelText.visibility = TextView.VISIBLE
} else {
birthView.visibility = TextView.GONE
birthLabelText.visibility = TextView.GONE
}

//gender
//default 값이 아닌 경우 값을 집어 넣도록 설정 - '성별' 문구 안뜨도록
if (!it.gender.isNullOrEmpty() && it.gender != " ") {
genderView.text = it.gender
genderView.visibility = TextView.VISIBLE
genderLabelText.visibility = TextView.VISIBLE
} else {
genderView.visibility = TextView.GONE
genderLabelText.visibility = TextView.GONE
}

//memo
if (!it.memo.isNullOrEmpty()) {
memoView.text = it.memo
memoView.visibility = TextView.VISIBLE
memoLabelText.visibility = TextView.VISIBLE
} else {
memoView.visibility = TextView.GONE
memoLabelText.visibility = TextView.GONE
}
}
}
}
Loading