2024-10-14
Kotlin & 客户端
00

目录

1 Note 相关内容
1.1 数据库设计
1.2 Repository 设定
1.3 ViewModel 设计
1.4 View 设计

主要内容:

  1. Note 相关内容

1 Note 相关内容

Note 相关的功能主要效果大致上就是一个临时的笔记功能。它需要一个编辑页面,一个展示页面。

1.1 数据库设计

为了使 SQLite 存入时间,需要加入一个LocalDateTime的类型转换器。

kotlin
// data/local/SQLiteDataBase.kt @Database(entities = [Note::class], version = 1) @TypeConverters(com.misaka.workflow.data.local.TypeConverter::class) abstract class SQLiteDataBase : RoomDatabase() { abstract fun getNoteDAO(): NoteDAO companion object { @Volatile private var INSTANCE: SQLiteDataBase? = null /** * getInstance 单例模式 获取数据库实例 通过它获取 DAO */ fun getInstance (context: Context): SQLiteDataBase { return INSTANCE ?: synchronized(this) { // 确保线程安全 INSTANCE ?: Room.databaseBuilder( context.applicationContext, SQLiteDataBase::class.java, "WorkFlow" ).build().also { INSTANCE = it } } } } } /** * TypeConverter * * 提供给 Room 的类型转换器 * * 注意 数据库里没存时区 时区固定使用系统默认时区 */ class TypeConverter { private val timeZoneId = ZoneId.systemDefault() private val timeZoneOffset = ZonedDateTime.now().offset @TypeConverter fun fromTimeStamp (timeStamp: Long?): LocalDateTime? { return timeStamp?.let { it -> LocalDateTime.ofInstant(Instant.ofEpochMilli(it), timeZoneId) } } @TypeConverter fun fromLocalDateTime (localDateTime: LocalDateTime?): Long? { return localDateTime?.toInstant(timeZoneOffset)?.toEpochMilli() } }
kotlin
// data/model/Note.kt /** * Note * * 该实体对应数据库中的 note 表 其内部存储一个便签的标签 内容和最后修改时间 */ @Entity( tableName = "note" ) data class Note( @PrimaryKey(autoGenerate = true) var id: Long = 0, // 这里将 id 设置为 var 是有意义的 因为主键在获取 Note 实例时无法确定 只能在插入数据库之后才能确定 @ColumnInfo(name = "title") val title: String = "", @ColumnInfo(name = "noteContent") val content: String = "", @ColumnInfo(name = "modifyTime") val modifyTime: LocalDateTime = LocalDateTime.now() )

提示

Room 并不会主动修改通过其存入的字段所对应的实体的主键。如果表的主键被设置为自增,那么最好是把主键相关字段设置为可变,并且将存储的方法返回值设置为Long以获取实际在表中的主键值来修改主键字段。

如果 ViewModel 层的数据来源是 Room 所返回的 Flow 的话,这一点其实不是那么重要。

kotlin
// data/local/NoteDAO.kt @Dao interface NoteDAO { /** * delete 方法返回的是成功删除的行数 * * insert 方法返回的是新插入的实体的 rowId * * update 方法返回的是成功更新的行数 */ @Query("select * from note") fun getAllNote(): List<Note> @Delete fun deleteNotes(vararg notes: Note): Int /** * attention 插入一个新的实体并不会修改实体的id 必须通过 insert 返回的 id 获取插入后的 id */ @Insert(entity = Note::class, onConflict = OnConflictStrategy.REPLACE) fun insertNewNotes(vararg node: Note): List<Long> @Update(onConflict = OnConflictStrategy.REPLACE) fun updateNotes(vararg note: Note): Int @Query("select * from note") fun getAllNoteInFlow(): Flow<List<Note>> @Query("select * from note where id = :id") fun getNoteByIdInFlow(id: Int): Flow<Note?> /** * getLatestModifyTime 获取最新的修改时间所对应的数据 */ @Query("select * from note order by modifyTime DESC limit 1") fun getLatestModifyTime(): Note? // 全局按标题和内容搜索 @Query("select * from note where title like '%' || :text || '%' or noteContent like '%' || :text || '%'") fun getNotesByString(text: String): List<Note> }

1.2 Repository 设定

kotlin
// repository/NoteRepository.kt class NoteRepository @Inject constructor(private val dao: NoteDAO) { // latestModifyTime 用于维护一个最新的修改时间 以确定是否需要和云端进行同步 private var latestModifyTime: LocalDateTime = LocalDateTime.now() // mutex 锁 保护上面的 latestModifyTime 的 private val mutex = Mutex() init { CoroutineScope(Dispatchers.IO).launch { dao.getLatestModifyTime()?.let { latestModifyTime = it.modifyTime } } } // 还没写的与云端同步的方法 suspend fun syncWithCloud() { } /** * insertNewNote * * 该方法会将 note 实例插入数据库 同时设置好实例的 id 值 */ suspend fun insertNewNotes(vararg notes: Note) { val ids = dao.insertNewNotes(*notes) ids.forEachIndexed { index, id -> notes[index].id = id } mutex.withLock { notes.forEach { note -> if (latestModifyTime.isAfter(note.modifyTime)) { latestModifyTime = note.modifyTime } } } } /** * updateNotes * * 更新传入的多个 note 实例 返回值用于指示是否全部更新成功 */ suspend fun updateNotes(vararg notes: Note): Boolean { if (dao.updateNotes(*notes) == notes.size) { mutex.withLock { notes.forEach { note -> if (latestModifyTime.isAfter(note.modifyTime)) { latestModifyTime = note.modifyTime } } } return true } else { return false } } /** * deleteNote * * 删除传入的 note 实例 返回值用于指示是否删除成功 */ suspend fun deleteNote(note: Note): Boolean { if (dao.deleteNotes(note) == 1) { mutex.withLock { latestModifyTime = LocalDateTime.now() } return true } else { return false } } suspend fun getAllNote(): List<Note> { return dao.getAllNote() } fun getNoteByIdInFlow(noteId: Int): Flow<Note?> { return dao.getNoteByIdInFlow(noteId) } fun getAllNoteInFlow(): Flow<List<Note>> { return dao.getAllNoteInFlow() } suspend fun getNotesByString(text: String): List<Note> { return dao.getNotesByString(text) } }

1.3 ViewModel 设计

kotlin
// /viewModel/NoteViewModel.kt @HiltViewModel class NoteViewModel @Inject constructor(private val repository: NoteRepository): ViewModel() { // 编辑页状态 var editNoteState by mutableStateOf<Note>(Note()) // 是否进入加载页面的状态 var isLoading by mutableStateOf(true) // 从数据库返回的 Flow 将其 collect 到一个状态中 该 Flow 启动的时候就会返回一次值 val notesFlow: StateFlow<List<Note>> = repository.getAllNoteInFlow() .map { notes -> notes.sortedByDescending { it.modifyTime } // 按时间倒序排序 } .stateIn(viewModelScope, SharingStarted.Lazily, emptyList()) // 上面这俩都是 // 主页面 搜索结果状态 var notesSearchResultState by mutableStateOf<List<Note>>(listOf()) fun insertNewNote(note: Note) { viewModelScope.launch { withContext(Dispatchers.IO) { repository.insertNewNotes(note) } } } fun updateNote(note: Note) { viewModelScope.launch { withContext(Dispatchers.IO) { repository.updateNotes(note) } } } fun deleteNote(note: Note) { viewModelScope.launch { withContext(Dispatchers.IO) { repository.deleteNote(note) } } } fun getNoteByIdInState(noteId: Int) { isLoading = true val flow = repository.getNoteByIdInFlow(noteId) viewModelScope.launch { withContext(Dispatchers.IO) { flow.take(1).collect { note -> if (note != null) { editNoteState = note } isLoading = false } } } } fun searchNoteByString(text: String) { viewModelScope.launch { withContext(Dispatchers.IO) { notesSearchResultState = repository.getNotesByString(text) } } } }

1.4 View 设计

kotlin
// view/NoteView.kt @Composable fun NoteView() { val navController = rememberNavController() // Note 内的 route 配置函数 NoteViewNavigatorGraphConfig(navController) } @OptIn(ExperimentalMaterial3Api::class) @Composable fun ShowNoteView( viewModel: NoteViewModel = hiltViewModel(), navController: NavController ) { // 搜索相关状态 val searchPopup = remember { mutableStateOf(false) } val searchKeyword = remember { mutableStateOf("") } val isShowSearchResult = remember { mutableStateOf(false) } // 显示所有的符合条件的 Note 相关状态 val notesFlowState by viewModel.notesFlow.collectAsStateWithLifecycle() val displayNotes = if (isShowSearchResult.value) viewModel.notesSearchResultState else notesFlowState // 删除相关状态 val deleteConfirmPopup = remember { mutableStateOf(false) } val deleteNoteIndex = remember { mutableIntStateOf(1) } // 显示 Note 详细信息相关状态 val isShowNoteDetail = remember { mutableStateOf(false) } val showNoteDetailIndex = remember { mutableIntStateOf(0) } Scaffold ( modifier = Modifier.fillMaxSize(), topBar = { TopAppBar( title = { Text(text = "Note") }, actions = { if (isShowSearchResult.value) { IconButton( onClick = { isShowSearchResult.value = false } ) { Icon(imageVector = Icons.Default.Clear, contentDescription = "Close Search Result") } } else { IconButton( onClick = { searchPopup.value = true } ) { Icon(imageVector = Icons.Default.Search, contentDescription = "Search Note") } } } ) }, floatingActionButton = { FloatingActionButton( onClick = { navController.navigate("editNote/-1") }, containerColor = MaterialTheme.colorScheme.primary, shape = CircleShape ) { Icon( imageVector = Icons.Default.Add, contentDescription = "Add New Note", ) } } ) { innerPadding -> if (isShowNoteDetail.value) { NoteItemDetail( displayNotes[showNoteDetailIndex.intValue].title, displayNotes[showNoteDetailIndex.intValue].content, displayNotes[showNoteDetailIndex.intValue].modifyTime, isShowNoteDetail ) } if (searchPopup.value) { AlertDialog ( modifier = Modifier.padding(innerPadding), onDismissRequest = { searchPopup.value = false }, icon = { Icon(imageVector = Icons.Default.Search, contentDescription = "Input Search Keyword to Search") }, title = { Text(text = "搜索") }, text = { Column { OutlinedTextField( value = searchKeyword.value, onValueChange = { searchKeyword.value = it }, label = { Text(text = "关键词") }, modifier = Modifier .fillMaxWidth() .padding(5.dp), singleLine = true, trailingIcon = { if (searchKeyword.value.isNotBlank()) { IconButton(onClick = { searchKeyword.value = "" }) { Icon(imageVector = Icons.Default.Clear, contentDescription = "Clear the Search Keyword TextField") } } }, ) } }, confirmButton = { IconButton( onClick = { viewModel.searchNoteByString(searchKeyword.value) searchPopup.value = false isShowSearchResult.value = true }, ) { Icon(imageVector = Icons.Default.Done, contentDescription = "Yes") } }, dismissButton = { IconButton( onClick = { searchPopup.value = false }, ) { Icon(imageVector = Icons.Default.Clear, contentDescription = "Cancel") } }, ) } if (deleteConfirmPopup.value) { AlertDialog ( modifier = Modifier.padding(innerPadding), onDismissRequest = { deleteConfirmPopup.value = false }, icon = { Icon(imageVector = Icons.Default.Info, contentDescription = "Confirm to Delete this Note") }, title = { Text(text = "删除") }, text = { Column { Text(text = "是否删除该Note?") Spacer(modifier = Modifier.height(10.dp)) NoteItemComponent( title = displayNotes[deleteNoteIndex.intValue].title, content = displayNotes[deleteNoteIndex.intValue].content, dateTime = displayNotes[deleteNoteIndex.intValue].modifyTime, isShowLongContent = false, isShowButton = false, onDelete = {}, onEdit = {}, onClick = {} ) } }, confirmButton = { IconButton( onClick = { viewModel.deleteNote(displayNotes[deleteNoteIndex.intValue]) deleteConfirmPopup.value = false }, ) { Icon(imageVector = Icons.Default.Done, contentDescription = "Yes") } }, dismissButton = { IconButton( onClick = { deleteConfirmPopup.value = false }, ) { Icon(imageVector = Icons.Default.Clear, contentDescription = "Cancel") } }, ) } if (displayNotes.isNotEmpty()) { LazyColumn ( modifier = Modifier.padding(innerPadding) ) { items(displayNotes.size) { index -> Row( modifier = Modifier .fillMaxWidth() .padding(10.dp) ) { NoteItemComponent( title = displayNotes[index].title, content = displayNotes[index].content, dateTime = displayNotes[index].modifyTime, isShowLongContent = true, isShowButton = true, onEdit = { navController.navigate("editNote/${displayNotes[index].id}") }, onDelete = { deleteNoteIndex.intValue = index deleteConfirmPopup.value = true }, onClick = { showNoteDetailIndex.intValue = index isShowNoteDetail.value = true } ) } } } } else { Box( modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center ) { Text( text = "无结果", style = TextStyle(fontSize = 24.sp), ) } } } } /** * EditNoteView * * 编辑/新建 Note 页面 当 noteIndex 为 -1 时是新建 Note */ @OptIn(ExperimentalMaterial3Api::class) @Composable fun EditNoteView( viewModel: NoteViewModel = hiltViewModel(), noteIndex: Int = -1, onBack: () -> Unit ) { // LaunchedEffect 函数用于从页面层启动一个协程 它在运行时会启动一次 并且在传入参数的值发生变化时再启动一次 LaunchedEffect(noteIndex) { if (noteIndex == -1) { viewModel.isLoading = false viewModel.editNoteState = Note() } else { viewModel.getNoteByIdInState(noteIndex) } } // 视图 if (viewModel.isLoading) { // 加载中 LoadingAnimation() } else { Scaffold ( modifier = Modifier.fillMaxSize(), topBar = { TopAppBar( title = { if (noteIndex == -1) { Text(text = "添加Note") } else { Text(text = "修改Note") } }, navigationIcon = { IconButton(onClick = { onBack.invoke() }) { Icon(imageVector = Icons.Default.Clear, contentDescription = "Back to another Page") } } ) }, floatingActionButton = { FloatingActionButton( onClick = { if (noteIndex == -1) { viewModel.insertNewNote(viewModel.editNoteState.copy(modifyTime = LocalDateTime.now())) } else { viewModel.updateNote(viewModel.editNoteState.copy(modifyTime = LocalDateTime.now())) } onBack.invoke() }, containerColor = MaterialTheme.colorScheme.primary, shape = CircleShape ) { Icon(imageVector = Icons.Default.Done, contentDescription = "Submit the File Info") } } ){ paddingValues -> Column(modifier = Modifier .fillMaxSize() .padding(paddingValues) .border(1.dp, Color.Unspecified, RoundedCornerShape(4.dp)) .padding(8.dp) ) { // 标题输入框 OutlinedTextField( value = viewModel.editNoteState.title, onValueChange = { viewModel.editNoteState = viewModel.editNoteState.copy(title = it) }, label = { Text(text = "标题") }, modifier = Modifier .fillMaxWidth() .padding(5.dp), trailingIcon = { if (viewModel.editNoteState.title.isNotBlank()) { IconButton(onClick = { viewModel.editNoteState = viewModel.editNoteState.copy(title = "") }) { Icon(imageVector = Icons.Default.Clear, contentDescription = "Clear the File Name TextField") } } }, singleLine = true ) // 内容输入框 OutlinedTextField( value = viewModel.editNoteState.content, onValueChange = { viewModel.editNoteState = viewModel.editNoteState.copy(content = it) }, label = { Text(text = "内容") }, modifier = Modifier .fillMaxWidth() .padding(5.dp) .height(500.dp) ) } } } }
kotlin
// view/Navigator.kt @Composable fun NoteViewNavigatorGraphConfig(navController: NavHostController) { NavHost( navController = navController, startDestination = "showNote", enterTransition = { slideInHorizontally(initialOffsetX = { 1000 }) + fadeIn() }, exitTransition = { slideOutHorizontally(targetOffsetX = { -1000 }) + fadeOut() }, popEnterTransition = { slideInHorizontally(initialOffsetX = { -1000 }) + fadeIn() }, popExitTransition = { slideOutHorizontally(targetOffsetX = { 1000 }) + fadeOut() } ) { composable("showNote") { ShowNoteView(navController = navController) } composable("editNote/{noteIndex}") { backStackEntry -> val noteIndex = backStackEntry.arguments?.getString("noteIndex")?.toIntOrNull() noteIndex?.let { EditNoteView(noteIndex = noteIndex) { navController.popBackStack() } } } } }
kotlin
// view/component/Note.kt // 主页面中显示所有 Note 的单个列表项 这个列表项应该能复用 @Composable fun NoteItemComponent( title: String, content: String, dateTime: LocalDateTime, isShowLongContent: Boolean, isShowButton: Boolean, onEdit: () -> Unit, onDelete: () -> Unit, onClick: () -> Unit, modifier: Modifier = Modifier ) { Card( modifier = modifier.fillMaxWidth(), onClick = onClick ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically, horizontalArrangement = Arrangement.SpaceBetween ) { Row( modifier = Modifier.weight(1f), verticalAlignment = Alignment.CenterVertically ) { Column( modifier = Modifier .fillMaxWidth() .padding(10.dp), horizontalAlignment = AbsoluteAlignment.Left ) { Row( modifier = Modifier .fillMaxWidth() .padding(10.dp), verticalAlignment = Alignment.CenterVertically ) { Text( modifier = Modifier.weight(1f), text = title, maxLines = 1, overflow = TextOverflow.Ellipsis, style = TextStyle(fontSize = 24.sp, fontWeight = FontWeight.Bold), ) Spacer(modifier = Modifier.width(5.dp)) Text( text = Util.localDateTimeToString(time = dateTime), style = TextStyle(fontSize = 12.sp) ) } Text( text = content, modifier = Modifier.padding(10.dp), maxLines = if (isShowLongContent) 10 else 3, overflow = TextOverflow.Ellipsis ) } } if (isShowButton) { Row { IconButton( modifier = Modifier, onClick = { onEdit.invoke() } ) { Icon( imageVector = Icons.Default.Edit, contentDescription = "Edit This Note" ) } IconButton( onClick = { onDelete.invoke() } ) { Icon( imageVector = Icons.Default.Delete, contentDescription = "Delete This Note" ) } } } } } } // 从下向上弹出的弹窗组件 @OptIn(ExperimentalMaterial3Api::class, ExperimentalFoundationApi::class) @Composable fun NoteItemDetail( title: String, content: String, dateTime: LocalDateTime, isShowState: MutableState<Boolean>, ) { // 注意 这个弹窗组件需要区分一下 它有三个状态:未展开 展开一半和完全展开 val sheetState = rememberModalBottomSheetState(skipPartiallyExpanded = false) val scope = rememberCoroutineScope() if (isShowState.value) { ModalBottomSheet( modifier = Modifier, onDismissRequest = { isShowState.value = false }, containerColor = MaterialTheme.colorScheme.primary, sheetState = sheetState ) { Column( modifier = Modifier .fillMaxSize() .padding(10.dp), horizontalAlignment = Alignment.Start ) { Row( modifier = Modifier.fillMaxWidth(), verticalAlignment = Alignment.CenterVertically ) { Text( modifier = Modifier .padding(10.dp) .weight(1f), text = title, style = TextStyle(fontSize = 30.sp, fontWeight = FontWeight.Bold), ) if (sheetState.currentValue == SheetValue.Expanded) { IconButton( onClick = { scope.launch { sheetState.hide() }.invokeOnCompletion { if (!sheetState.isVisible) { isShowState.value = false } } } ) { Icon( imageVector = Icons.Default.Close, contentDescription = "Close Note Item Detail" ) } } } Text( modifier = Modifier.padding(10.dp), text = Util.localDateTimeToString(time = dateTime), style = TextStyle(fontSize = 15.sp) ) Card( modifier = Modifier .fillMaxSize() .padding(10.dp) .verticalScroll(rememberScrollState()), shape = RoundedCornerShape(10.dp) ) { SelectionContainer { Text( text = content, modifier = Modifier.padding(10.dp), style = TextStyle(fontSize = 20.sp) ) } } } } } }

大体的框架就是差不多这样了,之后再写基本可以在这个框架的基础上再加东西就是了。

本文作者:御坂19327号

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!