主要内容:
Note 相关的功能主要效果大致上就是一个临时的笔记功能。它需要一个编辑页面,一个展示页面。
为了使 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>
}
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)
}
}
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)
}
}
}
}
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 许可协议。转载请注明出处!