尧图网络科技YAOTU DIGITAL 获取报价
获取报价
首页 / 资讯中心 / 文章详情

从零构建安卓记事本应用:Room数据库与Jetpack Compose实战

发布时间:2026/9/4 8:07:21

资讯中心
01
ARTICLE

从零构建安卓记事本应用:Room数据库与Jetpack Compose实战

从零构建安卓记事本应用:Room数据库与Jetpack Compose实战
简介这是一份面向Android开发初学者与课程设计学生的Java语言记事本应用实战项目基于Android Studio完整实现用户登录注册、记事本增删改查及SQLite本地持久化存储功能覆盖移动应用开发核心流程。资源包共55个文件包含12个Java业务逻辑与Activity类、16个XML布局与配置文件、12个PNG图标资源、3个Gradle构建脚本及APK安装包、MP4演示视频、Word说明文档等总大小4.39MB结构清晰便于逐模块学习调试。已有9305人学习下载适合用于Android基础实训、课程大作业或毕业设计参考。用户可直接安装APK体验完整功能结合源码理解SQLite数据库操作、Activity生命周期管理与UI交互逻辑并通过运行文档快速搭建开发环境针对部分用户反馈解压异常问题资源已适配主流解压工具推荐使用WinRAR确保文件完整性。1. 项目概述从零构建一个安卓记事本应用最近在整理手机里的零散信息时发现很多临时想法、待办事项和重要信息都散落在各个聊天记录、截图和便签里找起来非常麻烦。市面上的笔记应用功能要么太复杂要么充斥着广告要么就是云端同步需要付费。作为一个有动手能力的开发者我萌生了自己动手做一个纯粹、轻量、完全掌控的安卓记事本应用的想法。这个项目的核心目标很明确打造一个功能完整、数据安全、界面简洁的本地记事本它应该能快速创建、编辑、删除和搜索笔记并且所有数据都存储在手机本地不依赖任何网络服务。这个项目非常适合有一定Java或Kotlin基础并希望深入理解安卓应用开发核心流程的开发者。通过它你不仅能实践Android Studio的使用、Activity与Fragment的生命周期管理、SQLite数据库操作、RecyclerView列表展示等核心技能还能深刻体会到从需求分析、UI设计、数据建模到功能实现、测试调试的完整项目闭环。整个过程就像搭积木从最基础的数据存储“地基”开始一层层构建起用户交互的“楼层”最终形成一个可用的产品。接下来我将详细拆解这个记事本应用的开发全过程分享其中的设计思路、关键技术实现以及我踩过的一些坑。2. 开发环境搭建与项目初始化2.1 Android Studio安装与基础配置工欲善其事必先利其器。开发安卓应用的首选工具就是Android Studio。直接从官网下载最新稳定版安装包即可安装过程基本是“下一步”到底。安装完成后首次启动会进行一些组件和SDK的下载这里需要保持网络通畅。一个关键的配置点是SDK Manager。我建议至少安装一个目前市场占有率较高的API级别例如API 33 Android 13的SDK Platform以及对应版本的System Image用于模拟器和Sources for Android SDK方便查看源码。注意国内网络环境下载SDK可能较慢或失败可以在Android Studio的设置中找到Appearance Behavior-System Settings-HTTP Proxy设置一个可用的代理镜像源例如将代理设置为mirrors.neusoft.edu.cn:80大连东软信息学院镜像站能显著提升下载速度。创建新项目时选择Empty Views Activity模板即可语言我选择了Kotlin因为它现在是安卓开发的官方首选语言语法更简洁安全。Minimum SDK的选择需要权衡版本太低无法使用新特性版本太高会限制应用安装范围。对于记事本这种工具类应用选择API 24: Android 7.0 (Nougat)是一个不错的平衡点既能使用现代API又能覆盖绝大多数仍在使用的设备。项目创建后建议第一时间在File-Settings-Editor-General-Auto Import中勾选Add unambiguous imports on the fly和Optimize imports on the fly这能自动管理import语句提升编码效率。2.2 项目结构规划与依赖引入一个清晰的项目结构是后续高效开发的基础。Android Studio默认的工程结构Android视图已经做了很好的归类。我们主要关注app模块下的几个关键目录manifests/: 存放AndroidManifest.xml这是应用的“身份证”声明了组件和权限。java/(或kotlin/): 存放所有的源代码按功能分包。例如我们可以创建com.yourname.notebook.ui存放Activity、Fragment、com.yourname.notebook.data存放数据库、实体类、仓库、com.yourname.notebook.adapter存放RecyclerView适配器等包。res/: 存放所有资源文件包括布局layout/、图片drawable/、字符串values/、样式values/等。对于记事本应用我们需要引入一些额外的库来简化开发。在app模块的build.gradle.kts(或build.gradle) 文件的dependencies块中添加。核心依赖包括Room用于SQLite数据库抽象、ViewModel和LiveData用于以生命周期感知的方式管理UI相关数据。以下是Kotlin DSL的配置示例dependencies { implementation(androidx.core:core-ktx:1.12.0) implementation(androidx.appcompat:appcompat:1.6.1) implementation(com.google.android.material:material:1.11.0) implementation(androidx.constraintlayout:constraintlayout:2.1.4) // Room for database val room_version 2.6.1 implementation(androidx.room:room-runtime:$room_version) kapt(androidx.room:room-compiler:$room_version) implementation(androidx.room:room-ktx:$room_version) // optional - Kotlin Extensions and Coroutines support // Lifecycle components implementation(androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0) implementation(androidx.lifecycle:lifecycle-livedata-ktx:2.7.0) implementation(androidx.lifecycle:lifecycle-common-java8:2.7.0) // Coroutines for asynchronous operations implementation(org.jetbrains.kotlinx:kotlinx-coroutines-android:1.7.3) testImplementation(junit:junit:4.13.2) androidTestImplementation(androidx.test.ext:junit:1.1.5) androidTestImplementation(androidx.test.espresso:espresso-core:3.5.1) }添加后同步项目Sync Now。使用Room而不是直接操作SQLiteDatabase可以让我们用面向对象的方式操作数据库编译器会检查SQL语句的正确性大大减少了样板代码和运行时错误。3. 数据层设计实体、DAO与数据库记事本应用的核心是数据因此我们先从数据层开始构建。采用Jetpack组件推荐的Repository模式数据层由Entity实体、DAO数据访问对象和Database数据库组成。3.1 定义数据实体Entity实体类对应数据库中的一张表。一个简单的笔记需要哪些字段我设计了id主键自增、title标题、content内容、createdTime创建时间和updatedTime最后修改时间。创建时间有助于回顾修改时间能快速定位最新编辑的笔记。在data包下创建Note.kt文件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, var title: String , var content: String , val createdTime: Date Date(), var updatedTime: Date Date() )Entity注解标明这是一个Room实体tableName指定表名。PrimaryKey定义主键autoGenerate true表示由数据库自动生成递增的ID。使用Date类型存储时间Room可以通过类型转换器TypeConverter将其存储为数据库能识别的格式如Long类型的时间戳。3.2 创建类型转换器TypeConverter由于Room不能直接存储Date对象我们需要定义一个转换器。在data包下创建Converters.ktimport androidx.room.TypeConverter import java.util.Date class Converters { TypeConverter fun fromTimestamp(value: Long?): Date? { return value?.let { Date(it) } } TypeConverter fun dateToTimestamp(date: Date?): Long? { return date?.time } }3.3 定义数据访问对象DAODAO是一个接口其中定义了访问数据库的各种方法增删改查。Room会在编译时为我们生成具体的实现。在data包下创建NoteDao.ktimport androidx.room.* import kotlinx.coroutines.flow.Flow Dao interface NoteDao { Query(SELECT * FROM notes ORDER BY updatedTime DESC) fun getAllNotes(): FlowListNote // 使用Flow当数据变化时自动通知观察者 Query(SELECT * FROM notes WHERE title LIKE % || :query || % OR content LIKE % || :query || % ORDER BY updatedTime DESC) fun searchNotes(query: String): FlowListNote Query(SELECT * FROM notes WHERE id :noteId) suspend fun getNoteById(noteId: Long): Note? Insert(onConflict OnConflictStrategy.REPLACE) suspend fun insertNote(note: Note): Long // 返回插入行的ID Update suspend fun updateNote(note: Note) Delete suspend fun deleteNote(note: Note) }这里有几个关键点FlowListNote:getAllNotes和searchNotes返回Flow类型。这是Kotlin协程中的冷流结合Room它可以实现数据库的实时观察。当笔记数据发生任何变化增删改时所有收集此Flow的UI都会自动收到更新后的列表无需手动刷新。这是构建响应式UI的基石。搜索查询:searchNotes方法使用了SQL的LIKE操作符和||连接符SQLite中用于字符串拼接实现了在标题和内容中进行模糊匹配。suspend函数: 除了返回Flow的查询其他操作数据库的函数都标记为suspend。这意味着它们必须在协程作用域内调用Room会确保这些耗时的IO操作在后台线程执行不会阻塞主线程。3.4 创建数据库Database最后我们将Entity、DAO和TypeConverter组合起来创建数据库实例。在data包下创建AppDatabase.ktimport androidx.room.Database import androidx.room.Room import androidx.room.RoomDatabase import androidx.room.TypeConverters import android.content.Context Database(entities [Note::class], version 1, exportSchema false) TypeConverters(Converters::class) abstract class AppDatabase : RoomDatabase() { abstract fun noteDao(): NoteDao companion object { Volatile private var INSTANCE: AppDatabase? null fun getDatabase(context: Context): AppDatabase { return INSTANCE ?: synchronized(this) { val instance Room.databaseBuilder( context.applicationContext, AppDatabase::class.java, notebook_database ).build() INSTANCE instance instance } } } }这里使用了单例模式来确保整个应用只有一个数据库实例避免重复打开数据库造成资源浪费和潜在错误。exportSchema false暂时关闭了架构导出对于简单项目可以简化配置。数据库文件将以notebook_database命名保存在设备的私有存储空间中。4. 仓库层与ViewModel连接数据与UI数据层准备好了我们需要一个中间层来协调数据访问并为UI提供易于观察的数据源。这就是Repository和ViewModel的职责。4.1 实现仓库RepositoryRepository封装了数据源这里就是Room数据库对上层提供统一的数据访问接口。在data包下创建NoteRepository.ktimport kotlinx.coroutines.flow.Flow class NoteRepository(private val noteDao: NoteDao) { val allNotes: FlowListNote noteDao.getAllNotes() fun searchNotes(query: String): FlowListNote noteDao.searchNotes(query) suspend fun getNoteById(id: Long): Note? noteDao.getNoteById(id) suspend fun insert(note: Note): Long noteDao.insertNote(note) suspend fun update(note: Note) noteDao.updateNote(note) suspend fun delete(note: Note) noteDao.deleteNote(note) }仓库的构造依赖于DAO。它几乎只是对DAO方法的一层薄封装但这样做的好处是如果未来数据源发生变化例如增加网络同步我们只需要修改Repository内部的实现而所有依赖Repository的ViewModel和UI代码都无需改动。4.2 创建笔记列表的ViewModelViewModel负责为UIActivity/Fragment准备和管理数据。它会在配置变更如屏幕旋转时存活避免数据丢失。在ui包下创建子包list然后创建NoteListViewModel.ktimport androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.launch class NoteListViewModel(private val repository: NoteRepository) : ViewModel() { // 私有可变的搜索词流 private val _searchQuery MutableStateFlow() // 公开只读的搜索词流 val searchQuery: StateFlowString _searchQuery // 笔记列表流结合搜索词和所有笔记流动态过滤 val notes: StateFlowListNote combine( repository.allNotes, _searchQuery ) { notes, query - if (query.isBlank()) { notes // 搜索词为空返回全部笔记 } else { notes.filter { note - note.title.contains(query, ignoreCase true) || note.content.contains(query, ignoreCase true) } } }.stateIn( scope viewModelScope, started SharingStarted.WhileSubscribed(5000), // 当有订阅者时启动最后一个订阅者离开5秒后停止 initialValue emptyList() ) // 更新搜索词 fun onSearchQueryChanged(query: String) { _searchQuery.value query } // 删除笔记 fun deleteNote(note: Note) { viewModelScope.launch { repository.delete(note) } } }这个ViewModel的设计有几个精妙之处响应式搜索没有在每次用户输入时都去查询数据库。而是使用combine操作符将“所有笔记流”和“搜索词流”结合起来。每当其中任何一个流发出新数据时combine块都会重新执行根据最新的搜索词对笔记列表进行内存过滤。这比频繁的数据库查询更高效。StateFlow的使用_searchQuery是私有的MutableStateFlow通过一个公开的只读StateFlow暴露给UI。notes也被转换为StateFlow确保UI总能拿到一个最新的、非空的列表状态初始值为空列表。StateFlow是热流会记住最后一个值新的订阅者会立即得到当前值。协程作用域所有可能耗时的操作如delete都包裹在viewModelScope.launch中确保在ViewModel销毁时自动取消避免内存泄漏。4.3 创建编辑笔记的ViewModel在ui包下创建子包edit然后创建NoteEditViewModel.kt用于处理单条笔记的创建和编辑。import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import kotlinx.coroutines.flow.* import kotlinx.coroutines.launch class NoteEditViewModel( private val repository: NoteRepository, savedStateHandle: SavedStateHandle // 用于获取导航传递的参数 ) : ViewModel() { private val noteId: Long savedStateHandle.getLong(noteId) ?: -1L // 用于UI绑定的状态 private val _title MutableStateFlow() val title: StateFlowString _title private val _content MutableStateFlow() val content: StateFlowString _content private val _isLoading MutableStateFlow(false) val isLoading: StateFlowBoolean _isLoading private val _saveSuccess MutableStateFlow(false) val saveSuccess: StateFlowBoolean _saveSuccess init { // 如果noteId有效则加载已有笔记 if (noteId 0) { loadNote() } } private fun loadNote() { viewModelScope.launch { _isLoading.value true val note repository.getNoteById(noteId) note?.let { _title.value it.title _content.value it.content } _isLoading.value false } } fun updateTitle(newTitle: String) { _title.value newTitle } fun updateContent(newContent: String) { _content.value newContent } fun saveNote() { val currentTitle _title.value val currentContent _content.value if (currentTitle.isBlank() currentContent.isBlank()) { // 标题和内容都为空不保存或可视为删除这里选择不保存 return } viewModelScope.launch { _isLoading.value true val note if (noteId 0) { // 更新现有笔记 Note(id noteId, title currentTitle, content currentContent, createdTime Date(), updatedTime Date()) } else { // 创建新笔记 Note(title currentTitle, content currentContent) } if (noteId 0) { repository.update(note) } else { repository.insert(note) } _isLoading.value false _saveSuccess.value true // 通知UI保存成功可用于导航回列表 } } }这个ViewModel处理了两种场景创建新笔记noteId为-1或0和编辑已有笔记noteId0。它通过SavedStateHandle获取从列表页面传递过来的笔记ID。saveNote方法包含了简单的验证逻辑防止保存空笔记。保存成功后通过_saveSuccess流发出信号UI可以监听此信号并做出反应例如关闭当前页面。5. UI层实现列表、编辑与搜索UI层我们使用Jetpack Compose来构建这是目前官方推荐的现代UI工具包声明式语法让UI代码更直观。如果你的项目仍在使用XML思路是相通的只是实现方式不同。5.1 依赖配置与主题设置首先确保在app/build.gradle.kts中启用了Compose并添加了必要依赖android { buildFeatures { compose true } composeOptions { kotlinCompilerExtensionVersion 1.5.7 } } dependencies { // Compose implementation(platform(androidx.compose:compose-bom:2024.02.00)) implementation(androidx.compose.ui:ui) implementation(androidx.compose.ui:ui-graphics) implementation(androidx.compose.ui:ui-tooling-preview) implementation(androidx.compose.material3:material3) implementation(androidx.lifecycle:lifecycle-runtime-compose:2.7.0) // 用于在Compose中收集Flow implementation(androidx.lifecycle:lifecycle-viewmodel-compose:2.7.0) // 在Compose中获取ViewModel implementation(androidx.navigation:navigation-compose:2.7.6) // 导航 androidTestImplementation(platform(androidx.compose:compose-bom:2024.02.00)) debugImplementation(androidx.compose.ui:ui-tooling) }然后修改MainActivity.kt设置Compose主题和导航入口import android.os.Bundle import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.ui.Modifier import com.yourname.notebook.ui.theme.NotebookTheme class MainActivity : ComponentActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContent { NotebookTheme { // 应用自定义主题由Android Studio自动生成 Surface( modifier Modifier.fillMaxSize(), color MaterialTheme.colorScheme.background ) { NotebookApp() // 应用的根Composable } } } } }5.2 实现导航与应用入口在ui包下创建NotebookApp.kt定义导航图import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.lifecycle.viewmodel.compose.viewModel import androidx.navigation.NavHostController import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController import com.yourname.notebook.data.AppDatabase import com.yourname.notebook.data.NoteRepository import com.yourname.notebook.ui.edit.NoteEditScreen import com.yourname.notebook.ui.edit.NoteEditViewModel import com.yourname.notebook.ui.list.NoteListScreen import com.yourname.notebook.ui.list.NoteListViewModel Composable fun NotebookApp() { val navController rememberNavController() // 依赖注入创建数据库、仓库并传递给ViewModel val database remember { AppDatabase.getDatabase(LocalContext.current) } val repository remember { NoteRepository(database.noteDao()) } NavHost(navController navController, startDestination noteList) { composable(noteList) { val viewModel: NoteListViewModel viewModel( factory NoteListViewModelFactory(repository) ) NoteListScreen( viewModel viewModel, onNoteClick { noteId - navController.navigate(noteEdit/$noteId) }, onAddNoteClick { navController.navigate(noteEdit/-1) // -1 表示新建 } ) } composable( route noteEdit/{noteId}, arguments listOf(navArgument(noteId) { type NavType.LongType }) ) { backStackEntry - val noteId backStackEntry.arguments?.getLong(noteId) ?: -1L val viewModel: NoteEditViewModel viewModel( factory NoteEditViewModelFactory(repository, noteId) ) NoteEditScreen( viewModel viewModel, onBack { navController.popBackStack() } ) } } }这里使用了Compose Navigation进行页面导航。我们定义了两个目的地笔记列表noteList和笔记编辑noteEdit/{noteId}。{noteId}是一个路径参数用于传递要编辑的笔记ID。viewModel()函数配合自定义的Factory来创建ViewModel并注入依赖Repository。remember用于在重组过程中保持数据库和仓库实例的单例性。5.3 实现笔记列表界面NoteListScreen在ui/list包下创建NoteListScreen.ktimport androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Add import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Search import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch OptIn(ExperimentalMaterial3Api::class) Composable fun NoteListScreen( viewModel: NoteListViewModel, onNoteClick: (Long) - Unit, onAddNoteClick: () - Unit ) { val notes by viewModel.notes.collectAsStateWithLifecycle() // 收集笔记列表状态 var searchQuery by remember { mutableStateOf() } val scope rememberCoroutineScope() // 监听搜索词变化并通知ViewModel LaunchedEffect(searchQuery) { viewModel.onSearchQueryChanged(searchQuery) } Scaffold( topBar { TopAppBar( title { Text(我的记事本) }, actions { // 搜索图标点击可展开搜索栏简化版实际可做更复杂交互 IconButton(onClick { /* 可在此处切换搜索栏显示状态 */ }) { Icon(Icons.Default.Search, contentDescription 搜索) } } ) }, floatingActionButton { FloatingActionButton(onClick onAddNoteClick) { Icon(Icons.Default.Add, contentDescription 添加笔记) } } ) { innerPadding - Column(modifier Modifier.padding(innerPadding)) { // 搜索框 OutlinedTextField( value searchQuery, onValueChange { searchQuery it }, modifier Modifier .fillMaxWidth() .padding(16.dp), label { Text(搜索标题或内容...) }, singleLine true ) if (notes.isEmpty()) { Box( modifier Modifier.fillMaxSize(), contentAlignment Alignment.Center ) { Text( text if (searchQuery.isNotBlank()) 未找到相关笔记 else 暂无笔记点击右下角按钮添加, style MaterialTheme.typography.bodyLarge ) } } else { LazyColumn(modifier Modifier.fillMaxSize()) { items( items notes, key { it.id } // 为每个item设置唯一key优化重组性能 ) { note - NoteListItem( note note, onNoteClick { onNoteClick(note.id) }, onDeleteClick { viewModel.deleteNote(note) } ) Divider() // 列表项之间的分割线 } } } } } } OptIn(ExperimentalMaterial3Api::class) Composable fun NoteListItem( note: Note, onNoteClick: () - Unit, onDeleteClick: () - Unit ) { Card( modifier Modifier .fillMaxWidth() .padding(horizontal 16.dp, vertical 8.dp), onClick onNoteClick ) { Row( modifier Modifier .fillMaxWidth() .padding(16.dp), horizontalArrangement Arrangement.SpaceBetween, verticalAlignment Alignment.CenterVertically ) { Column(modifier Modifier.weight(1f)) { Text( text note.title.ifBlank { (无标题) }, style MaterialTheme.typography.titleMedium, maxLines 1, overflow TextOverflow.Ellipsis ) Spacer(modifier Modifier.height(4.dp)) Text( text note.content.ifBlank { (无内容) }, style MaterialTheme.typography.bodyMedium, maxLines 2, overflow TextOverflow.Ellipsis, color MaterialTheme.colorScheme.onSurfaceVariant ) Spacer(modifier Modifier.height(4.dp)) Text( text 更新于${SimpleDateFormat(yyyy-MM-dd HH:mm).format(note.updatedTime)}, style MaterialTheme.typography.labelSmall, color MaterialTheme.colorScheme.outline ) } IconButton(onClick onDeleteClick) { Icon(Icons.Default.Delete, contentDescription 删除, tint MaterialTheme.colorScheme.error) } } } }列表界面主要包含一个顶栏、一个搜索框、一个笔记列表和一个悬浮添加按钮。使用LazyColumn高效渲染长列表。NoteListItem展示单条笔记的标题截断、内容预览和更新时间。删除按钮直接放在列表项上操作便捷但需谨慎最好可以增加确认对话框。5.4 实现笔记编辑界面NoteEditScreen在ui/edit包下创建NoteEditScreen.ktimport androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.filled.Save import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.focus.FocusRequester import androidx.compose.ui.focus.focusRequester import androidx.compose.ui.text.input.ImeAction import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.launch OptIn(ExperimentalMaterial3Api::class) Composable fun NoteEditScreen( viewModel: NoteEditViewModel, onBack: () - Unit ) { val title by viewModel.title.collectAsStateWithLifecycle() val content by viewModel.content.collectAsStateWithLifecycle() val isLoading by viewModel.isLoading.collectAsStateWithLifecycle() val saveSuccess by viewModel.saveSuccess.collectAsStateWithLifecycle() val scope rememberCoroutineScope() val focusRequester remember { FocusRequester() } // 监听保存成功信号成功后返回上一页 LaunchedEffect(saveSuccess) { if (saveSuccess) { onBack() } } Scaffold( topBar { TopAppBar( title { Text(if (viewModel.noteId 0) 编辑笔记 else 新建笔记) }, navigationIcon { IconButton(onClick onBack) { Icon(Icons.Default.ArrowBack, contentDescription 返回) } }, actions { IconButton( onClick { viewModel.saveNote() }, enabled !isLoading (title.isNotBlank() || content.isNotBlank()) ) { if (isLoading) { CircularProgressIndicator(modifier Modifier.size(24.dp)) } else { Icon(Icons.Default.Save, contentDescription 保存) } } } ) } ) { innerPadding - Column(modifier Modifier.padding(innerPadding)) { OutlinedTextField( value title, onValueChange { viewModel.updateTitle(it) }, modifier Modifier .fillMaxWidth() .padding(horizontal 16.dp, vertical 8.dp), label { Text(标题 (可选)) }, singleLine true, imeAction ImeAction.Next, // 自动聚焦到标题栏新建时 focusRequester focusRequester ) OutlinedTextField( value content, onValueChange { viewModel.updateContent(it) }, modifier Modifier .fillMaxWidth() .weight(1f) .padding(horizontal 16.dp, vertical 8.dp), label { Text(内容) }, singleLine false, maxLines Int.MAX_VALUE, // 允许多行 textStyle LocalTextStyle.current.copy(lineHeight TextStyle.Default.lineHeight * 1.2) // 增加行高 ) } } // 自动聚焦逻辑仅在新笔记时 if (viewModel.noteId 0) { LaunchedEffect(Unit) { focusRequester.requestFocus() } } }编辑界面相对简单包含一个返回按钮、一个保存按钮在加载时显示进度条、一个标题输入框和一个内容输入框。内容框使用maxLines Int.MAX_VALUE使其可以无限扩展Modifier.weight(1f)让它占据剩余的所有垂直空间。通过LaunchedEffect监听saveSuccess一旦保存成功就自动返回列表页提供了流畅的用户体验。自动聚焦功能提升了新建笔记时的操作效率。6. 功能增强、测试与优化基础功能完成后一个可用的记事本应用已经成型。但要让其更健壮、更好用还需要进行功能增强和优化。6.1 添加笔记排序与筛选用户可能希望按创建时间、更新时间或字母顺序查看笔记。我们可以在列表ViewModel和界面中增加排序功能。首先在NoteListViewModel中增加排序状态和逻辑// 在NoteListViewModel中 sealed class SortOrder { object ByUpdatedTimeDesc : SortOrder() // 按更新时间降序默认 object ByUpdatedTimeAsc : SortOrder() object ByCreatedTimeDesc : SortOrder() object ByTitleAsc : SortOrder() } private val _sortOrder MutableStateFlowSortOrder(SortOrder.ByUpdatedTimeDesc) val sortOrder: StateFlowSortOrder _sortOrder fun updateSortOrder(newOrder: SortOrder) { _sortOrder.value newOrder } // 修改notes流的combine逻辑加入排序 val notes: StateFlowListNote combine( repository.allNotes, _searchQuery, _sortOrder ) { notes, query, order - var filteredNotes if (query.isBlank()) notes else { notes.filter { it.title.contains(query, ignoreCase true) || it.content.contains(query, ignoreCase true) } } // 根据排序规则排序 filteredNotes when (order) { is SortOrder.ByUpdatedTimeDesc - filteredNotes.sortedByDescending { it.updatedTime } is SortOrder.ByUpdatedTimeAsc - filteredNotes.sortedBy { it.updatedTime } is SortOrder.ByCreatedTimeDesc - filteredNotes.sortedByDescending { it.createdTime } is SortOrder.ByTitleAsc - filteredNotes.sortedBy { it.title.lowercase() } else - filteredNotes } filteredNotes }.stateIn(...)然后在NoteListScreen的TopBar的actions中添加一个下拉菜单用于选择排序方式。6.2 数据备份与恢复简易版对于本地应用数据备份至关重要。一个简单的方法是定期将数据库文件复制到应用的“外部存储”私有目录或用户选择的目录需要请求存储权限。可以使用WorkManager定期执行备份任务。更用户友好的方式是提供“导出为文件”和“从文件导入”的功能将笔记列表导出为JSON或CSV格式。6.3 单元测试与UI测试单元测试为Repository和ViewModel编写单元测试确保业务逻辑正确。使用androidx.arch.core:core-testing来测试ViewModel和LiveData/StateFlow使用Room的内存数据库Room.inMemoryDatabaseBuilder来测试Repository避免污染真实数据。UI测试使用Espresso对于View系统或Compose UI Test对于Compose来模拟用户操作验证界面交互是否正确。例如测试添加笔记、编辑内容、搜索、删除等流程。6.4 性能优化与常见问题排查数据库查询优化确保Query中的SQL语句高效。为经常用于搜索和排序的列如title,updatedTime建立索引。在Note实体类中可以通过ColumnInfo(index true)注解或直接在DAO的Query中使用CREATE INDEX语句需在数据库升级中处理。Entity(tableName notes, indices [Index(value [updatedTime], unique false)]) data class Note(...)列表性能在NoteListItem中使用Modifier的clickable而非Card的onClick可能会在某些情况下有细微性能差异但通常可忽略。确保LazyColumn的items提供了稳定的key这是避免不必要的重组和动画错误的关键。内存泄漏预防在Compose中收集Flow时使用collectAsStateWithLifecycle()需要添加androidx.lifecycle:lifecycle-runtime-compose依赖而不是普通的collectAsState()。这能确保在应用进入后台时停止收集节省资源。在ViewModel中使用viewModelScope启动协程它会自动在ViewModel清除时取消。常见编译/运行时问题“Cannot access database on the main thread”: 确保所有Room的suspend函数都在协程中调用或者使用allowMainThreadQueries()仅用于调试生产环境禁用。“TypeConverter not found”: 检查TypeConverters注解是否正确添加到Database类上并且转换器方法是public的。Compose预览无法渲染如果预览报错找不到ViewModel可以在Preview函数中提供默认值或使用PreviewParameter。对于依赖真实数据库的Composable预览可能比较复杂可以考虑将数据依赖抽象出来在预览中注入假数据。应用图标与名称在res/mipmap-*目录下替换应用图标在res/values/strings.xml中修改app_name。在AndroidManifest.xml中检查android:icon和android:label属性是否正确引用了这些资源。开发这样一个完整的应用从设计到实现会遇到各种预料之外的问题。我的经验是每当遇到一个报错首先仔细阅读错误信息它通常已经指明了方向。其次善用Android Studio的Logcat查看详细堆栈并使用断点调试功能逐步执行代码观察变量状态。最后官方文档developer.android.com和社区如Stack Overflow是解决问题最可靠的资源。这个记事本项目麻雀虽小五脏俱全涵盖了现代安卓开发的主要技术点是巩固知识、练习架构思维的绝佳练手项目。本文还有配套的精品资源点击获取
02
RELATED NEWS

相关资讯

更多网站建设与数字化升级内容

03
WHY YAOTU

想打造同款高转化官网?

懂行业、懂生意,从建站到增长一站式陪跑

场景化定制

不做模板站,围绕你的业务场景量身设计,小众不撞款。

营销型架构

以转化目标组织内容与路径,让官网真正带来询盘。

全周期服务

设计、开发、运营、运维一体,上线只是开始。

免费获取你的建站方案

留下需求,专属顾问 24 小时内为你输出方案建议。