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
    }
}
相关推荐
雨白7 小时前
Jetpack系列(二):Lifecycle与LiveData结合,打造响应式UI
android·android jetpack
kk爱闹9 小时前
【挑战14天学完python和pytorch】- day01
android·pytorch·python
每次的天空11 小时前
Android-自定义View的实战学习总结
android·学习·kotlin·音视频
恋猫de小郭11 小时前
Flutter Widget Preview 功能已合并到 master,提前在体验毛坯的预览支持
android·flutter·ios
断剑重铸之日12 小时前
Android自定义相机开发(类似OCR扫描相机)
android
随心最为安12 小时前
Android Library Maven 发布完整流程指南
android
岁月玲珑12 小时前
【使用Android Studio调试手机app时候手机老掉线问题】
android·ide·android studio
还鮟16 小时前
CTF Web的数组巧用
android
小蜜蜂嗡嗡18 小时前
Android Studio flutter项目运行、打包时间太长
android·flutter·android studio
aqi0018 小时前
FFmpeg开发笔记(七十一)使用国产的QPlayer2实现双播放器观看视频
android·ffmpeg·音视频·流媒体