ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • [Android] MVVM 패턴 회원가입 유효성 검사
    안드로이드 2024. 4. 9. 20:58

    ✏️ TIL(Today I Learned)

    기존에 만들어둔 회원가입 유효성을 확인하는 프로젝트가 모든 로직을 뷰모델에만 의존하는 것 같아서 분리해 봤다.

     

    [Android] 회원가입시 LiveData 사용한 비밀번호 유효성 검사 (ViewModel/ Pattern, Matcher 정규식)

    ✏️ TIL(Today I Learned) 회원가입시, 비밀번호의 조건을 만족할 때까지 아래에 주의문구가 보이도록 만들었다. LiveData를 사용하여 입력받은 text를 관찰하면서, 조건을 만족하면 주의문구의 visibility

    muk-clouds.tistory.com

     

    우선 SignUpMember 데이터 클래스를 만들어서 회원가입 시 사용되던 변수들을 하나로 묶었다.

    data class SignUpMember(
        val name: String,
        val id: String,
        val password: String,
    )

     

     

    사용자로부터 입력받은 값의 상태를 관리하기 위한

    SignUpErrorUiState  데이터 클래스와 SignUpValidUiState 인터페이스를 정의했다.

     

    SignUpErrorUiState 데이터 클래스에서 name, emailId, passwordInput, passwordConfirm 필드는 각각 이름, 이메일(아이디), 비밀번호, 비밀번호 확인의 SignUpValidUiState을 나타내고 enabled는 회원가입 버튼이 활성화되는지 여부를 나타낸다.

     

    sealed 키워드를 사용함으로써, 해당 인터페이스의 하위 클래스들이 같은 파일 내에서만 정의되고 제한되도록 했다.

    data class SignUpErrorUiState(
        val name: SignUpValidUiState,
        val emailId: SignUpValidUiState,
        val passwordInput: SignUpValidUiState,
        val passwordConfirm: SignUpValidUiState,
        val enabled: Boolean,
    ) {
        companion object {
            fun init() = SignUpErrorUiState(
                name = SignUpValidUiState.Init,
                emailId = SignUpValidUiState.Init,
                passwordInput = SignUpValidUiState.Init,
                passwordConfirm = SignUpValidUiState.Init,
                enabled = false
            )
        }
    }
    
    sealed interface SignUpValidUiState {
        // 초기 상태
        object Init : SignUpValidUiState
    
        // 통과
        object Valid : SignUpValidUiState
    
        // 이름
        object Name : SignUpValidUiState
    
        // 이메일
        object EmailBlank : SignUpValidUiState
        object Emailvalid : SignUpValidUiState
    
        // 비밀번호
        object PasswordInputLength : SignUpValidUiState
        object PasswordInputSpecialCharacters : SignUpValidUiState
        object PasswordInputUpperCase : SignUpValidUiState
    
        // 비밀번호 확인
        object PasswordConfirm : SignUpValidUiState
    }

     

     

    뷰모델에서는 errorUiState, event 라이브 데이터를 만들어서 뷰(엑티비티)에서 관찰하도록 했다.

    이때, copy()를 통해 데이터 클래스의 인스턴스를 복사하여 일부 속성을 변경하고 그 변경된 내용을 가진 새로운 인스턴스를 생성했다. 이러면, 기존의 데이터를 변경하지 않고 새로운 데이터를 생성할 수 있다.

    class SignUpViewModel : ViewModel() {
        private val _errorUiState: MutableLiveData<SignUpErrorUiState> =
            MutableLiveData(SignUpErrorUiState.init())
        val errorUiState: LiveData<SignUpErrorUiState>
            get() = _errorUiState
    
        private val _event: MutableLiveData<SignUpEvent> = MutableLiveData()
        val event: LiveData<SignUpEvent>
            get() = _event
    
        fun checkValidName(text: String) {
            _errorUiState.value = errorUiState.value?.copy(
                name = if (text.isBlank()) {
                    SignUpValidUiState.Name
                } else {
                    SignUpValidUiState.Valid
                }
            )
        }
    
        fun checkValidEmail(text: String) {
            _errorUiState.value = errorUiState.value?.copy(
                emailId = when {
                    text.isBlank() -> SignUpValidUiState.EmailBlank
                    text.validEmail().not() -> SignUpValidUiState.Emailvalid
    
                    else -> SignUpValidUiState.Valid
                }
            )
        }
    
        fun checkValidPasswordInput(text: String) {
            _errorUiState.value = errorUiState.value?.copy(
                passwordInput = when {
                    text.length < 8 || text.length > 15 -> SignUpValidUiState.PasswordInputLength
                    text.includeSpecialCharacters().not() -> SignUpValidUiState.PasswordInputSpecialCharacters
                    text.includeUpperCase().not() -> SignUpValidUiState.PasswordInputUpperCase
    
                    else -> SignUpValidUiState.Valid
                }
            )
        }
    
        fun checkValidPasswordConfirm(
            text: String,
            confirm: String
        ) {
            _errorUiState.value = errorUiState.value?.copy(
                passwordConfirm = if (text != confirm) {
                    SignUpValidUiState.PasswordConfirm
                } else {
                    SignUpValidUiState.Valid
                }
            )
        }
    
        fun isConfirmButtonEnable() {
            val currentState = errorUiState.value ?: return
            val isEnabled = currentState.name == SignUpValidUiState.Valid &&
                    currentState.emailId == SignUpValidUiState.Valid &&
                    currentState.passwordInput == SignUpValidUiState.Valid &&
                    currentState.passwordConfirm == SignUpValidUiState.Valid
    
            _errorUiState.value = currentState.copy(enabled = isEnabled)
        }
    
        fun onClick(member: SignUpMember) {
            _event.value = SignUpEvent.SignUpSuccess(member)
        }
    }

     

     

    입력된 값의 유효성을 확인하는 정규식은 따로 오브젝트로 만들었다.

    object SignUpValidExtension {
        //이메일 정규 표현식
        fun String.validEmail() =
            Regex("^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,6}\$").matches(this)
    
        //특수 문자 포함
        fun String.includeSpecialCharacters() =
            Regex("[!@#\$%^&*()_+\\-=\\[\\]{};':\"\\\\|,.<>\\/?]+").containsMatchIn(this)
    
        //대문자 포함
        fun String.includeUpperCase() = Regex("[A-Z]").containsMatchIn(this)
    }

     

     

    마지막으로 뷰에서 입력된 값을 관찰하여, SignUpValidUiState의 상태에 따른 string이 텍스트뷰에 적용되도록 했다.

    class SignUpActivity : AppCompatActivity() {
        companion object {
            fun newIntent(
                context: Context,
            ): Intent = Intent(context, SignUpActivity()::class.java)
        }
    
        private val binding: ActivitySignUpBinding by lazy {
            ActivitySignUpBinding.inflate(layoutInflater)
        }
    
        private val viewModel: SignUpViewModel by viewModels()
    
        private val editTexts
            get() = with(binding) {
                listOf(
                    nameET,
                    emailET,
                    pwdET,
                    pwdCheckET
                )
            }
    
        override fun onCreate(savedInstanceState: Bundle?) {
            super.onCreate(savedInstanceState)
            setContentView(binding.root)
    
            initView()
            initViewModel()
        }
    
        private fun initView() {
            editTexts.forEach { editText ->
                editText.addTextChangedListener {
                    editText.checkValidElements()
                    viewModel.isConfirmButtonEnable()
                }
    
                editText.setOnFocusChangeListener { _, hasFocus ->
                    if (hasFocus.not()) {
                        editText.checkValidElements()
                        //viewModel.isConfirmButtonEnable()
                    }
                }
            }
            binding.signupBtn.setOnClickListener(){
                val name = binding.nameET.text.toString()
                val id = binding.emailET.text.toString()
                val password = binding.pwdET.text.toString()
    
                val member = SignUpMember(
                    name,
                    id,
                    password
                )
                viewModel.onClick(member)
                val intent = Intent(this@SignUpActivity, SignInActivity::class.java)
                intent.putExtra("id", id)
                intent.putExtra("pwd", password)
                setResult(RESULT_OK, intent)
                if (!isFinishing) finish()
            }
        }
    
        private fun initViewModel() = with(viewModel) {
    
            errorUiState.observe(this@SignUpActivity) { uiState ->
                with(binding) {
                    // 이름
                    nameWarningTV.visibility = when (uiState.name) {
                        SignUpValidUiState.Name -> View.VISIBLE
                        else -> View.GONE
                    }
    
                    // 이메일
                    emailWarningTV.setText(
                        when (uiState.emailId) {
                            SignUpValidUiState.EmailBlank -> R.string.sign_up_email_error_blank
                            SignUpValidUiState.Emailvalid -> R.string.sign_up_email_error
                            else -> R.string.sign_up_pass
                        }
                    )
                    emailWarningTV.visibility = when (uiState.emailId) {
                        SignUpValidUiState.EmailBlank -> View.VISIBLE
                        SignUpValidUiState.Emailvalid -> View.VISIBLE
                        else -> View.GONE
                    }
    
                    // 비밀번호
                    pwdWarningTV.setText(
                        when (uiState.passwordInput) {
                            SignUpValidUiState.PasswordInputLength -> R.string.sign_up_password_error_length
                            SignUpValidUiState.PasswordInputSpecialCharacters -> R.string.sign_up_password_error_special
                            SignUpValidUiState.PasswordInputUpperCase -> R.string.sign_up_password_error_upper
                            else -> R.string.sign_up_pass
                        }
                    )
                    pwdWarningTV.visibility = when (uiState.passwordInput) {
                        SignUpValidUiState.PasswordInputLength -> View.VISIBLE
                        SignUpValidUiState.PasswordInputSpecialCharacters -> View.VISIBLE
                        SignUpValidUiState.PasswordInputUpperCase -> View.VISIBLE
                        else -> View.GONE
                    }
    
                    // 비밀번호 확인
                    pwdCheckWarningTV.visibility = when (uiState.passwordConfirm) {
                        SignUpValidUiState.PasswordConfirm -> View.VISIBLE
                        else -> View.GONE
                    }
    
                    // 버튼
                    signupBtn.isEnabled = uiState.enabled
                    Log.d("TAG", "uiState.enabled: "+uiState.enabled)
                }
            }
    
            event.observe(this@SignUpActivity) { event ->
                when (event) {
                    is SignUpEvent.SignUpSuccess -> {
                        Toast.makeText(
                            this@SignUpActivity,
                            event.member.toString(),
                            Toast.LENGTH_SHORT
                        ).show()
                    }
                }
            }
        }
    
        // 각 항목의 유효성 검사
        private fun EditText.checkValidElements() = with(binding) {
            when (this@checkValidElements) {
                nameET -> viewModel.checkValidName(nameET.text.toString())
                emailET -> viewModel.checkValidEmail(emailET.text.toString())
                pwdET -> viewModel.checkValidPasswordInput(pwdET.text.toString())
                pwdCheckET -> viewModel.checkValidPasswordConfirm(
                    pwdET.text.toString(),
                    pwdCheckET.text.toString(),
                )
    
                else -> Unit
            }
        }
    }
Designed by Tistory.