ps:这个app既提供普通的登录注册,也提供使用谷歌邮箱进行登录和注册。可以按照自己的需要进行删改
1. 注册页面
-
RegisterScreen 函数定义了一个可组合的(Composable)界面,用于用户输入注册信息。它接收一个导航控制器(NavController)、一个视图模型(RegisterViewModel)和一个修饰符(Modifier)作为参数。
-
界面布局使用 Column 组件,以垂直方式排列子组件,并在水平方向上居中。
-
界面包含多个输入字段,包括用户名、电话号码、性别选择器、出生日期选择器、电子邮件和密码。每个字段都使用了 OutlinedTextField 组件,除了性别和出生日期选择器。
-
性别选择器 GenderSelector 是一个自定义的可组合函数,它显示一个下拉菜单让用户选择性别。
-
出生日期选择器使用 DatePicker 组件,允许用户选择一个日期。
-
密码输入字段包括一个切换密码可见性的图标,以及一个密码强度规则的说明。
-
注册按钮在用户输入有效信息时启用,点击后会调用视图模型的 registerUser 函数来处理注册逻辑。
-
界面底部有一个文本按钮,提示用户如果已有账户可以登录。
-
PasswordRule 函数用于显示密码规则,它会显示一个图标和文本,指示密码规则是否满足。
-
GenderSelector 函数用于创建一个性别选择下拉菜单。
-
@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. 登录页面
-
界面布局:使用 Column 组件来垂直排列界面元素,并在水平方向上居中。
-
登录标题:显示一个标题 "Login",使用加粗字体和主题颜色。
-
电子邮件输入:提供一个 OutlinedTextField 用于输入电子邮件,如果输入无效(通过 isValidEmail 函数验证),则显示错误信息。
-
密码输入:提供一个 OutlinedTextField 用于输入密码,支持显示/隐藏密码功能。如果密码长度小于8个字符或不包含数字(通过 isValidPassword 函数验证),则显示错误信息。
-
登录按钮:当电子邮件和密码输入有效时,点击登录按钮会触发 viewModel.loginUser 函数。
-
登录状态观察:通过 viewModel.loginStatus 观察登录状态,如果登录成功,则导航到 "Book" 页面;如果登录失败,则显示错误信息。
-
第三方登录:提供了一个 "Sign in with Google" 按钮,使用 googleSignInClient 来处理谷歌登录。
-
注册链接:提供了一个 "Register" 按钮,用于导航到注册页面。
-
辅助函数:
- 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
-
依赖注入:RegisterViewModel 构造函数接收一个 UserRepository 类型的参数,这通常用于数据访问和业务逻辑处理。
-
LiveData 状态:
- _registrationStatus:一个 MutableLiveData 实例,用于观察注册操作的状态(成功或失败)。
registrationStatus:一个 LiveData 实例,提供注册状态的只读访问。 - _loginStatus:一个 MutableLiveData 实例,用于观察登录操作的状态。
- loginStatus:一个 LiveData 实例,提供登录状态的只读访问。
-
用户存在检查:userAlreadyExists 属性是一个布尔值,通过 userRepository 的 userAlreadyExists 属性来获取,用于检查用户是否已存在。
-
用户注册:
- checkAndInsertUser 函数接收一个用户ID(uid),并返回一个 LiveData,表示用户是否存在或插入操作的结果。
- registerUser 函数接收电子邮件、密码和用户信息(UserInfo),并在 IO 线程上执行注册逻辑,然后将注册结果更新到 _registrationStatus。
- 用户登录:
- loginUser 函数接收电子邮件和密码,在 IO 线程上执行登录逻辑,然后将登录结果更新到 _loginStatus。
- 重置登录状态:
-
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
}
}