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
    }
}
相关推荐
hai_android29 分钟前
Android 设备锁定:Kiosk Mode 实战
android·java·android jetpack
我不会插花弄玉38 分钟前
9. 表的内连和外连【由浅入深-MySQL】
android·数据库·mysql
嘟哩DuliDuli1 小时前
剧本节点输入归一化:文本来源、版本与下游引用
android·人工智能·安全·ai·软件工程
high一马平川1 小时前
# SystemUI Dagger2 精读 · 第 2 章 @Inject:1200 处构造注入,和 1 处例外
android·java·kotlin
大熊的瓜地2 小时前
从0开始写android -完结篇
android
扶尔魔ocy2 小时前
【QT android】模仿行车记录仪案例
android·qt
看我debug2 小时前
Android 运行时分析工作台 (Demo)。统一管理 Frida 脚本、自动注入(修改运行时代码)、收集结构化结果、生成 JSON 报告。
android·python·pip
通信侠2 小时前
android 13重启清除任务快照
android
小铁-Android3 小时前
CompletableDeferred:Kotlin 协程中用于一次性挂起通信的工具
android·kotlin
恋猫de小郭3 小时前
Flutter 的文字为什么比 SwiftUI 更细?问题主要在 Skia 的 256 项查找表里
android·前端·flutter