안녕하세요!. 열심히 개발한 티가 나네요!
ViewModel, Repository, 광고, SNS, Alarm, EventBus 등 지금까지 배운 것을 열심히 녹여내었어요!
잘한 것들은 이미 발표회에서 칭찬을 많이 들었으니 개선점들 많이 많이 선물해 드릴게요!
기능적으로 너무 화려하지만 공들인 만큼 UX가 예쁘게 나오진 않은 것 같긴합니다. 패션의 완성은 얼굴이라는 우스갯 소리가 있는 것처럼 앱도 UX도 중요하니 다음에는 더 깜찍한 UX 기대해 볼게요!
기술적으로 배운 내용을 녹여내려고 고민 많이 한 흔적이 보여서 멋지구요. 아직 완벽하진 않기 때문에 애매한 부분은 튜터룸에 와서 자주 물어봐 주세요! 앞으로 심화 과정과 최종 프로젝트를 하면서 얼마나 더 성장할지 기대가 됩니다.
아직은 화면마다 담당자가 다른 것인지 각자의 코드가 구현되어 있지만,
앞으로는 숲을 보며 구조 설계를 먼저 하고 개발을 진행하면 코드 구조 통일에 도움이 될 것 같아요!
전반적으로 Item이 없는 경우에 대한 NoItems(contact이나 callLog가 없는 경우 등) 처리가 안되어 있네요.
이 부분도 처리가 되면 좀 더 사용성이 높아질 것이예요.
전반적으로 라인 정리가 안되어 있네요. Ctrl(Mac: Cmd) + Alt + L 라인정리를 생활화하세요!
파일이 많아져서 기능별로 package를 나눠서 관리한 점 잘 했어요. 심화과제부터는 MVVM 패턴이나, 클린 아키텍쳐에 대해 배웠으니 data/presentation layer로 분리해서도 구성해 보세요.
코드양이 많아서 모든 것을 피드백 드리진 못했으니 궁금한 것은 언제나 튜터룸으로!!
너무 수고하셨습니다. 엄청난 발전에 박수를!!!
// TextView 확장 함수 정의
fun TextView.setWarning(message: String, color: Int = Color.RED) {
text = message
setTextColor(color)
}
fun TextView.clearWarning() {
text = ""
}
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/alarm/ViewModel/AlarmViewModel.kt#L19
- viewModel에서는 viewModelScope 사용하면 라이프사이클에 따라 동작할수 있어요.
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/alarm/ViewModel/AlarmViewModel.kt#L8
- interface가 있었었군요! 삭제한 이유가 있을까요?
- 알람 동작을 위해 Room도 활용했군요! 열심히 준비한 모습 멋지네요!
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/alarm/AlarmReceiver.kt#L31
- 이 조건은 minSDK가 28이라 필요없겠네요)
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/alarm/RebootAlarmReceiver.kt#L14
- reboot까지 신경쓰다니! 내공이 보이네요!
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/data/ViewModel/ContactViewModel.kt#L15
- 구현체가 아닌 Interface로 인자를 받으시는게 구현체에 대한 의존성을 제거하는데 좋습니다. (Repository Pattern 참고)
`class ContactViewModel(private val contactRepository: ContactRepository)`
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/data/ViewModel/ContactViewModel.kt#L17-L18
- LiveData나 Flow로 구현해주셔야 실시간 observe가 가능해집니다!
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/data/ViewModel/ContactViewModel.kt#L29
- viewModel에 직접 adapter를 넘기지 않고 View와 ViewModel을 끊어내야 합니다. LiveData로 list data 들을 관리해서 값 변경시 Adapter가 실시간 모니터링 하도록 구조 개선해서 MVVM 패턴의 장점을 살려보세요.
private val _contacts = MutableLiveData<List<ContactEntity>>()
val contacts: LiveData<List<ContactEntity>> get() = _contacts
private val _callLogs = MutableLiveData<List<CallLogEntity>>()
val callLogs: LiveData<List<CallLogEntity>> get() = _callLogs
private fun observeContacts() {
viewModelScope.launch {
contactRepository.getContactList().collectLatest { contactList ->
_contacts.postValue(contactList)
}
}
}
private fun observeCallLogs() {
viewModelScope.launch {
contactRepository.getCallLogs().collectLatest { callLogList ->
_callLogs.postValue(callLogList)
}
}
}
class MainActivity : AppCompatActivity() {
private val contactViewModel: ContactViewModel by viewModels()
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/data/CallLogAdapter.kt#L8
- MVVM 패턴이 적용되면 ListAdapter가 더 사용이 편리합니다. 다음 프로젝트에선 ListAdapter도 활용해 보세요!
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/data/contactMethods.kt#L23
- Contact/callLog 모두 잘 읽어왔네요! use나 generateSequence 등을 활용하면 좀 더 가독성을 높일 수 있어요.
fun Context.contactList(): List {
val projection = arrayOf(
ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.CommonDataKinds.Photo.PHOTO_URI,
ContactsContract.Contacts.STARRED
)
return contentResolver.query(
ContactsContract.CommonDataKinds.Phone.CONTENT_URI,
projection,
null,
null,
null
)?.use { cursor ->
generateSequence { if (cursor.moveToNext()) cursor else null }
.map {
val name = it.getStringOrNull(cursor.getColumnIndexOrThrow(projection[1])) ?: ""
val number = it.getStringOrNull(cursor.getColumnIndexOrThrow(projection[2]))?.replace("-", "") ?: ""
val photoUri = it.getStringOrNull(cursor.getColumnIndexOrThrow(projection[3]))?.toUri()
val starred = it.getInt(cursor.getColumnIndexOrThrow(projection[4]))
ContactEntity(name, convertString(name), number, starred, photoUri, null, null)
}
.toList()
.sortedWith(compareByDescending<ContactEntity> { it.tag }.thenBy { it.name })
} ?: emptyList()
}
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/List/ListAdapter.kt#L56-L57
- 한줄로 작성되지 않는 if else 문은 괄호 생략하지 않아요!
[](https://developer.android.com/kotlin/style-guide?hl=ko#braces)
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/List/ListFragment.kt#L59
- 어떤 Fragment인지 정확히 알수 있도록 ContactListFragment로 개명! 누구나 이름만 보면 역할을 알수있도록 이름을 지어주세요!
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/List/ListFragment.kt#L71-L73
- 특별한 동작이 추가되지 않았으면 생략가능해요!
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/List/ListFragment.kt#L89
- 외부에서 사용하지 않으니 private
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/mypage/data/data/MyPageData.kt#L10
- object로 구현했으면 MyPageDataObj가 없어도 됬겠어요!
총평
안녕하세요!. 열심히 개발한 티가 나네요! ViewModel, Repository, 광고, SNS, Alarm, EventBus 등 지금까지 배운 것을 열심히 녹여내었어요! 잘한 것들은 이미 발표회에서 칭찬을 많이 들었으니 개선점들 많이 많이 선물해 드릴게요! 기능적으로 너무 화려하지만 공들인 만큼 UX가 예쁘게 나오진 않은 것 같긴합니다. 패션의 완성은 얼굴이라는 우스갯 소리가 있는 것처럼 앱도 UX도 중요하니 다음에는 더 깜찍한 UX 기대해 볼게요! 기술적으로 배운 내용을 녹여내려고 고민 많이 한 흔적이 보여서 멋지구요. 아직 완벽하진 않기 때문에 애매한 부분은 튜터룸에 와서 자주 물어봐 주세요! 앞으로 심화 과정과 최종 프로젝트를 하면서 얼마나 더 성장할지 기대가 됩니다. 아직은 화면마다 담당자가 다른 것인지 각자의 코드가 구현되어 있지만, 앞으로는 숲을 보며 구조 설계를 먼저 하고 개발을 진행하면 코드 구조 통일에 도움이 될 것 같아요! 전반적으로 Item이 없는 경우에 대한 NoItems(contact이나 callLog가 없는 경우 등) 처리가 안되어 있네요. 이 부분도 처리가 되면 좀 더 사용성이 높아질 것이예요. 전반적으로 라인 정리가 안되어 있네요. Ctrl(Mac: Cmd) + Alt + L 라인정리를 생활화하세요! 파일이 많아져서 기능별로 package를 나눠서 관리한 점 잘 했어요. 심화과제부터는 MVVM 패턴이나, 클린 아키텍쳐에 대해 배웠으니 data/presentation layer로 분리해서도 구성해 보세요. 코드양이 많아서 모든 것을 피드백 드리진 못했으니 궁금한 것은 언제나 튜터룸으로!! 너무 수고하셨습니다. 엄청난 발전에 박수를!!!
코드 피드백
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/AndroidManifest.xml#L47-L49
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/AddContact/AddFragment.kt#L48-L52
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/AddContact/AddFragment.kt#L70
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/AddContact/AddFragment.kt#L81
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/AddContact/AddFragment.kt#L109-L113
val likeStatus = if (binding.addTbtnLike.isChecked) 1 else 0
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/AddContact/AddFragment.kt#L123
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/AddContact/AddFragment.kt#L137-L145
https://github.com/maecbanseok/BakedEggs/blob/2f5b57f775e636ea426a2ea6c2d02cddeaab40b3/app/src/main/java/com/example/bakedeggs/AddContact/AddFragment.kt#L156
fun EditText.isValidPhone(warningView: TextView, pattern: Regex): Boolean { val phone = text.toString().trim() return when { phone.isEmpty() -> { warningView.setWarning("번호를 입력해 주세요") false } !phone.matches(pattern) -> { warningView.setWarning("번호는 9~11자 입니다.", Color.MAGENTA) false } else -> { warningView.clearWarning() true } } }
fun EditText.isValidEmail(warningView: TextView, pattern: Regex): Boolean { val email = text.toString().trim() return when { email.isEmpty() -> { warningView.setWarning("이메일을 입력해 주세요") false } !email.matches(pattern) -> { warningView.setWarning("입력한 이메일을 확인해 주세요", Color.MAGENTA) false } else -> { warningView.clearWarning() true } } }
// TextView 확장 함수 정의 fun TextView.setWarning(message: String, color: Int = Color.RED) { text = message setTextColor(color) }
fun TextView.clearWarning() { text = "" }
class MainActivity : AppCompatActivity() {
.. contactAdapter = ContactAdapter() recyclerViewContacts.layoutManager = LinearLayoutManager(this) recyclerViewContacts.adapter = contactAdapter contactViewModel.contacts.observe(this, Observer { contacts -> contactAdapter.submitList(contacts) })
fun Context.contactList(): List {
val projection = arrayOf(
ContactsContract.CommonDataKinds.Phone.CONTACT_ID,
ContactsContract.CommonDataKinds.Phone.DISPLAY_NAME,
ContactsContract.CommonDataKinds.Phone.NUMBER,
ContactsContract.CommonDataKinds.Photo.PHOTO_URI,
ContactsContract.Contacts.STARRED
)
}