BookRecord的登录和注册页面以及相关的viewmodel

ps:这个app既提供普通的登录注册,也提供使用谷歌邮箱进行登录和注册。可以按照自己的需要进行删改

1. 注册页面

  1. RegisterScreen 函数定义了一个可组合的(Composable)界面,用于用户输入注册信息。它接收一个导航控制器(NavController)、一个视图模型(RegisterViewModel)和一个修饰符(Modifier)作为参数。

  2. 界面布局使用 Column 组件,以垂直方式排列子组件,并在水平方向上居中。

  3. 界面包含多个输入字段,包括用户名、电话号码、性别选择器、出生日期选择器、电子邮件和密码。每个字段都使用了 OutlinedTextField 组件,除了性别和出生日期选择器。

  4. 性别选择器 GenderSelector 是一个自定义的可组合函数,它显示一个下拉菜单让用户选择性别。

  5. 出生日期选择器使用 DatePicker 组件,允许用户选择一个日期。

  6. 密码输入字段包括一个切换密码可见性的图标,以及一个密码强度规则的说明。

  7. 注册按钮在用户输入有效信息时启用,点击后会调用视图模型的 registerUser 函数来处理注册逻辑。

  8. 界面底部有一个文本按钮,提示用户如果已有账户可以登录。

  9. PasswordRule 函数用于显示密码规则,它会显示一个图标和文本,指示密码规则是否满足。

  10. GenderSelector 函数用于创建一个性别选择下拉菜单。

  11. @OptIn(ExperimentalMaterial3Api::class) 注解允许使用实验性的Material 3 API。

app/src/main/java/com/example/BookRecord/RegisterScreen.kt

kotlin 复制代码
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun RegisterScreen(
    navController: NavController,
    viewModel: RegisterViewModel, // 保留 viewModel 参数
    modifier: Modifier = Modifier
) {
    Column(
        modifier = modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        var email by remember { mutableStateOf("") }
        var emailError by remember { mutableStateOf(false) }
        var password by remember { mutableStateOf("") }
        var passwordError by remember { mutableStateOf(false) }
        var passwordVisible by remember { mutableStateOf(false) }
        var username by remember { mutableStateOf("") }
        var phoneNumber by remember { mutableStateOf("") }
        var gender by remember { mutableStateOf("") }
        var showDatePicker by remember { mutableStateOf(false) }
        val datePickerState = rememberDatePickerState(initialSelectedDateMillis = Instant.now().toEpochMilli())
        var selectedDate by remember { mutableStateOf(Instant.now().toEpochMilli()) }
        val formatter = SimpleDateFormat("dd/MM/yyyy", Locale.getDefault())
        val context = LocalContext.current


        // 用户名输入框
        OutlinedTextField(
            value = username,
            onValueChange = { username = it },
            label = { Text("Username") },
            singleLine = true,
            modifier = Modifier.fillMaxWidth()
        )

        Spacer(modifier = Modifier.height(8.dp))

        // 电话号码输入框
        OutlinedTextField(
            value = phoneNumber,
            onValueChange = { phoneNumber = it },
            label = { Text("Phone Number") },
            singleLine = true,
            modifier = Modifier.fillMaxWidth()
        )

        Spacer(modifier = Modifier.height(8.dp))

        // 性别选择框
        GenderSelector(
            gender = gender,
            onGenderSelect = { selectedGender ->
                gender = selectedGender
            }
        )

        Spacer(modifier = Modifier.height(8.dp))

        // 日期选择框
        OutlinedTextField(
            value = formatter.format(Date(selectedDate)),
            onValueChange = { /* 无需处理 */ },
            label = { Text("Birthdate") },
            readOnly = true,
            singleLine = true,
            modifier = Modifier.fillMaxWidth().clickable { showDatePicker = true },
            trailingIcon = {
                // Ensure icon is also clickable
                Icon(
                    imageVector = Icons.Default.CalendarToday,
                    contentDescription = "Select Date",
                    modifier = Modifier.clickable { showDatePicker = true }  // Ensure icon is clickable
                )
            }
        )

        if (showDatePicker) {
            DatePickerDialog(
                onDismissRequest = { showDatePicker = false },
                confirmButton = {
                    TextButton(onClick = {
                        showDatePicker = false
                        selectedDate = datePickerState.selectedDateMillis!!
                    }) {
                        Text("OK")
                    }
                },
                dismissButton = {
                    TextButton(onClick = { showDatePicker = false }) {
                        Text("Cancel")
                    }
                }
            ) {
                DatePicker(state = datePickerState)
            }
        }

        Spacer(modifier = Modifier.height(8.dp))

        // Email 输入框
        OutlinedTextField(
            value = email,
            onValueChange = {
                email = it
                emailError = !isValidEmail(email)
            },
            label = { Text("Email") },
            singleLine = true,
            modifier = Modifier.fillMaxWidth(),
            isError = emailError
        )
        if (emailError) {
            Text("Invalid email", color = MaterialTheme.colorScheme.error)
        }

        Spacer(modifier = Modifier.height(8.dp))

        // 密码输入框
        var minLength by remember { mutableStateOf(false) }
        var hasNumber by remember { mutableStateOf(false) }
        //val context = LocalContext.current  // 获取当前 Compose 的 Context

        OutlinedTextField(
            value = password,
            onValueChange = {
                password = it
                minLength = password.length >= 8
                hasNumber = password.any { it.isDigit() }
                passwordError = !(minLength && hasNumber) // Ensure both conditions are met
            },
            label = { Text("Password") },
            singleLine = true,
            visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), // Visibility is based on the toggle state
            trailingIcon = {
                val image = if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
                IconButton(onClick = { passwordVisible = !passwordVisible }) {
                    Icon(imageVector = image, contentDescription = if (passwordVisible) "Hide password" else "Show password")
                }
            },
            modifier = Modifier.fillMaxWidth(),
            isError = passwordError
        )

        //Spacer(modifier = Modifier.height(10.dp))
// 密码规则说明
        Column(modifier = Modifier.padding(top = 8.dp)) {
            PasswordRule("Minimum 8 characters", minLength)
            PasswordRule("At least one number", hasNumber)
        }

        Spacer(modifier = Modifier.height(14.dp))

        // 注册按钮
        Button(
            onClick = {
                if (!emailError && !passwordError) {
                    val birthdateString = formatter.format(Date(selectedDate))  // 将时间戳转换为字符串
                    val userInfo = UserInfo(
                        email = email,
                        username = username,
                        phoneNumber = phoneNumber,
                        gender = gender,
                        birthdate = birthdateString
                    )
                    viewModel.registerUser(email, password, userInfo)
                    Toast.makeText(context, "Registration successful. Login.", Toast.LENGTH_LONG).show()
                    viewModel.resetLoginStatus()
                    navController.navigate("LoginScreen")
                }
            },
            modifier = Modifier
                .fillMaxWidth()
                .height(50.dp),
            enabled = !emailError && !passwordError
        ) {
            Text(
                text = "Register",
                fontSize = 18.sp,
                fontWeight = FontWeight.Bold
            )
        }

        Spacer(modifier = Modifier.height(8.dp))

        // 已经有账号?返回登录界面
        TextButton(onClick = { navController.navigate("LoginScreen") }) {
            Text("Already have an account? Log In")
        }
    }
}


@Composable
fun PasswordRule(text: String, isValid: Boolean) {
    Row(verticalAlignment = Alignment.CenterVertically,
        horizontalArrangement = Arrangement.Start) {
        Icon(
            imageVector = if (isValid) Icons.Filled.CheckCircle else Icons.Filled.Error,
            contentDescription = null,
            tint = if (isValid) Color(0xC36FB147) else Color(0xFFF44336),
            modifier = Modifier.size(20.dp)
        )
        Spacer(modifier = Modifier.width(8.dp))
        Text(text, color = if (isValid) Color(0xC36FB147) else Color(0xFFF44336), style = MaterialTheme.typography.bodyMedium)
    }
}

@Composable
fun GenderSelector(
    gender: String,
    onGenderSelect: (String) -> Unit,
    modifier: Modifier = Modifier
) {
    val genderOptions = listOf("Male", "Female", "Other")
    var showMenu by remember { mutableStateOf(false) }

    Column(modifier = modifier) {
        OutlinedTextField(
            value = gender,
            onValueChange = { /* 无需处理 */ },
            label = { Text("Gender") },
            singleLine = true,
            readOnly = true,  // 使文本框只读
            modifier = Modifier.fillMaxWidth(),
            trailingIcon = {
                Icon(
                    imageVector = Icons.Default.ArrowDropDown,
                    contentDescription = "Select Gender",
                    modifier = Modifier.clickable { showMenu = true }
                )
            },
        )
        DropdownMenu(
            expanded = showMenu,
            onDismissRequest = { showMenu = false }
        ) {
            genderOptions.forEach { option ->
                DropdownMenuItem(
                    onClick = {
                        onGenderSelect(option)
                        showMenu = false
                    }) {
                    Text(text = option)
                }
            }
        }
    }
}



@Preview
@Composable
fun RegisterScreen() {

}

2. 登录页面

  1. 界面布局:使用 Column 组件来垂直排列界面元素,并在水平方向上居中。

  2. 登录标题:显示一个标题 "Login",使用加粗字体和主题颜色。

  3. 电子邮件输入:提供一个 OutlinedTextField 用于输入电子邮件,如果输入无效(通过 isValidEmail 函数验证),则显示错误信息。

  4. 密码输入:提供一个 OutlinedTextField 用于输入密码,支持显示/隐藏密码功能。如果密码长度小于8个字符或不包含数字(通过 isValidPassword 函数验证),则显示错误信息。

  5. 登录按钮:当电子邮件和密码输入有效时,点击登录按钮会触发 viewModel.loginUser 函数。

  6. 登录状态观察:通过 viewModel.loginStatus 观察登录状态,如果登录成功,则导航到 "Book" 页面;如果登录失败,则显示错误信息。

  7. 第三方登录:提供了一个 "Sign in with Google" 按钮,使用 googleSignInClient 来处理谷歌登录。

  8. 注册链接:提供了一个 "Register" 按钮,用于导航到注册页面。

  9. 辅助函数:

  • isValidEmail:验证电子邮件地址是否有效。
  • isValidPassword:验证密码是否至少有8个字符并且包含至少一个数字。

新参数:

signInLauncher:一个 ActivityResultLauncher,用于处理第三方登录的启动和结果。

googleSignInClient:用于谷歌登录的客户端对象。

app/src/main/java/com/example/BookRecord/LoginScreen.kt

kotlin 复制代码
@Composable
fun LoginScreen(
    //auth: FirebaseAuth,
    navController: NavController,
    signInLauncher: ActivityResultLauncher<Intent>, // 添加这个新参数
    googleSignInClient: GoogleSignInClient, // 这个应该从Activity或ViewModel传递到Composable
    modifier: Modifier = Modifier,
    viewModel: RegisterViewModel,
) {
    Column(
        modifier = modifier
            .fillMaxSize()
            .padding(16.dp),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        Text(
            text = "Login",
            fontSize = 30.sp,
            fontWeight = FontWeight.Bold,
            color = MaterialTheme.colorScheme.primary
        )

        Spacer(modifier = Modifier.height(16.dp))

        var email by remember { mutableStateOf("") }
        var emailError by remember { mutableStateOf(false) }

        OutlinedTextField(
            value = email,
            onValueChange = {
                email = it
                emailError = !isValidEmail(email)
            },
            label = { Text("Email") },
            singleLine = true,
            modifier = Modifier.fillMaxWidth(),
            isError = emailError
        )
        if (emailError) {
            Text("Invalid email", color = MaterialTheme.colorScheme.error)
        }

        Spacer(modifier = Modifier.height(5.dp))

        var password by remember { mutableStateOf("") }
        var passwordError by remember { mutableStateOf(false) }
        var passwordVisible by remember { mutableStateOf(false) } // 新增状态

        OutlinedTextField(
            value = password,
            onValueChange = {
                password = it
                passwordError = !isValidPassword(password)
            },
            label = { Text("Password") },
            singleLine = true,
            visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), // 根据状态选择是否显示密码
            trailingIcon = { // 添加眼睛图标作为尾随图标
                val image = if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff
                val description = if (passwordVisible) "Hide password" else "Show password"
                IconButton(onClick = { passwordVisible = !passwordVisible }) {
                    Icon(imageVector = image, contentDescription = description)
                }
            },
            modifier = Modifier.fillMaxWidth(),
            isError = passwordError
        )
        if (passwordError) {
            Text("Should be more than 8 characters", color = MaterialTheme.colorScheme.error)
        }
        Spacer(modifier = Modifier.height(32.dp))


        Button(
            onClick = {
                if (!emailError && !passwordError) {
                    viewModel.loginUser(email, password)
                }
            },
            modifier = Modifier
                .fillMaxWidth()
                .height(50.dp)
        ) {
            Text(
                text = "Log In",
                fontSize = 18.sp,
                fontWeight = FontWeight.Bold
            )
        }
// 观察登录状态,并根据状态进行导航或显示错误消息
        val loginStatus by viewModel.loginStatus.observeAsState()
        loginStatus?.let { isSuccess ->
            if (isSuccess) {
                LaunchedEffect(isSuccess) {
                    navController.navigate("Book")
                    {
                        popUpTo(0) { inclusive = true }
                    }
                }
            } else {
                // 显示错误信息
                Text("Invalid email or password", color = MaterialTheme.colorScheme.error)
            }
        }
        Spacer(modifier = Modifier.height(8.dp))

        Row {
            TextButton(
                onClick = {
                    val signInIntent = googleSignInClient.signInIntent
                    Log.d("GoogleSignIn", "Launching sign in intent.")
                    signInLauncher.launch(signInIntent) // 使用signInLauncher来启动登录意图
                }) {
                Text("Sign in with Google")
            }


            Spacer(Modifier.weight(1f))

            TextButton(onClick = {
                navController.navigate("Register")
            }) {
                Text("Register")
            }
        }
    }
}

fun isValidEmail(email: String): Boolean {
    return android.util.Patterns.EMAIL_ADDRESS.matcher(email).matches()
}

fun isValidPassword(password: String): Boolean {
    return password.length >= 8 && password.any { it.isDigit() }
}

@Preview
@Composable
fun LoginScreen() {

}

3. Register View Model

  1. 依赖注入:RegisterViewModel 构造函数接收一个 UserRepository 类型的参数,这通常用于数据访问和业务逻辑处理。

  2. LiveData 状态:

  • _registrationStatus:一个 MutableLiveData 实例,用于观察注册操作的状态(成功或失败)。
    registrationStatus:一个 LiveData 实例,提供注册状态的只读访问。
  • _loginStatus:一个 MutableLiveData 实例,用于观察登录操作的状态。
  • loginStatus:一个 LiveData 实例,提供登录状态的只读访问。
  1. 用户存在检查:userAlreadyExists 属性是一个布尔值,通过 userRepository 的 userAlreadyExists 属性来获取,用于检查用户是否已存在。

  2. 用户注册:

  • checkAndInsertUser 函数接收一个用户ID(uid),并返回一个 LiveData,表示用户是否存在或插入操作的结果。
  • registerUser 函数接收电子邮件、密码和用户信息(UserInfo),并在 IO 线程上执行注册逻辑,然后将注册结果更新到 _registrationStatus。
  1. 用户登录:
  • loginUser 函数接收电子邮件和密码,在 IO 线程上执行登录逻辑,然后将登录结果更新到 _loginStatus。
  1. 重置登录状态:
  • resetLoginStatus 函数用于重置或清除登录状态,将 _loginStatus 的值设置为 false。

    协程处理:

  • 使用 viewModelScope.launch 在 ViewModel 的作用域内启动协程,以便在后台线程上执行耗时操作。

  • 使用 Dispatchers.IO 指定在 IO 线程上执行数据库操作或其他 IO 密集型任务。

  • 使用 withContext(Dispatchers.Main) 将操作切换回主线程,以便更新 UI 相关的 LiveData 状态。

app/src/main/java/com/example/BookRecord/RegisterViewModel.kt

kotlin 复制代码
package com.example.BookRecord


import androidx.lifecycle.LiveData
import androidx.lifecycle.MutableLiveData
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext

class RegisterViewModel(private val userRepository: UserRepository) : ViewModel() {

    private val _registrationStatus = MutableLiveData<Boolean>()
    val registrationStatus: LiveData<Boolean> = _registrationStatus
    private val _loginStatus = MutableLiveData<Boolean>()
    val loginStatus: LiveData<Boolean> = _loginStatus
    val userAlreadyExists: Boolean
        get() = userRepository.userAlreadyExists

    fun checkAndInsertUser(uid: String): LiveData<Boolean> {
        val result = MutableLiveData<Boolean>()
        viewModelScope.launch {
            result.value = userRepository.checkAndInsertUser(uid)
        }
        return result
    }

    fun registerUser(email: String, password: String, userInfo: UserInfo) {
        viewModelScope.launch(Dispatchers.IO) {
            val isSuccess = userRepository.registerUser(email, password, userInfo)
            withContext(Dispatchers.Main) {
                _registrationStatus.value = isSuccess
            }
        }
    }

    fun loginUser(email: String, password: String) {
        viewModelScope.launch(Dispatchers.IO) {
            val isSuccess = userRepository.loginUser(email, password)
            withContext(Dispatchers.Main) {
                _loginStatus.value = isSuccess
            }
        }
    }

    fun resetLoginStatus() {
        // 逻辑来重置或清除登录状态
        _loginStatus.value = false
    }
}
相关推荐
千里马学框架1 天前
一起学 Android 14:ShellTransition 屏幕旋转过程深度剖析
android·智能手机·性能优化·framework·性能·屏幕旋转·rotation
美狐美颜SDK开放平台1 天前
开发直播APP时如何接入视频美颜SDK?开发流程与注意事项
android·人工智能·计算机视觉·音视频·直播美颜sdk
AFinalStone1 天前
Android7 SystemUI源码解析(七)Keyguard锁屏模块深度解析
android·systemui
致远ccc1 天前
Google Play 上架前如何测试 App?多国家 Android 环境测试
android·app测试·googleplay·多国家应用测试
ttyyttemo1 天前
Kotlin 协程中的 Job 结构化并发与取消
android
sun0077001 天前
tbox 4g/5g切换,导致wan ip 改变,导致车机旧网络不可用。需要重启车机才行
android
其实防守也摸鱼1 天前
内网穿透与反向代理:原理、工具与实战指南
android·大数据·运维·安全·网络安全·自动化·渗透
AFinalStone2 天前
Android7 SystemUI 源码解析(四)NavigationBar 导航栏与 SystemBars
android·systemui
JMchen2 天前
属性动画原理与高级动画实现
android·kotlin·canvas
AFinalStone2 天前
Android7 SystemUI 源码解析(二)启动流程深度解析
android·systemui