实现搜索和标签管理功能
这次我们将逐步实现搜索和标签管理功能。以下是详细的实现步骤:
1. 数据模型
首先,扩展 Note 数据类以支持标签。
Note.kt
// Note.kt
package com.nemo.notes.model
import androidx.room.Entity
import androidx.room.PrimaryKey
import java.util.Date
@Entity(tableName = "notes")
data class Note(
@PrimaryKey(autoGenerate = true) val id: Long = 0,
val title: String,
val content: String,
val tags: List<String> = emptyList(), // 新增标签字段
val createdAt: Date = Date(),
val updatedAt: Date = Date()
)
2. 数据库访问对象 (DAO)
更新 NoteDao 接口以支持标签搜索。
NoteDao.kt
package com.nemo.notes.database
import androidx.room.Dao
import androidx.room.Delete
import androidx.room.Insert
import androidx.room.Query
import androidx.room.Update
import com.nemo.notes.model.Note
import kotlinx.coroutines.flow.Flow
@Dao
interface NoteDao {
@Query("SELECT * FROM notes ORDER BY updatedAt DESC")
fun getAllNotes(): Flow<List<Note>>
@Insert
suspend fun insert(note: Note): Long
@Update
suspend fun update(note: Note): Int
@Query("DELETE FROM notes WHERE id = :noteId")
suspend fun delete(noteId: Long): Int
@Delete
suspend fun delete(note: Note): Int
@Query("SELECT * FROM notes WHERE title LIKE :query OR content LIKE :query OR tags LIKE :query ORDER BY updatedAt DESC")
fun searchNotes(query: String): Flow<List<Note>>
}
数据库中读取 tags 字段。需要创建一个类型转换器来处理 List<String> 和支持的数据库类型(如 String)之间的转换。
Converters.kt
// Converters.kt
package com.nemo.notes.database
import androidx.room.TypeConverter
class Converters {
@TypeConverter
fun fromString(value: String): List<String> {
return value.split(",").map { it.trim() }
}
@TypeConverter
fun listToString(list: List<String>): String {
return list.joinToString(",")
}
}
在 AppDatabase 类中注册 Converters 类
**`NoteDatabase.kt
package com.nemo.notes.database
import android.content.Context
import androidx.room.Database
import androidx.room.Room
import androidx.room.RoomDatabase
import androidx.room.TypeConverters
import com.nemo.notes.model.Note
@Database(entities = [Note::class], version = 1, exportSchema = false)
@TypeConverters(DateConverter::class, Converters::class)
abstract class NoteDatabase : RoomDatabase() {
abstract fun noteDao(): NoteDao
companion object {
@Volatile
private var INSTANCE: NoteDatabase? = null
fun getDatabase(context: Context): NoteDatabase {
return INSTANCE ?: synchronized(this) {
val instance = Room.databaseBuilder(
context.applicationContext,
NoteDatabase::class.java,
"note_database"
).build()
INSTANCE = instance
instance
}
}
}
}
3. 仓库
更新 NoteRepository 类以支持搜索功能。
NoteRepository.kt
package com.nemo.notes.repository
import com.nemo.notes.database.NoteDao
import com.nemo.notes.model.Note
import kotlinx.coroutines.flow.Flow
import javax.inject.Inject
class NoteRepository @Inject constructor(
private val noteDao: NoteDao
) {
fun getAllNotes(): Flow<List<Note>> = noteDao.getAllNotes()
suspend fun insert(note: Note) = noteDao.insert(note)
suspend fun update(note: Note) = noteDao.update(note)
suspend fun delete(noteId: Long) = noteDao.delete(noteId)
// 新增搜索方法
fun searchNotes(query: String): Flow<List<Note>> = noteDao.searchNotes("%$query%")
}
4. ViewModel
更新 NoteViewModel 类以支持搜索功能。
NoteViewModel.kt
package com.nemo.notes.viewmodel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.nemo.notes.model.Note
import com.nemo.notes.repository.NoteRepository
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.launch
import javax.inject.Inject
@HiltViewModel
class NoteViewModel @Inject constructor(
private val repository: NoteRepository
) : ViewModel() {
val allNotes: Flow<List<Note>> = repository.getAllNotes()
// 新增搜索字段
private val searchQuery = MutableStateFlow("")
fun insert(note: Note) = viewModelScope.launch {
repository.insert(note)
}
fun update(note: Note) = viewModelScope.launch {
repository.update(note)
}
fun delete(noteId: Long) = viewModelScope.launch {
repository.delete(noteId)
}
// 新增搜索方法
val filteredNotes: Flow<List<Note>> = combine(allNotes, searchQuery) { notes, query ->
query to notes
}.flatMapLatest { (query, notes) ->
if (query.isEmpty()) {
flowOf(notes)
} else {
repository.searchNotes(query)
}
}
// 新增搜索方法
fun setSearchQuery(query: String) {
searchQuery.value = query
}
}
5. UI 界面
5.1 笔记列表界面
更新 NoteListScreen 以支持搜索功能。
NoteListScreen.kt
package com.nemo.notes.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.navigation.NavHostController
import com.nemo.notes.viewmodel.NoteViewModel
@Composable
fun NoteListScreen(navController: NavHostController, viewModel: NoteViewModel) {
// 使用 Hilt 注入 NoteViewModel val viewModel: NoteViewModel = hiltViewModel()
// 收集所有笔记的状态
//val notes by viewModel.allNotes.collectAsState(initial = emptyList())
val notes by viewModel.filteredNotes.collectAsState(initial = emptyList())
// 新增搜索字段
var searchQuery by remember { mutableStateOf("") }
Column(modifier = Modifier.padding(16.dp)) {
// 搜索框
TextField(
value = searchQuery,
onValueChange = {
searchQuery = it
viewModel.setSearchQuery(it)
},
label = { Text("Search") },
modifier = Modifier.fillMaxWidth()
)
// 添加间距
Spacer(modifier = Modifier.height(16.dp))
// 显示标题
Text(text = "My Notes", style = MaterialTheme.typography.headlineMedium)
// 显示笔记列表
LazyColumn {
items(notes) { note ->
// 每个笔记项使用 Card 显示
Card(
onClick = { navController.navigate("noteEdit/${note.id}") },
modifier = Modifier.padding(8.dp)
) {
Column(modifier = Modifier.padding(16.dp)) {
// 显示笔记标题
Text(text = note.title, style = MaterialTheme.typography.titleMedium)
// 显示笔记内容
Text(text = note.content, style = MaterialTheme.typography.bodyMedium)
// 显示笔记标签
if (note.tags.isNotEmpty()) {
Text(text = "Tags: ${note.tags.joinToString(", ")}", style = MaterialTheme.typography.bodySmall)
}
}
} } } // 添加间距
Spacer(modifier = Modifier.height(16.dp))
// 添加新笔记按钮
Button(
onClick = { navController.navigate("noteEdit/null") },
modifier = Modifier.fillMaxWidth()
) {
Text("Add New Note")
}
}}
5.2 笔记编辑界面
更新 NoteEditScreen 以支持标签管理。
NoteEditScreen.kt
package com.nemo.notes.ui
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextField
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.navigation.NavController
import com.nemo.notes.model.Note
import com.nemo.notes.viewmodel.NoteViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun NoteEditScreen(
navController: NavController,
viewModel: NoteViewModel,
noteId: Long?
) {
// 使用 remember 保存笔记状态
val note = remember { mutableStateOf(Note(title = "", content = "")) }
// 新增标签字段
var newTag by remember { mutableStateOf("") }
// 如果 noteId 不为空,加载对应的笔记
if (noteId != null) {
LaunchedEffect(noteId) {
viewModel.allNotes.collect { notes ->
notes.find { it.id == noteId }?.let { note.value = it }
} } }
Column(modifier = Modifier.padding(16.dp)) {
// 标题输入框
TextField(
value = note.value.title,
onValueChange = { note.value = note.value.copy(title = it) },
label = { Text("Title") },
modifier = Modifier.fillMaxWidth()
)
Spacer(modifier = Modifier.height(16.dp))
// 内容输入框
TextField(
value = note.value.content,
onValueChange = { note.value = note.value.copy(content = it) },
label = { Text("Content") },
modifier = Modifier.fillMaxWidth().height(200.dp)
)
Spacer(modifier = Modifier.height(16.dp))
// 新增标签输入框
Text(text = "Tags: ${note.value.tags.joinToString(", ")}", style = MaterialTheme.typography.bodySmall)
Row(modifier = Modifier.fillMaxWidth()) {
// 输入标签
TextField(
value = newTag,
onValueChange = { newTag = it },
label = { Text("Add Tag") },
modifier = Modifier.weight(1f)
)
// 添加标签按钮
Button(
onClick = {
if (newTag.isNotBlank()) {
note.value = note.value.copy(tags = note.value.tags + newTag)
newTag = ""
}
},
modifier = Modifier.padding(start = 8.dp)
) {
Text("Add")
}
} Spacer(modifier = Modifier.height(16.dp))
// 保存按钮
Button(
onClick = {
if (noteId == null) {
viewModel.insert(note.value)
} else {
viewModel.update(note.value.copy(id = noteId))
}
navController.popBackStack()
},
modifier = Modifier.fillMaxWidth()
) {
Text(if (noteId == null) "Create Note" else "Update Note")
}
}}
6. 运行项目
- 确保您的 Android 项目配置正确。
- 运行项目并测试搜索和标签管理功能。
项目代码参考地址:github.com/wxxzy/Notes…
通过以上步骤,您已经成功实现了搜索和标签管理功能。接下来,您可以继续实现云同步和备份功能。如果有任何问题或需要进一步的帮助,请随时告诉我!