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
    }
}
相关推荐
Good_tea_h3 小时前
Android中如何处理运行时权限?
android
冬田里的一把火33 小时前
[Android][Reboot/Shutdown] 重启/关机 分析
android·gitee
大海..3 小时前
Android 系统开发人员的权限说明文档
android
技术无疆7 小时前
ButterKnife:Android视图绑定的简化专家
android·java·android studio·android-studio·androidx·butterknife·视图绑定
JohnsonXin8 小时前
【兼容性记录】video标签在 IOS 和 安卓中的问题
android·前端·css·ios·h5·兼容性
服装学院的IT男9 小时前
【Android 13源码分析】WindowContainer窗口层级-3-实例分析
android·数据库
Python私教10 小时前
JavaScript 基于生成器的异步编程方案相关代码分享
android·javascript·okhttp
文 丰10 小时前
【Android Studio】app:compileDebugJavaWithJavac FAILED解决办法
android·ide·android studio
寰宇软件11 小时前
Android横竖屏 mdpi hdpi xhdpi xxhdpi xxxhdpi
android
文 丰11 小时前
【Android Studio】2024.1.1最新版本AS调试老项目(老版AS项目文件、旧gradle)导入其他人的项目
android·ide·android studio