Android 笔记 App4:实现搜索和标签管理功能

382 阅读2分钟

实现搜索和标签管理功能

这次我们将逐步实现搜索和标签管理功能。以下是详细的实现步骤:


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. 运行项目

  1. 确保您的 Android 项目配置正确。
  2. 运行项目并测试搜索和标签管理功能。

1.png

2.png

3.png

项目代码参考地址:github.com/wxxzy/Notes…

通过以上步骤,您已经成功实现了搜索和标签管理功能。接下来,您可以继续实现云同步和备份功能。如果有任何问题或需要进一步的帮助,请随时告诉我!