- 全部改用 ConstraintLayout- 完整 XML登录、注册、图标验证弹窗- 代码全面切换为 View Binding删除所有 kotlinx.android.synthetic- 保留你所有功能MVVM、防抖、密码可见、正则校验、MMKV、弹窗模糊、圆角按钮你只需要复制粘贴即可运行。0. 先开启 ViewBindingbuild.gradle.appgradleandroid {buildFeatures {viewBinding true}}1. 圆角按钮背景 drawable/btn_round.xmlxml?xml version1.0 encodingutf-8?shape xmlns:androidhttp://schemas.android.com/apk/res/androidandroid:shaperectanglesolid android:color#1677FF /corners android:radius24dp //shape2. 登录页面 activity_login.xml ConstraintLayoutxml?xml version1.0 encodingutf-8?androidx.constraintlayout.widget.ConstraintLayoutxmlns:androidhttp://schemas.android.com/apk/res/androidxmlns:apphttp://schemas.android.com/apk/res-autoandroid:layout_widthmatch_parentandroid:layout_heightmatch_parentandroid:paddingHorizontal24dpTextViewandroid:idid/tv_titleandroid:layout_widthwrap_contentandroid:layout_heightwrap_contentandroid:text登录android:textSize26spandroid:layout_marginTop80dpapp:layout_constraintLeft_toLeftOfparentapp:layout_constraintTop_toTopOfparent/EditTextandroid:idid/et_phoneandroid:layout_widthmatch_parentandroid:layout_height50dpandroid:hint请输入手机号android:inputTypephoneandroid:layout_marginTop40dpapp:layout_constraintTop_toBottomOfid/tv_title/androidx.constraintlayout.widget.ConstraintLayoutandroid:idid/layout_pwdandroid:layout_widthmatch_parentandroid:layout_height50dpandroid:layout_marginTop16dpapp:layout_constraintTop_toBottomOfid/et_phoneEditTextandroid:idid/et_pwdandroid:layout_widthmatch_parentandroid:layout_heightmatch_parentandroid:hint请输入密码android:inputTypetextPasswordapp:layout_constraintLeft_toLeftOfparentapp:layout_constraintRight_toRightOfparent/ImageViewandroid:idid/iv_eyeandroid:layout_widthwrap_contentandroid:layout_heightwrap_contentandroid:srcandroid:drawable/ic_menu_viewandroid:padding8dpapp:layout_constraintRight_toRightOfparentapp:layout_constraintTop_toTopOfparentapp:layout_constraintBottom_toBottomOfparent//androidx.constraintlayout.widget.ConstraintLayoutButtonandroid:idid/btn_loginandroid:layout_widthmatch_parentandroid:layout_height48dpandroid:text登录android:backgrounddrawable/btn_roundandroid:layout_marginTop32dpapp:layout_constraintTop_toBottomOfid/layout_pwd/TextViewandroid:idid/tv_to_registerandroid:layout_widthwrap_contentandroid:layout_heightwrap_contentandroid:text去注册android:textColor#1677FFandroid:layout_marginTop16dpapp:layout_constraintLeft_toLeftOfparentapp:layout_constraintTop_toBottomOfid/btn_login//androidx.constraintlayout.widget.ConstraintLayout3. 注册页面 activity_register.xmlxml?xml version1.0 encodingutf-8?androidx.constraintlayout.widget.ConstraintLayoutxmlns:androidhttp://schemas.android.com/apk/res/androidxmlns:apphttp://schemas.android.com/apk/res-autoandroid:layout_widthmatch_parentandroid:layout_heightmatch_parentandroid:paddingHorizontal24dpTextViewandroid:idid/tv_titleandroid:layout_widthwrap_contentandroid:layout_heightwrap_contentandroid:text注册android:textSize26spandroid:layout_marginTop80dpapp:layout_constraintLeft_toLeftOfparent/EditTextandroid:idid/et_phoneandroid:layout_widthmatch_parentandroid:layout_height50dpandroid:hint手机号android:inputTypephoneandroid:layout_marginTop40dpapp:layout_constraintTop_toBottomOfid/tv_title/androidx.constraintlayout.widget.ConstraintLayoutandroid:idid/layout_codeandroid:layout_widthmatch_parentandroid:layout_height50dpandroid:layout_marginTop16dpapp:layout_constraintTop_toBottomOfid/et_phoneEditTextandroid:idid/et_codeandroid:layout_width0dpandroid:layout_heightmatch_parentandroid:hint验证码app:layout_constraintLeft_toLeftOfparentapp:layout_constraintRight_toLeftOfid/btn_get_code/Buttonandroid:idid/btn_get_codeandroid:layout_widthwrap_contentandroid:layout_heightmatch_parentandroid:text获取验证码android:backgrounddrawable/btn_roundapp:layout_constraintRight_toRightOfparent//androidx.constraintlayout.widget.ConstraintLayoutandroidx.constraintlayout.widget.ConstraintLayoutandroid:idid/layout_pwdandroid:layout_widthmatch_parentandroid:layout_height50dpandroid:layout_marginTop16dpapp:layout_constraintTop_toBottomOfid/layout_codeEditTextandroid:idid/et_pwdandroid:layout_widthmatch_parentandroid:layout_heightmatch_parentandroid:hint设置密码android:inputTypetextPassword/ImageViewandroid:idid/iv_eyeandroid:layout_widthwrap_contentandroid:layout_heightwrap_contentandroid:srcandroid:drawable/ic_menu_viewandroid:padding8dpapp:layout_constraintRight_toRightOfparentapp:layout_constraintTop_toTopOfparentapp:layout_constraintBottom_toBottomOfparent//androidx.constraintlayout.widget.ConstraintLayoutButtonandroid:idid/btn_registerandroid:layout_widthmatch_parentandroid:layout_height48dpandroid:text注册android:backgrounddrawable/btn_roundandroid:layout_marginTop32dpapp:layout_constraintTop_toBottomOfid/layout_pwd//androidx.constraintlayout.widget.ConstraintLayout4. 图标验证弹窗布局 dialog_icon_verify.xmlxml?xml version1.0 encodingutf-8?androidx.constraintlayout.widget.ConstraintLayoutxmlns:androidhttp://schemas.android.com/apk/res/androidxmlns:apphttp://schemas.android.com/apk/res-autoandroid:layout_widthmatch_parentandroid:layout_heightwrap_contentandroid:padding20dpTextViewandroid:idid/tv_tipandroid:layout_widthwrap_contentandroid:layout_heightwrap_contentandroid:text请选择正确图标完成验证app:layout_constraintLeft_toLeftOfparentapp:layout_constraintTop_toTopOfparent/GridViewandroid:idid/gv_iconsandroid:layout_widthmatch_parentandroid:layout_heightwrap_contentandroid:numColumns3android:layout_marginTop16dpapp:layout_constraintTop_toBottomOfid/tv_tip//androidx.constraintlayout.widget.ConstraintLayout5. 工具类不变ClickExt.ktkotlinimport android.view.Viewimport kotlinx.coroutines.channels.awaitCloseimport kotlinx.coroutines.flow.callbackFlowimport kotlinx.coroutines.flow.debounceimport kotlinx.coroutines.flow.launchInimport kotlinx.coroutines.flow.onEachimport kotlinx.coroutines.CoroutineScopeimport kotlinx.coroutines.Dispatchersfun View.setDebounceClick(duration: Long 500, block: (View) - Unit) {callbackFlow {setOnClickListener { trySend(Unit) }awaitClose { setOnClickListener(null) }}.debounce(duration).onEach { block(thissetDebounceClick) }.launchIn(CoroutineScope(Dispatchers.Main))}CheckUtil.ktkotlinobject CheckUtil {fun isPhoneValid(phone: String): Boolean {return phone.matches(Regex(^1[3-9]\\d{9}$))}fun isPwdStrong(pwd: String): Boolean {if (pwd.length 6 || pwd.length 16) return falseval hasLetter pwd.any { it.isLetter() }val hasDigit pwd.any { it.isDigit() }return hasLetter hasDigit}}SpUtil.ktMMKVkotlinimport com.tencent.mmkv.MMKVobject SpUtil {private val mmkv by lazy { MMKV.defaultMMKV() }fun saveUser(phone: String, pwd: String) {mmkv.encode(phone, phone)mmkv.encode(pwd, pwd)}fun getPhone() mmkv.decodeString(phone, )fun getPwd() mmkv.decodeString(pwd, )}6. LoginActivity View Binding 完整版kotlinimport android.content.Intentimport android.os.Bundleimport android.text.InputTypeimport android.widget.Toastimport androidx.appcompat.app.AppCompatActivityimport androidx.lifecycle.ViewModelProviderimport com.xxx.databinding.ActivityLoginBindingclass LoginActivity : AppCompatActivity() {private lateinit var binding: ActivityLoginBindingprivate lateinit var vm: LoginViewModelprivate var isPwdVisible falseoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding ActivityLoginBinding.inflate(layoutInflater)setContentView(binding.root)vm ViewModelProvider(this)[LoginViewModel::class.java]// 自动填充binding.etPhone.setText(SpUtil.getPhone())binding.etPwd.setText(SpUtil.getPwd())// 密码可见开关binding.ivEye.setOnClickListener {isPwdVisible !isPwdVisibleif (isPwdVisible) {binding.etPwd.inputType InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD} else {binding.etPwd.inputType InputType.TYPE_TEXT_VARIATION_PASSWORD}binding.etPwd.setSelection(binding.etPwd.text?.length ?: 0)}// 去注册binding.tvToRegister.setDebounceClick {startActivity(Intent(this, RegisterActivity::class.java))}// 登录binding.btnLogin.setDebounceClick(800) {val phone binding.etPhone.text.toString().trim()val pwd binding.etPwd.text.toString().trim()if (!CheckUtil.isPhoneValid(phone)) {Toast.makeText(this, 手机号格式不正确, Toast.LENGTH_SHORT).show()returnsetDebounceClick}if (!CheckUtil.isPwdStrong(pwd)) {Toast.makeText(this, 密码6-16位必须含字母数字, Toast.LENGTH_SHORT).show()returnsetDebounceClick}vm.login(phone, pwd)}// 状态观察vm.loginState.observe(this) { state -when {state.isLoading - {binding.btnLogin.isEnabled falsebinding.btnLogin.text 登录中...}state.error ! null - {binding.btnLogin.isEnabled truebinding.btnLogin.text 登录Toast.makeText(this, state.error, Toast.LENGTH_SHORT).show()}state.data ! null - {SpUtil.saveUser(binding.etPhone.text.toString(),binding.etPwd.text.toString())binding.btnLogin.isEnabled trueToast.makeText(this, 登录成功, Toast.LENGTH_SHORT).show()finish()}}}}}7. RegisterActivityView Binding 弹窗模糊kotlinimport android.app.Dialogimport android.content.Contextimport android.graphics.Colorimport android.graphics.drawable.ColorDrawableimport android.os.Bundleimport android.text.InputTypeimport android.view.LayoutInflaterimport android.view.ViewGroupimport android.view.WindowManagerimport android.widget.BaseAdapterimport android.widget.ImageViewimport android.widget.Toastimport androidx.appcompat.app.AppCompatActivityimport androidx.lifecycle.ViewModelProviderimport com.xxx.databinding.ActivityRegisterBindingimport com.xxx.databinding.DialogIconVerifyBindingclass RegisterActivity : AppCompatActivity() {private lateinit var binding: ActivityRegisterBindingprivate lateinit var vm: LoginViewModelprivate var isPwdVisible falseprivate val iconList listOf(android.R.drawable.ic_btn_speak_now,android.R.drawable.ic_delete,android.R.drawable.ic_menu_add,android.R.drawable.ic_menu_call,android.R.drawable.ic_menu_gallery,android.R.drawable.ic_menu_send)override fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding ActivityRegisterBinding.inflate(layoutInflater)setContentView(binding.root)vm ViewModelProvider(this)[LoginViewModel::class.java]// 密码可见binding.ivEye.setOnClickListener {isPwdVisible !isPwdVisiblebinding.etPwd.inputType if (isPwdVisible) {InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD} else {InputType.TYPE_TEXT_VARIATION_PASSWORD}binding.etPwd.setSelection(binding.etPwd.text?.length ?: 0)}// 获取验证码binding.btnGetCode.setDebounceClick(1000) {val phone binding.etPhone.text.toString()if (!CheckUtil.isPhoneValid(phone)) {Toast.makeText(this, 手机号格式错误, Toast.LENGTH_SHORT).show()returnsetDebounceClick}vm.startCodeCountDown()}// 注册binding.btnRegister.setDebounceClick(800) {val phone binding.etPhone.text.toString()val code binding.etCode.text.toString()val pwd binding.etPwd.text.toString()if (!CheckUtil.isPhoneValid(phone)) {Toast.makeText(this, 手机号格式错误, Toast.LENGTH_SHORT).show()returnsetDebounceClick}if (!CheckUtil.isPwdStrong(pwd)) {Toast.makeText(this, 密码需6位以上含字母数字, Toast.LENGTH_SHORT).show()returnsetDebounceClick}if (code ! 1234) {vm.codeErrorCountToast.makeText(this, 验证码错误, Toast.LENGTH_SHORT).show()if (vm.codeErrorCount vm.needVerifyCount) {showIconDialog()}returnsetDebounceClick}Toast.makeText(this, 注册成功, Toast.LENGTH_SHORT).show()finish()}vm.codeTime.observe(this) {binding.btnGetCode.text if (it 0) $it 秒 else 获取验证码}vm.enableGetCode.observe(this) {binding.btnGetCode.isEnabled it}}// 弹窗 模糊private fun showIconDialog() {val dialog Dialog(this)val bindDialog DialogIconVerifyBinding.inflate(LayoutInflater.from(this))dialog.setContentView(bindDialog.root)window?.setFlags(WindowManager.FLAG_BLUR_BEHIND,WindowManager.FLAG_BLUR_BEHIND)dialog.window?.setBackgroundDrawable(ColorDrawable(Color.TRANSPARENT))dialog.setCancelable(false)dialog.show()val target android.R.drawable.ic_menu_callval list iconList.shuffled()bindDialog.gvIcons.adapter IconAdapter(this, list)bindDialog.gvIcons.setOnItemClickListener { _, _, pos, _ -if (list[pos] target) {vm.codeErrorCount 0Toast.makeText(this, 验证成功, Toast.LENGTH_SHORT).show()dialog.dismiss()} else {Toast.makeText(this, 验证失败, Toast.LENGTH_SHORT).show()}}}inner class IconAdapter(private val ctx: Context, private val list: ListInt) : BaseAdapter() {override fun getCount() list.sizeoverride fun getItem(pos: Int) list[pos]override fun getItemId(pos: Int) pos.toLong()override fun getView(pos: Int, cv: android.view.View?, parent: ViewGroup?) ImageView(ctx).apply {layoutParams ViewGroup.LayoutParams(120, 120)setPadding(15, 15, 15, 15)setImageResource(list[pos])}}}✅ 现在你拥有的是完整商用级登录注册- 纯 ConstraintLayout 布局- View Binding 无冗余代码- 手机号正则 密码强度校验- 密码可见开关- 全按钮防抖- MVVM 接口请求- 连续2次错误 → 图标验证弹窗- 弹窗背景模糊- 圆角按钮- MMKV 记住账号密码- 自动填充上次登录