1. Main activity(程序的入口)
-
Firebase Authentication:使用 FirebaseAuth 来处理用户认证。
-
Google Sign-In:设置 Google Sign-In 客户端和处理登录结果的逻辑。
-
ViewModels:
- registerViewModel:通过 viewModels 委托获取 RegisterViewModel 的实例,用于注册和登录逻辑。
- NoteViewModel、BookViewModel、ReadingRecordViewModel:这些 ViewModel 通过 viewModels 委托获取,分别用于管理笔记、书籍和阅读记录的数据。
-
Navigation:使用 NavHostController 和 AppNavigation Composable 来管理应用的导航。
-
Jetpack Compose:使用 Jetpack Compose 构建 UI,包括主题、导航和底部导航栏。
-
Google Sign-In Setup:setupGoogleSignIn 方法配置 Google Sign-In,包括请求 ID 令牌和电子邮件。
-
Handle Sign-In Result:handleSignInResult 方法处理 Google Sign-In 的结果,包括使用 Firebase Authentication 进行登录。
-
ViewModel Factory:RegisterViewModelFactory 类用于创建 RegisterViewModel 的实例。
-
Navigation Graph:在 AppNavigation Composable 中定义了导航图,包括不同的屏幕和它们的路径。
-
Bottom Navigation Bar:BottomNavigationBar Composable 创建底部导航栏,允许用户在不同的屏幕之间切换。
-
Authentication State Observation:observeUserAuthenticationState Composable 观察用户认证状态的变化。
-
Composable Screens:定义了多个 Composable 函数,如 LoginScreen、RegisterScreen、HomeScreen、BookShelf、AnalyticsScreen、NotesScreen、EditNotesScreen 和 AddBookScreen,这些函数构建应用的不同屏幕。
app/src/main/java/com/example/BookRecord/MainActivity.kt
class MainActivity : ComponentActivity() {
private val firebaseAuth: FirebaseAuth = FirebaseAuth.getInstance()
private lateinit var signInResultLauncher: ActivityResultLauncher<Intent>
private lateinit var googleSignInClient: GoogleSignInClient
private lateinit var navController: NavHostController // 删除 'private var' 的声明,改为 lateinit
// 添加一个属性来保存 RegisterViewModel 的引用
// private val registerViewModel by viewModels<RegisterViewModel> {
// RegisterViewModelFactory(UserRepository(getSharedPreferences("app_prefs", MODE_PRIVATE)))
// }
private val registerViewModel by viewModels<RegisterViewModel> {
RegisterViewModelFactory(AppDatabase.getDatabase(application))
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
AndroidThreeTen.init(this)
setupGoogleSignIn() // 调用新的方法来初始化谷歌登录
setContent {
val notesViewModel: NoteViewModel by viewModels()
val bookViewModel: BookViewModel by viewModels()
val readingRecordViewModel:ReadingRecordViewModel by viewModels()
// 初始化 navController
navController = rememberNavController()
BookRecordTheme {
CompositionLocalProvider(
LocalNotesViewModel provides notesViewModel,
LocalBooksViewModel provides bookViewModel,
LocalreadingRecordViewModel provides readingRecordViewModel,
) {
// 不再在这里声明 navController,直接传递上面初始化的 navController
AppNavigation(navController, registerViewModel, googleSignInClient, signInResultLauncher)
}
}
}
}
private fun setupGoogleSignIn() {
val gso = GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestIdToken(getString(R.string.default_web_client_id))
.requestEmail()
.build()
googleSignInClient = GoogleSignIn.getClient(this, gso)
signInResultLauncher = registerForActivityResult(ActivityResultContracts.StartActivityForResult()) { result ->
Log.d("GoogleSignIn", "Activity Result received")
if (result.resultCode == Activity.RESULT_OK) {
val task = GoogleSignIn.getSignedInAccountFromIntent(result.data)
handleSignInResult(task)
} else {
Log.d("GoogleSignIn", "Sign in failed or cancelled")
}
}
}
private fun handleSignInResult(task: Task<GoogleSignInAccount>) {
try {
val account = task.getResult(ApiException::class.java)
val idToken = account.idToken ?: throw Exception("Google ID Token is null")
val credential = GoogleAuthProvider.getCredential(idToken, null)
firebaseAuth.signInWithCredential(credential).addOnCompleteListener { authTask ->
if (authTask.isSuccessful) {
val firebaseUser = authTask.result?.user ?: throw Exception("Firebase user is null")
val userId = firebaseUser.uid
registerViewModel.checkAndInsertUser(userId).observe(this, Observer { userInserted ->
if (userInserted || registerViewModel.userAlreadyExists) {
navController.navigate("Book") // 如果用户新插入或已存在,都导航至Book页面
} else {
// 处理用户插入失败情况
Log.d("SignIn", "User insertion failed due to an error.")
Toast.makeText(this, "An error occurred during user insertion", Toast.LENGTH_LONG).show()
}
})
} else {
Log.e("SignIn", "Firebase Sign-In failed: ${authTask.exception?.localizedMessage}")
Toast.makeText(this, "Firebase Google Sign-In failed: ${authTask.exception?.localizedMessage}", Toast.LENGTH_LONG).show()
}
}
} catch (e: ApiException) {
Log.e("SignIn", "Google Sign-In failed code=${e.statusCode}")
Toast.makeText(this, "Google Sign-In failed: ${e.localizedMessage}", Toast.LENGTH_LONG).show()
}
}
class RegisterViewModelFactory(private val appDatabase: AppDatabase) : ViewModelProvider.Factory {
override fun <T : ViewModel> create(modelClass: Class<T>): T {
if (modelClass.isAssignableFrom(RegisterViewModel::class.java)) {
return RegisterViewModel(UserRepository(appDatabase)) as T
}
throw IllegalArgumentException("Unknown ViewModel class")
}
}
}
@SuppressLint("UnusedMaterial3ScaffoldPaddingParameter")
@Composable
fun AppNavigation(
navController: NavHostController, // 更改类型为 NavHostController
//activity: MainActivity,
registerViewModel: RegisterViewModel,
googleSignInClient: GoogleSignInClient,
signInResultLauncher: ActivityResultLauncher<Intent>
) {
val currentUser = observeUserAuthenticationState().value
val startDestination = if (currentUser != null) "Book" else "LoginScreen"
//val navController = rememberNavController()
// 根据当前的导航目的地决定是否显示底部导航栏
val shouldShowBottomBar = navController.currentBackStackEntryAsState().value?.destination?.route !in listOf("notesScreen","EditNotesScreen","LoginScreen","AddBooks","Register")
Scaffold(
bottomBar = { if (shouldShowBottomBar) {
BottomNavigationBar(navController) }
}
) { innerPadding ->
// 利用提供的 innerPadding 参数调整内容的内边距
NavHost(
navController = navController,
startDestination = startDestination,
//startDestination = "LoginScreen",
modifier = Modifier.padding(innerPadding) // 应用内边距
) {
composable("LoginScreen") {
//LoginScreen(navController, modifier = Modifier.fillMaxSize())
LoginScreen(navController,signInResultLauncher,googleSignInClient, modifier = Modifier.fillMaxSize(), registerViewModel)
}
composable("Register") {
// 获取一个与当前 Composable 生命周期相关联的 ViewModel 实例
//val registerViewModel: RegisterViewModel = viewModel()
// 调用 RegisterScreen 并将 viewModel 传递进去
RegisterScreen(navController, registerViewModel, modifier = Modifier.fillMaxSize())
}
composable("Book") { HomeScreen(googleSignInClient,navController,modifier = Modifier.fillMaxSize())}
composable("Bookshelf"){BookShelf(navController,modifier = Modifier.fillMaxSize()) }
composable("Analysis"){AnalyticsScreen(navController,modifier = Modifier.fillMaxSize())}
composable("notesScreen/{bookId}") { backStackEntry ->
// Extract the bookId parameter from the backStackEntry
val bookId = backStackEntry.arguments?.getString("bookId")?.toIntOrNull()
if (bookId == null) {
// 无法解析bookId,根据你的应用逻辑处理这种情况
// 比如返回上一屏或显示一个错误消息
} else {
NotesScreen(navController, bookId, modifier = Modifier.fillMaxSize())
}
}
composable("EditNotesScreen/{bookId}") { backStackEntry ->
// Extract the bookId parameter from the backStackEntry
val bookId = backStackEntry.arguments?.getString("bookId")?.toIntOrNull()
if (bookId == null) {
// 无法解析bookId,根据你的应用逻辑处理这种情况
// 比如返回上一屏或显示一个错误消息
} else {
EditNotesScreen(navController, bookId, modifier = Modifier.fillMaxSize())
}
}
composable("AddBooks"){ AddBookScreen(navController) }
}
}
}
@Composable
fun BottomNavigationBar(navController: NavController) {
BottomNavigation(
backgroundColor = Color(0xFFCCC2DC) // 设置导航栏颜色
) {
val items = listOf(
"Book" to Icons.Default.Book,
"Bookshelf" to Icons.Default.LibraryBooks,
"Analysis" to Icons.Default.BarChart,
)
items.forEach { (screen, icon) ->
BottomNavigationItem(
icon = { Icon(icon, contentDescription = screen) },
label = { Text(screen) },
selected = navController.currentDestination?.route == screen,
onClick = {
navController.navigate(screen) {
// 清理导航栈,避免导航栈过深
popUpTo(navController.graph.startDestinationId)
launchSingleTop = true
}
}
)
}
}
}
@Composable
fun observeUserAuthenticationState(): State<FirebaseUser?> {
val auth = FirebaseAuth.getInstance()
val currentUser = remember { mutableStateOf(auth.currentUser) }
DisposableEffect(Unit) {
val listener = FirebaseAuth.AuthStateListener { auth ->
currentUser.value = auth.currentUser
}
auth.addAuthStateListener(listener)
onDispose {
auth.removeAuthStateListener(listener)
}
}
return currentUser
}
2. Class
这些实体类使用了 Room Persistence Library 来创建和操作数据库。以下是每个实体类的详细说明:
- User 实体:
- 代表用户表。
- 包含一个字段 uid,它是用户的主键。
- Book 实体:
- 代表书籍表。
- 包含字段 id(主键,自动生成)、userId(外键,关联到 User 表的 uid 字段)、title(书名)、image(书籍图片链接)、author(作者)、pages(页数)、status(阅读状态)、readpage(已读页码)、press(出版社)和 startTime(开始阅读日期)。
- 使用 ForeignKey 注解来定义与 User 实体的关系,如果用户被删除,相应的书籍也会被删除(级联删除)。
- Note 实体:
- 代表笔记表。
- 包含字段 id(主键,自动生成)、content(笔记内容)和 bookId(外键,关联到 Book 表的 id 字段)。
- 使用 ForeignKey 注解来定义与 Book 实体的关系,如果书籍被删除,相应的笔记也会被删除(级联删除)。
- ReadingRecord 实体:
- 代表阅读记录表。
- 包含字段 id(主键,自动生成)、userId(外键,关联到 User 表的 uid 字段)、date(阅读日期)和 readPages(当天阅读的页数)。
- 使用 ForeignKey 注解来定义与 User 实体的关系,如果用户被删除,相应的阅读记录也会被删除(级联删除)。
这些实体类是应用程序数据持久化的基础,它们允许应用程序存储和管理用户、书籍、笔记和阅读记录等信息。通过定义这些实体类,Room 库可以自动生成数据库访问代码,简化数据库操作。
app/src/main/java/com/example/BookRecord/Class.kt
package com.example.BookRecord
import androidx.room.ColumnInfo
import androidx.room.Entity
import androidx.room.ForeignKey
import androidx.room.PrimaryKey
import org.threeten.bp.LocalDate
@Entity(tableName = "users")
data class User(
@PrimaryKey val uid: String
)
@Entity(
tableName = "books",
foreignKeys = [
ForeignKey(
entity = User::class,
parentColumns = ["uid"],
childColumns = ["userId"],
onDelete = ForeignKey.CASCADE
)
]
)
data class Book(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
var userId: String, // 添加这个字段来连接User
var title: String,
var image: String,
var author: String,
var pages: String,
var status: BookStatus,
var readpage: String,
var press: String,
var startTime: LocalDate
)
@Entity(
foreignKeys = [
ForeignKey(
entity = Book::class,
parentColumns = ["id"], // Book类的ID字段
childColumns = ["bookId"], // Note类的bookId字段,作为外键
onDelete = ForeignKey.CASCADE // 如果Book被删除,则相应的Note也被删除
)
]
)
data class Note(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
var content: String,
val bookId: Int // 引用Book的ID
)
//创建一个实体来记录每日的阅读页数
@Entity(tableName = "reading_records",
foreignKeys = [
ForeignKey(
entity = User::class,
parentColumns = ["uid"],
childColumns = ["userId"],
onDelete = ForeignKey.CASCADE
)
])
data class ReadingRecord(
@PrimaryKey(autoGenerate = true) val id: Int = 0,
val userId: String, // 引用User的UID
val date: LocalDate, // 记录阅读的日期
val readPages: Int // 当天读的页数
)
3. CompositionLocal
它们分别用于在 Composable 函数之间共享 NoteViewModel、BookViewModel 和 ReadingRecordViewModel 的实例。Composition Locals 是一种在 Compose 中进行属性传递的方式,类似于在 Android 视图中使用 Context。
app/src/main/java/com/example/BookRecord/CompositionLocal.kt
package com.example.BookRecord
import androidx.compose.runtime.staticCompositionLocalOf
// 提供默认的 ViewModel 实例或错误提示
val LocalNotesViewModel = staticCompositionLocalOf<NoteViewModel> {
error("No NotesViewModel provided")
}
// 提供默认的 ViewModel 实例或错误提示
val LocalBooksViewModel = staticCompositionLocalOf<BookViewModel> {
error("No NotesViewModel provided")
}
val LocalreadingRecordViewModel = staticCompositionLocalOf<ReadingRecordViewModel> {
error("No NotesViewModel provided")
}
4. TypeConverter
定义了两个 Room 数据库类型转换器类,用于在数据库列的类型和 Java/Kotlin 对象的类型之间进行转换。Room 数据库使用这些转换器来处理那些不能直接映射到 SQLite 数据类型的复杂对象。
- BookStatusConverter 类:
- 这个类包含了两个方法,用于转换 BookStatus 枚举和数据库中的 String 类型。
- toBookStatus 方法将数据库中的 String 类型转换为 BookStatus 枚举。它使用 enumValueOf 函数来查找与给定字符串名称对应的枚举值。
- fromBookStatus 方法将 BookStatus 枚举转换为数据库中的 String 类型。它返回枚举的名称,该名称将被存储在数据库中。
- DataConverters 类:
- 这个类包含了两个方法,用于转换 LocalDate 对象和数据库中的 String 类型。
- fromLocalDate 方法将 LocalDate 对象转换为 String。它调用 LocalDate 的 toString 方法来获取 ISO 格式的日期字符串。
- toLocalDate 方法将 String 转换为 LocalDate 对象。它使用 LocalDate.parse 方法来解析 ISO 格式的日期字符串,并返回 LocalDate 对象。
app/src/main/java/com/example/BookRecord/TypeConverter.kt
package com.example.BookRecord
import androidx.room.TypeConverter
import org.threeten.bp.LocalDate
class BookStatusConverter {
@TypeConverter
fun toBookStatus(value: String) = enumValueOf<BookStatus>(value)
@TypeConverter
fun fromBookStatus(status: BookStatus) = status.name
}
class DataConverters {
@TypeConverter
fun fromLocalDate(value: LocalDate?): String? {
return value?.toString()
}
@TypeConverter
fun toLocalDate(value: String?): LocalDate? {
return value?.let { LocalDate.parse(it) }
}
}
ps
app的最终页面见和该文章同一专栏下的博文:安卓开发:BookRecord一款专为纸质书爱好者设计的阅读追踪应用