과제의 목표
- 공통
- EditText를 실시간으로 입력 받아 유효성을 체크한다.
- EditText Focus out 할 때 입력하지 않았을 경우 에러 텍스트를 노출한다.
- 이메일
- EditText Focus 할 때 이메일을 입력하지 않았을 경우 에러 텍스트를 노출한다.
- 서비스 제공사를 Spinner로 변경, 직접 입력일 경우 EditText를 노출한다.
- 비밀번호
- 비밀번호 강도 체크하여 영문과 숫자를 포함하지 않고 길이가 10자 미만일 경우 에러 텍스트를 노출한다.
- 회원가입 버튼
- 모든 항목의 유효성 체크를 완료했을 때 버튼 활성화, 그러지 못할 경우 비활성화 한다.
class ValidationActivity : AppCompatActivity() {
private val etName: EditText by lazy {
findViewById(R.id.et_name)
}
private val etEmail: EditText by lazy {
findViewById(R.id.et_email)
}
private val etEmailServiceProvider: EditText by lazy {
findViewById(R.id.et_email_service_provider)
}
private val etPwd: EditText by lazy {
findViewById(R.id.et_pwd)
}
private val etPwdCheck: EditText by lazy {
findViewById(R.id.et_pwd_check)
}
private val spServiceProvider: Spinner by lazy {
findViewById(R.id.sp_service_provider)
}
private val tvNameMessage: TextView by lazy {
findViewById(R.id.tv_name_message)
}
private val tvEmailMessage: TextView by lazy {
findViewById(R.id.tv_email_message)
}
private val tvPwdMessage: TextView by lazy {
findViewById(R.id.tv_pwd_message)
}
private val tvPwdCheckMessage: TextView by lazy {
findViewById(R.id.tv_pwd_check_message)
}
private val btnSignUp: Button by lazy {
findViewById(R.id.btn_signUp)
}
private val editTexts
get() = listOf(
etName,
etEmail,
etEmailServiceProvider,
etPwd,
etPwdCheck
)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_validation)
initView()
}
private fun setSpinnerServiceProvider() {
/*
스피너에 어댑터 등록한다.
R.array.emailList : strings.xml 내의 emailList 아이템 목록을 추가한다.
*/
spServiceProvider.adapter = ArrayAdapter.createFromResource(
this,
R.array.emailList,
android.R.layout.simple_spinner_item
)
val lastIndex = spServiceProvider.adapter.count - 1 // 스피너의 마지막 요소
/*
onItemSelectedListener가 최초에 실행되는 문제를 방지하고자 Listener 이전에 setselection(position, false)을 해준다.
마지막 요소인 "직접 입력"으로 초기화
*/
spServiceProvider.setSelection(lastIndex, false)
// 사용자가 선택한 값을 알기 위해 Listener를 추가한다.
spServiceProvider.onItemSelectedListener = object : AdapterView.OnItemSelectedListener {
override fun onItemSelected(
parent: AdapterView<*>?,
view: View?,
position: Int,
id: Long
) {
if (position == lastIndex) { // "직접 입력" 일 경우
// EditText가 활성화 된다.
etEmailServiceProvider.isEnabled = true
etEmailServiceProvider.setText("")
} else {
/*
EditText가 비활성화 되고 선택한 값을 출력해준다.
gmail.com 선택시 @gmail.com 출력
*/
etEmailServiceProvider.isEnabled = false
val text = "@${spServiceProvider.selectedItem}"
etEmailServiceProvider.setText(text)
}
}
override fun onNothingSelected(p0: AdapterView<*>?) = Unit
}
}
private fun initView() {
setTextChangeListener()
setOnFocusChangedListener()
setSpinnerServiceProvider()
btnSignUp.setOnClickListener {
// TODO 회원가입 버튼 클릭시
}
}
private fun setTextChangeListener() { // addTextChangedListener
editTexts.forEach { editText ->
editText.addTextChangedListener(EditTextWatcher(
onChanged = { _, _, _, _ ->
editText.setErrorMessage() // EditText 유효성 체크 후 에러 텍스트 노출
setButtonEnable() // 유효성 체크에 따라 버튼 활성화 상태 변경
}
))
}
}
private fun setOnFocusChangedListener() { // setOnFocusChangeListener
editTexts.forEach { editText ->
editText.setOnFocusChangeListener { _, hasFocus ->
if (!hasFocus) {
editText.setErrorMessage() // EditText 유효성 체크 후 에러 텍스트 노출
setButtonEnable() // 유효성 체크에 따라 버튼 활성화 상태 변경
}
}
}
}
// EditText별 유효성 체크 후 에러 텍스트 노출 및 Visibility 상태 변경
private fun EditText.setErrorMessage() {
val message = when (this) {
etName -> getMessageValidName()
etEmail -> getMessageValidEmail()
etEmailServiceProvider -> getMessageValidEmailRegex()
etPwd -> getMessageValidPassword()
etPwdCheck -> getConfirmPassword()
else -> return
}
val textView = when (this) {
etName -> tvNameMessage
etEmail, etEmailServiceProvider -> tvEmailMessage
etPwd -> tvPwdMessage
etPwdCheck -> tvPwdCheckMessage
else -> return
}
textView.text = message
textView.setVisibility()
}
/*
TextView가 isBlank()가 아닐 경우 에러 메시지가 노출 된 상태기에 TextView를 VISIBLE
isBlank()일 경우는 유효성 검사에 성공한 상태기에 GONE 처리 해준다.
*/
private fun TextView.setVisibility() {
visibility = if (text.isBlank()) GONE else VISIBLE
}
private fun getMessageForInput(editText: EditText, message: String): String =
if (editText.text.isBlank()) { // EditText에 입력 된 값이 없을 경우
message
} else {
""
}
// 이름 유효성 체크
private fun getMessageValidName(): String =
getMessageForInput(etName, getString(R.string.text_input_name))
// 이메일 유효성 체크
private fun getMessageValidEmail(): String =
getMessageForInput(etEmail, getString(R.string.text_input_email))
// 이메일 정규 표현식 체크
private fun getMessageValidEmailRegex(): String {
val text = etEmailServiceProvider.text.toString()
val email = etEmail.text.toString() + text
return when {
text.isBlank() -> getString(R.string.text_input_service_provider)
!Patterns.EMAIL_ADDRESS.matcher(email).matches() -> getString(R.string.text_check_email)
else -> ""
}
}
// 비밀번호 유효성 체크
private fun getMessageValidPassword(): String {
val text = etPwd.text.toString()
val pattern = "^(?=.*[A-Za-z])(?=.*[0-9])[A-Za-z[0-9]]{10,}\$"
return when {
text.length < 10 -> getString(R.string.text_check_pwd_length)
!Pattern.matches(pattern, text) -> getString(R.string.text_check_pwd_regex)
else -> ""
}
}
// 비밀번호가 일치하는지 체크
private fun getConfirmPassword(): String =
if (etPwd.text.toString() != etPwdCheck.text.toString()) {
getString(R.string.text_pwd_mismach)
} else {
""
}
// 버튼 활성화 상태 변경을 위한 함수
private fun setButtonEnable() {
btnSignUp.isEnabled = getMessageValidName().isBlank()
&& getMessageValidEmail().isBlank()
&& getMessageValidEmailRegex().isBlank()
&& getMessageValidPassword().isBlank()
&& getConfirmPassword().isBlank()
}
}
/*
TextWatcher를 공통 클래스로 만들어 사용한다.
원하는 함수만 사용할 수 있게 하기 위해 nullable한 람다 함수를 매개 변수로 받도록 한다.
*/
class EditTextWatcher(
private val afterChanged: ((Editable?) -> Unit) = {},
private val beforeChanged: ((CharSequence?, Int, Int, Int) -> Unit) = { _, _, _, _ -> },
private val onChanged: ((CharSequence?, Int, Int, Int) -> Unit) = { _, _, _, _ -> }
) : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {
beforeChanged(s, start, count, after)
}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {
onChanged(s, start, before, count)
}
override fun afterTextChanged(s: Editable?) {
afterChanged(s)
}
}
EditText
의 inputType
을 textPassword
로 지정 했으며 입력된 문자는 *** 로 표시된다.<?xml version="1.0" encoding="utf-8"?>
<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=".ValidationActivity">
<TextView
android:id="@+id/tv_name"
style="@style/ThemeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/text_name"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<EditText
android:id="@+id/et_name"
style="@style/ThemeEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_name" />
<TextView
android:id="@+id/tv_name_message"
style="@style/ThemeValidTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_name" />
<TextView
android:id="@+id/tv_email"
style="@style/ThemeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/text_email"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_name_message" />
<EditText
android:id="@+id/et_email"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:layout_marginStart="32dp"
android:textSize="14sp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_email" />
<EditText
android:id="@+id/et_email_service_provider"
android:layout_width="100dp"
android:layout_height="wrap_content"
android:textSize="14sp"
app:layout_constraintBottom_toBottomOf="@+id/et_email"
app:layout_constraintStart_toEndOf="@id/et_email"
app:layout_constraintTop_toBottomOf="@id/tv_email"
app:layout_constraintTop_toTopOf="@id/et_email" />
<Spinner
android:id="@+id/sp_service_provider"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="32dp"
app:layout_constraintBottom_toBottomOf="@id/et_email_service_provider"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toEndOf="@id/et_email_service_provider"
app:layout_constraintTop_toTopOf="@id/et_email_service_provider" />
<TextView
android:id="@+id/tv_email_message"
style="@style/ThemeValidTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_email" />
<TextView
android:id="@+id/tv_pwd"
style="@style/ThemeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/text_pwd"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_email" />
<EditText
android:id="@+id/et_pwd"
style="@style/ThemeEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:hint="@string/text_pwd_condition"
android:inputType="textPassword"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_pwd" />
<TextView
android:id="@+id/tv_pwd_message"
style="@style/ThemeValidTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_pwd" />
<TextView
android:id="@+id/tv_pwd_check"
style="@style/ThemeTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/text_pwd_check"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_pwd" />
<EditText
android:id="@+id/et_pwd_check"
style="@style/ThemeEditText"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:inputType="textPassword"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_pwd_check" />
<TextView
android:id="@+id/tv_pwd_check_message"
style="@style/ThemeValidTextView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/et_pwd_check" />
<androidx.appcompat.widget.AppCompatButton
android:id="@+id/btn_signUp"
style="@style/ThemeButton"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:background="@drawable/btn_pressed_selector"
android:enabled="false"
android:text="@string/text_sign_up"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/tv_pwd_check_message" />
</androidx.constraintlayout.widget.ConstraintLayout>
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<solid android:color="@color/light_purple" />
<stroke android:width="2dp" android:color="@color/purple" />
<corners android:radius="10dp" />
</shape>
</item>
<item android:state_pressed="false">
<shape android:shape="rectangle">
<solid android:color="#00FFFFFF" />
<stroke android:width="2dp" android:color="@color/purple" />
<corners android:radius="10dp" />
</shape>
</item>
</selector>
<resources xmlns:tools="http://schemas.android.com/tools">
<!-- Base application theme. -->
<style name="Base.Theme.SelfIntroduction" parent="Theme.Material3.DayNight.NoActionBar">
<!-- Customize your light theme here. -->
<!-- <item name="colorPrimary">@color/my_light_primary</item> -->
</style>
<style name="Theme.SelfIntroduction" parent="Base.Theme.SelfIntroduction" />
<style name="ThemeEditText" parent="Widget.AppCompat.EditText">
<item name="android:textColor">@color/black</item>
<item name="android:textSize">16sp</item>
<item name="android:layout_marginStart">32dp</item>
<item name="android:layout_marginEnd">32dp</item>
</style>
<style name="ThemeTextView" parent="Widget.AppCompat.TextView">
<item name="android:textColor">@color/black</item>
<item name="android:textSize">16sp</item>
<item name="android:textStyle">bold</item>
<item name="android:layout_marginStart">32dp</item>
<item name="android:layout_marginEnd">32dp</item>
<item name="android:layout_marginTop">16dp</item>
</style>
<style name="ThemeValidTextView" parent="Widget.AppCompat.TextView">
<item name="android:textColor">@color/red</item>
<item name="android:textSize">13sp</item>
<item name="android:layout_marginStart">32dp</item>
<item name="android:layout_marginEnd">32dp</item>
</style>
<style name="ThemeButton" parent="Widget.AppCompat.Button">
<item name="android:textStyle">bold</item>
<item name="android:layout_marginStart">32dp</item>
<item name="android:layout_marginEnd">32dp</item>
<item name="android:layout_marginBottom">32dp</item>
<item name="android:textColor">@color/black</item>
<item name="android:layout_marginTop">16dp</item>
</style>
</resources>
<resources>
<string name="text_input_name">이름을 입력하세요.</string>
<string name="text_input_email">이메일을 입력해주세요.</string>
<string name="text_input_service_provider">서비스 제공사를 입력해주세요.</string>
<string name="text_check_email">올바른 이메일 주소를 입력해주세요.</string>
<string name="text_check_pwd_length">10자 이상 입력해주세요.</string>
<string name="text_check_pwd_regex">영문과 숫자를 포함해주세요.</string>
<string name="text_pwd_mismach">비밀번호가 일치하지 않습니다.</string>
<string name="text_pwd_condition">영문, 숫자, 10자 이상</string>
<string name="text_name">이름</string>
<string name="text_email">이메일</string>
<string name="text_pwd">비밀번호</string>
<string name="text_pwd_check">비밀번호 확인</string>
<string name="text_sign_up">회원가입</string>
<string-array name="emailList">
<item>gmail.com</item>
<item>kakao.com</item>
<item>naver.com</item>
<item>직접 입력</item>
</string-array>
</resources>
내일배움캠프를 수강하게 된 이유 중 하나가 내 입맛대로 작성하던 코드를 개선하고 코드를 효율적으로 작성하는 방법을 배우고 싶은 마음에 수강하게 되었는데, 수준별 학습을 들으며 수강하길 정말 잘했다..! 고 생각했다. 🥹 튜터님의 실시간 코딩을 보며 코드는 이렇게 작성하는거구나.. 이렇게도 작성할 수 있구나 ! 하고 감탄 했고 내 코드는 중복 되는 부분이 많으니 이런 부분을 우선적으로 수정하여 재사용성을 높여야겠다고 느꼈다. 나름 중복 되는 부분을 함수로 만들어 사용했다고 생각했는데 더 개선할 수 있도록 많이 배워야겠다. 앞으로 새로 배우게 될 내용들이 너무나 기대된다 🤭