ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Android备忘录开发实战:Kotlin+Room+RecyclerView

Android备忘录开发实战:Kotlin+Room+RecyclerView 1. 备忘录这个小题目为什么值得当正经Android项目做一遍如果你刚接触Android项目开发拿一个备忘录App来练手是最不容易走偏的选择。它的需求边界清晰——记录、查看、修改、删除——但真正动手写起来你会发现它把Android开发里最核心的几块骨头都串在了一根绳子上界面布局、列表复用、数据持久化、页面跳转、生命周期、打包签名一个都不少。很多人做过的第一个能跑起来的App就是它但真正能做到敢给朋友装着用的比例并不高差别就藏在那些看起来不起眼的细节里。我在带过几个初学的朋友之后发现一个规律大家都能在Android Studio里拖出一个带EditText和Button的界面点一下把文字塞进列表然后兴高采烈地截个图。可一旦退出App再进来数据没了屏幕一转输入框里的字消失了连着加几十条记录滚动开始一顿一顿的。这些问题不解决备忘录就只是个玩具而这几个问题的解决过程恰好覆盖了从入门到能独立完成一个App的全部关键节点。这篇内容面向几类人完全没写过Android、想跟着做一个完整项目的初学者写过一点但数据一关App就丢、想搞清楚持久化到底该怎么做的进阶者以及想拿一个简单App去熟悉Android Studio、Gradle、签名打包这条完整链路的同学。我会用一个真实可复现的技术路线把备忘录从零到能装到真机上使用的过程讲透中间涉及的关键取舍我都会说清楚为什么这么选而不是甩一堆代码让你照抄。我打算用的技术栈是Kotlin XML布局 Room RecyclerView这一套。理由后面会展开先给个结论这套组合是当前Android原生开发里文档最全、踩坑最少、对新版本兼容最稳的方案对新手最友好。后面所有代码和步骤都基于这套你可以一边看一边在Android Studio里对照操作。1.1 一个能用的备忘录到底该覆盖哪些能力我们先把需求列清楚避免写到一半发现漏了功能回头返工。备忘录的核心动作其实就四个字增、删、改、查。但落到具体交互上至少要满足下面这些新增一条记录记录至少包含标题、正文、创建时间、最后修改时间。列表页按最后修改时间倒序展示所有记录能看到标题和内容摘要。点某条记录进入编辑页能修改并保存。长按或侧滑删除记录删除后列表立刻刷新。关闭App再打开数据依然在。屏幕旋转、切到后台再回来当前正在编辑的内容不丢。这六条看着简单但每一条背后都对应一个具体的技术点。比如数据依然在直接决定你必须用数据库或者文件存储不能用内存变量旋转不丢逼着你去理解Activity生命周期和状态保存列表刷新则牵扯到RecyclerView的Adapter通知机制。我见过太多人卡在删除之后列表没变这种问题上根因就是对Adapter的刷新方式理解不到位。还有几个可以加、但不必一开始就做的加分项搜索过滤、置顶、背景色标记、字数统计。这些留到主流程跑通之后再加会让项目有成长空间也能让你在已有代码上练习修改而不是每次重头来。1.2 技术选型的几个岔路口我为什么这么选做Android项目你会遇到几个经典的二选一这里把理由讲清楚。第一个岔路口是语言Kotlin还是Java。十年前大家清一色Java现在官方主推Kotlin新建工程默认就是Kotlin。我更推荐Kotlin它的空安全特性能帮你挡掉一大批NullPointerException代码也更短。当然如果你所在团队全是Java那用Java也完全没问题本文的思路是通用的代码逻辑一一对应。第二个岔路口是界面方案传统的XML View体系还是Jetpack Compose。Compose是趋势写起来确实爽但对于第一次做完整项目的人来说我反而不建议一上来就用它。原因很实际你会在网上搜到大量基于XML的教程和问答存量资料多遇到问题好查XML的布局层级、视图树这些概念理解之后对排查界面问题很有帮助而且Compose和传统View混用时容易出问题新手很难判断是哪里错了。所以这篇走XML路线Compose留给你做完第一版之后再去尝试。第三个岔路口是数据存储SharedPreferences、文件、还是数据库。SharedPreferences适合存设置项这种零散的键值对比如主题、语言它不适合存列表数据因为它的设计就不是干这个的。文件存储要自己处理序列化和并发麻烦。数据库里原生SQLite能用但写起来啰嗦Room是Google在SQLite之上封装的ORM框架用注解就能定义表结构编译期就能检查SQL错误是最优解。这篇用Room。把这三个选择定下来项目的地基就稳了。后面所有内容都围绕这套地基展开你可以放心照着往下走。2. 从Android Studio装好到工程骨架搭起来动手写代码之前环境这一步能卡住不少人而且卡住的原因往往很隐蔽——不是你不会装而是某个版本没对上、某个SDK没勾选导致工程一编译就报一堆红。我先把这块最容易出问题的地方说透。2.1 安装Android Studio时那些默认勾选之外的东西从官网下载Android Studio安装过程本身没什么难度一路下一步即可。真正需要注意的是安装向导里那个SDK Components的勾选页面以及第一次启动后SDK Manager里的配置。我的建议是SDK Platforms里至少保留你targetSdk对应的那个版本外加一个稍低的版本用于测试兼容SDK Tools里重点确认下面几项都装了——Android SDK Build-Tools、Android SDK Platform-Tools里面有adb真机调试全靠它、Android Emulator、Android SDK Command-line Tools。最后这一项很多人会漏导致后面想在命令行跑gradlew时各种找不到命令。关于国内下载慢的问题你可以在SDK Manager里看到每个组件的位置如果某次下载一直转圈多半是网络问题换个时间段或者检查代理设置通常能解决。注意这里的代理指的是你本地开发环境的网络配置和任何特殊的上网手段无关仅仅是连接官方资源时的常规网络设置。还有一点Android Studio现在对系统内存要求不低8G内存的机器跑模拟器会比较吃力有条件上16G。如果实在吃力用真机调试是个好选择后面第6节会讲怎么用adb连真机。2.2 GradleAnrdoid项目里最容易劝退的一环Gradle是Android的构建工具负责把源码、资源、依赖库打包成APK。新手遇到的第一波红色报错八成来自Gradle。它有两个层面的版本要区分一个是Gradle本身的版本在gradle/wrapper/gradle-wrapper.properties里一个是Android Gradle PluginAGP的版本在项目根目录build.gradle里。这两个版本是配套的不匹配就会报错。我一般这么配新建工程时用Android Studio默认给的组合不要手贱去改。因为默认组合是官方测试过能跑通的你改一个数字就可能触发连锁问题。等工程能正常跑起来再去研究升级。依赖库统一写在模块级的build.gradle.kts新版或者build.gradle旧版里。备忘录项目需要引入的依赖主要有// 模块级 build.gradle.kts 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) implementation(androidx.recyclerview:recyclerview:1.3.2) // Room 三件套 implementation(androidx.room:room-runtime:2.6.1) implementation(androidx.room:room-ktx:2.6.1) kapt(androidx.room:room-compiler:2.6.1) }注意room-compiler用的是kapt而不是implementation因为它是个注解处理器只在编译期用不会打进最终的APK。如果你搜到的教程里写的是annotationProcessor那是Java项目的写法Kotlin项目要用kapt。另外用kapt的话还要确保顶部应用了kotlin-kapt插件否则compile会失败提示找不到注解处理器。这个点很隐蔽我第一次踩的时候排查了半小时。提示如果Gradle同步时一直停在Downloading或者报网络相关的错优先检查是否开启了离线模式File → Settings → Build → Gradle → Offline work关掉它再同步。2.3 包结构怎么分决定了你后期改代码顺不顺手很多新手把所有文件都堆在一个包下面写着写着就找不着北了。备忘录虽然小也建议按职责分包养成习惯com.example.memo/ ├── data/ // 数据层 │ ├── Note.kt // 实体类 │ ├── NoteDao.kt // 数据访问接口 │ └── NoteDatabase.kt// 数据库 ├── ui/ // 界面层 │ ├── list/ │ │ ├── MainActivity.kt │ │ └── NoteAdapter.kt │ └── edit/ │ └── EditActivity.kt └── util/ // 工具类 └── TimeUtils.kt这样的分层不是形式主义。当你要改数据库字段时你清楚只该动data包要改界面样式时只动ui包。职责边界清晰出问题时排查范围就小。这是我在多个项目里验证过的经验分包分得清楚的项目加新功能的速度明显快过一锅炖的项目。3. 数据层用Room把记一条这件事做扎实数据层是整个备忘录的地基它决定了你的记录能不能可靠地保存下来。这一节我会把Room的三段式结构讲清楚并解释每个部分为什么这么写。3.1 为什么不该用SharedPreferences或内存变量存记录先说内存变量。很多人的第一版是这么写的在Activity里定义一个val noteList mutableListOfString()添加就往里塞。问题在于Activity被销毁比如切后台后被系统回收、旋转屏幕时这个List就没了数据全丢。这是新手最常见的数据丢失根因。再说SharedPreferences。它确实能持久化但它是为少量键值对设计的底层是一个XML文件。你想存一个列表就得把整个List序列化成JSON字符串再存进去读的时候反序列化。数据量小还凑合一旦记录多了每次读写都要处理整个字符串效率低而且并发写容易出问题。它适合存上次排序方式这种设置项不适合当记录仓库。数据库是正经方案。SQLite是Android内置的轻量数据库但直接用它的API要写很多样板代码还容易拼错SQL。Room在SQLite之上做了一层封装你用注解声明表结构和查询其余交给它生成编译期就能发现SQL写错了。对备忘录这种结构化数据、需要增删改查的场景Room是天然对口。3.2 实体、DAO、数据库三段式代码的具体写法Room的核心是三个东西我把它们一次写全你对照理解。第一部分是实体类Entity它对应数据库里的一张表。每个属性对应一个字段。Entity(tableName notes) data class Note( PrimaryKey(autoGenerate true) val id: Long 0, ColumnInfo(name title) val title: String, ColumnInfo(name content) val content: String, ColumnInfo(name created_at) val createdAt: Long, ColumnInfo(name updated_at) val updatedAt: Long )这里几个细节值得说。PrimaryKey(autoGenerate true)让主键自增你不用手动管理id。id的默认值给0是因为插入新记录时Room会自动分配。时间字段我用的是Long类型的时间戳而不是格式化后的字符串原因是时间戳排序方便、时区处理简单展示的时候再转成2024-06-01 14:30这种格式转换逻辑放在util里。如果你用字符串存时间按字母序排序勉强能用但一旦格式不统一就会乱。第二部分是DAOData Access Object声明你对数据的操作。Dao interface NoteDao { Query(SELECT * FROM notes ORDER BY updated_at DESC) fun getAll(): FlowListNote Query(SELECT * FROM notes WHERE id :id) suspend fun getById(id: Long): Note? Insert(onConflict OnConflictStrategy.REPLACE) suspend fun insert(note: Note): Long Update suspend fun update(note: Note) Delete suspend fun delete(note: Note) Query(DELETE FROM notes WHERE id :id) suspend fun deleteById(id: Long) }getAll()返回FlowListNote是这套方案的一个关键点。Flow是Kotlin协程里的数据流数据库里数据一变它就会自动发射新值配合界面层收集列表就自动刷新了你不用手动去调刷新列表。这比传统做法查完手动notifyDataSetChanged优雅得多也不容易漏。insert、update、delete这些返回Unit或Long的方法为什么加suspend因为它们要读写磁盘是耗时操作标成挂起函数就会强制你在协程里调用避免你在主线程直接跑导致界面卡顿甚至ANR。这个设计是在帮你避坑理解这一点很重要。第三部分是数据库类把实体和DAO串起来。Database(entities [Note::class], version 1, exportSchema false) abstract class NoteDatabase : RoomDatabase() { abstract fun noteDao(): NoteDao companion object { Volatile private var INSTANCE: NoteDatabase? null fun getInstance(context: Context): NoteDatabase { return INSTANCE ?: synchronized(this) { INSTANCE ?: Room.databaseBuilder( context.applicationContext, NoteDatabase::class.java, memo.db ).build().also { INSTANCE it } } } } }这里的单例写法双检锁是为了保证整个App只创建一个数据库实例。数据库连接是重资源创建多个实例会浪费内存还可能引发并发问题。用applicationContext而不是Activity的context是为了避免内存泄漏——如果持有Activity的contextActivity销毁了数据库还握着它就泄漏了。这两个细节都是过来人的经验值得背下来。3.3 数据库版本升级现在就给未来留条后路Database注解里的version 1现在没问题但当你想给备忘录加个是否置顶字段时就得把version改成2。这时候如果你不做处理用户更新App后一运行就崩日志里会提示Migration not found。正确的做法是提供迁移脚本val MIGRATION_1_2 object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL(ALTER TABLE notes ADD COLUMN pinned INTEGER NOT NULL DEFAULT 0) } } // 在builder链上加 .addMigrations(MIGRATION_1_2)迁移脚本的要点是只增不减不轻易改类型加字段给默认值这样老数据不会丢。有些教程图省事教你用fallbackToDestructiveMigration()意思是升级时直接删库重建。这在练手项目里能用但你要是真给朋友用人家记的东西全没了后果很严重。我强烈建议从一开始就养成写迁移脚本的习惯。4. 列表页与编辑页交互细节决定手感数据层铺好了接下来是用户能看到、能点的部分。备忘录的界面不复杂但交互上的小心思能大幅提升使用体验。4.1 RecyclerView适配器ViewHolder复用的原理和写法列表我用RecyclerView。它比老的ListView强在强制你使用ViewHolder模式。所谓复用是指屏幕上只创建能显示的那几个item视图滚动时把滑出屏幕的视图回收重新绑定新数据来用。这样即便有一千条记录内存里也就那么几个视图对象滚动才流畅。理解了这一点你就明白为什么不应该在onBindViewHolder里做耗时操作——它会被频繁调用。适配器写法class NoteAdapter( private val onItemClick: (Note) - Unit, private val onItemLongClick: (Note) - Unit ) : ListAdapterNote, NoteAdapter.NoteViewHolder(DIFF_CALLBACK) { class NoteViewHolder(val binding: ItemNoteBinding) : RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): NoteViewHolder { val binding ItemNoteBinding.inflate( LayoutInflater.from(parent.context), parent, false ) return NoteViewHolder(binding) } override fun onBindViewHolder(holder: NoteViewHolder, position: Int) { val note getItem(position) holder.binding.tvTitle.text note.title holder.binding.tvContent.text note.content holder.binding.tvTime.text TimeUtils.format(note.updatedAt) holder.binding.root.setOnClickListener { onItemClick(note) } holder.binding.root.setOnLongClickListener { onItemLongClick(note); true } } companion object { private val DIFF_CALLBACK object : DiffUtil.ItemCallbackNote() { override fun areItemsTheSame(oldItem: Note, newItem: Note) oldItem.id newItem.id override fun areContentsTheSame(oldItem: Note, newItem: Note) oldItem newItem } } }我用的是ListAdapter而不是基础的RecyclerView.Adapter。区别在于ListAdapter内置了DiffUtil你只要提供比较规则它就能算出新旧列表的差异只刷新变化的那几项。基础的Adapter你调notifyDataSetChanged()是全量刷新整屏都会重绘数据多的时候能明显感觉到卡。areItemsTheSame判断是不是同一条记录比idareContentsTheSame判断内容有没有变比整个对象。这两个方法写对删除和修改后的刷新就自动且高效了。4.2 新增和编辑复用同一个页面一个常见的偷懒做法是新增和编辑各写一个Activity。但它们的界面几乎一样都是标题输入框加正文输入框各写一个会有大量重复代码改一处要改两遍。更好的做法是复用同一个EditActivity通过Intent传参区分模式。// 新增 startActivity(Intent(this, EditActivity::class.java)) // 编辑 startActivity(Intent(this, EditActivity::class.java).apply { putExtra(note_id, note.id) })在EditActivity里读note_id如果传了就查出来填充输入框标题栏显示编辑没传就是新建保存时插入。这套模式的好处是显而易见的界面逻辑只有一份维护成本低。这个思路在很多地方都能用上——判断是不是同一个东西的不同状态能复用就复用。保存逻辑要注意自动填充时间val now System.currentTimeMillis() if (isEdit) { val updated oldNote.copy( title titleInput, content contentInput, updatedAt now ) dao.update(updated) } else { dao.insert(Note(title titleInput, content contentInput, createdAt now, updatedAt now)) }编辑时我只改updatedAt不动createdAt这样列表按修改时间排序后刚编辑过的记录会跳到最上面符合使用直觉。4.3 删除、撤销与长按菜单的交互删除是最容易做得让人后悔的操作。我的建议是长按弹出确认对话框而不是按下就删。别小看这个确认步骤它能救回很多手滑。更贴心一点的做法是用Snackbar做撤销。点击确认删除后记录先被移除底部弹出一个已删除撤销的提示几秒内点撤销就把数据恢复。这个体验比单纯的确认对话框更友好因为确认对话框是防错撤销是容错。实现上删除时把被删的Note对象暂存起来撤销时重新insert回去就行因为id还在insert时要处理好主键冲突。长按还可以弹出上下文菜单提供置顶复制内容分享等选项。这些都可以在跑通主流程之后再逐步加不要一次做完容易乱。5. 生命周期、线程和兼容性真实项目里绕不开的坎主流程跑通之后真正拉开差距的是对边界情况的处理。这一节讲的几个坑几乎每个做过Android的人都踩过我把排查思路完整还原出来。5.1 旋转屏幕内容就没了一次完整的排查过程现象在EditActivity里写了一半把手机横过来输入框清空了。排查第一步我打开Android Studio的Logcat在EditActivity的onCreate、onSaveInstanceState、onDestroy里各打一条日志。旋转屏幕后看到日志依次输出onDestroy再onCreate——这就说明白问题了默认情况下屏幕旋转会导致Activity销毁重建重新走一遍生命周期成员变量自然回到初始值。知道了根因方案就清晰了。有两种思路。第一种是用onSaveInstanceState保存当前输入内容重建后在onCreate里读回来override fun onSaveInstanceState(outState: Bundle) { super.onSaveInstanceState(outState) outState.putString(draft_title, binding.etTitle.text.toString()) outState.putString(draft_content, binding.etContent.text.toString()) } override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) // ... savedInstanceState?.let { binding.etTitle.setText(it.getString(draft_title)) binding.etContent.setText(it.getString(draft_content)) } }第二种思路是禁止旋转在AndroidManifest里给Activity加android:screenOrientationportrait。这是一种回避虽然简单但用户体验上不如支持横屏。我的建议是备忘录这种App还是支持旋转用第一种方案处理。顺带说一句onSaveInstanceState能被系统调用的前提是这个Activity在将要被销毁但可能恢复的状态比如旋转、切后台。如果用户主动按返回键退出这个回调不会被调用这符合预期——主动退出意味着放弃草稿。5.2 主线程里碰数据库卡顿和ANR的元凶现象记录加到几百条以后打开列表页会白屏一瞬间滑动也偶尔卡住。排查思路是看有没有在主线程做耗时操作。我检查列表页的加载代码发现有人是这样写的在onCreate里直接调dao.getAll()然后赋值给列表。虽然Room的方法我标了suspend但如果用错了地方比如用runBlocking强行在主线程同步等待就等于把磁盘读的耗时又拉回了主线程。Android的主线程UI线程负责处理界面绘制和用户输入一旦被阻塞超过约5秒系统就会弹ANR应用无响应对话框。数据库读写、网络请求、大文件处理这些都必须放到子线程。用Kotlin协程的话正确姿势是这样lifecycleScope.launch { repeatOnLifecycle(Lifecycle.State.STARTED) { dao.getAll().collect { notes - adapter.submitList(notes) } } }lifecycleScope绑定Activity生命周期Activity销毁时协程自动取消不会泄漏。repeatOnLifecycle(STARTED)保证界面不可见时停止收集可见时再恢复这是官方推荐的收集Flow的标准写法。别小看这两行它解决的是界面销毁后还在更新UI导致崩溃这类隐蔽问题。5.3 老设备上的兼容别假设所有手机都一样你会在自己的新手机上跑得飞快但用户手里的设备五花八门。几个常见的兼容问题深色模式如果不做适配用户在深色模式下打开你的App白底黑字可能变成白底白字看不见。用主题里定义的?attr/colorOnSurface这类颜色属性别写死#000000。刘海屏和安全区域顶部内容可能被刘海挡住。用WindowInsets处理或者用fitsSystemWindows。权限差异备忘录本身通常不需要危险权限但如果你想加导出为文件功能就得处理不同Android版本的存储权限差异。存储权限在新版本上变化很大这块要单独查对应版本的文档。我个人的习惯是每做完一个功能就在模拟器里新建一个低版本比如API 24的机器跑一遍能提前发现不少只在老版本上出现的问题。6. 打包、签名与真机验证从能跑到能装工程在Android Studio里点绿三角能运行只说明调试版没问题。要真正装到别人手机上还得走一遍签名的流程。6.1 生成签名并配置到Gradle调试版APK用的是自动生成的调试签名不能用于正式分发。正式版要自己生成一个签名文件keystore。在Android Studio里走Build → Generate Signed Bundle / APK选择APK然后创建新的keystore。你需要填别名、密码、有效期等信息。这个keystore文件和密码一定要保存好因为App后续更新必须用同一个签名丢了就只能换包名重新上架等于前功尽弃。生成后建议在build.gradle.kts里配置签名这样每次打包不用手动输入android { signingConfigs { create(release) { storeFile file(../memo.jks) storePassword 你的密码 keyAlias memo keyPassword 你的密码 } } buildTypes { release { isMinifyEnabled true signingConfig signingConfigs.getByName(release) proguardFiles( getDefaultProguardFile(proguard-android-optimize.txt), proguard-rules.pro ) } } }把密码明文写在gradle里其实不安全正经项目会用环境变量或者把keystore放到版本控制之外。练手可以先用明文但要知道正式项目不能这么干。isMinifyEnabled true开启代码混淆和压缩能减小APK体积但也可能把Room这类靠反射的库搞坏所以如果开了混淆后运行报错检查proguard规则里有没有为Room、数据类保留必要信息。Room本身会生成混淆规则通常不需要额外配置但你自己如果用反射就别关混淆。6.2 真机调试adb能帮你省下很多时间模拟器好用但真机才最接近真实体验。用USB连上手机打开开发者选项和USB调试然后在终端跑adb devices能看到设备序列号就说明连上了。如果显示unauthorized是手机上还没点允许调试的弹窗重新插拔一下。真机调试的价值在于能体验真实触感、真实性能也能发现模拟器测不出的问题比如某些厂商系统的后台限制。我遇到过在模拟器上后台保存正常在真机上因为系统省电策略被限制的情况只有真机能复现逼着我把保存逻辑改成更早触发。还有个实用技巧是看日志adb logcat能实时输出设备日志。你可以在代码里用Log.d(MemoTag, 保存成功)打点然后在Logcat里过滤MemoTag排查问题时比满屏刷日志高效得多。崩溃的时候Logcat里的FATAL EXCEPTION后面的堆栈是最重要的线索顺着它找到报错的行问题基本就定位了。6.3 发布前我会自己走一遍的自测清单打包之前我习惯按这张表过一遍能挡掉大部分低级问题检查项操作预期结果冷启动杀掉进程重新打开数据完整显示新增记一条并保存列表顶部出现该条编辑改内容再保存内容更新且排到最前删除长按删除并确认列表移除该条旋转编辑中旋转屏幕正在输入的内容不丢后台切到后台一分钟再回来界面状态正常深色模式系统切深色文字清晰可读大量数据加到500条滚动基本流畅这张表看着啰嗦但每一条都对应一个我上面讲过的技术点走一遍相当于把整篇内容复习一次。7. 做完第一版之后还能往哪些方向继续打磨主流程加自测跑完你手上就有一个真正能用的备忘录了。接下来如果想继续练我建议按这个顺序加功能先是搜索用Room的Query配合LIKE关键字就能做注意搜索框输入要加防抖不然每敲一个字都查库会卡然后是置顶给实体加个布尔字段排序时把置顶排前面再往后可以试试分类标签这会涉及两张表笔记表、标签表的关联查询是学习Room关系映射的好练习。我把这个项目推荐给初学的朋友时常说一句话别急着往上堆功能先把一件事做扎实。备忘录的价值不在于它有多复杂而在于它小而完整让你把Android开发的一条完整链路走通一遍。你现在对一个记录从输入、保存、读库、展示、修改、删除的全过程对生命周期怎么影响数据对打包签名怎么把App交到别人手里都有了具体的认识。这些认识换成任何一个更复杂的项目都是通用的。如果非要我说一个新手最容易翻车、也最值得反复琢磨的细节那就是线程和生命周期。数据该在哪个线程读写界面刷新该在什么时机停止这两个问题贯穿所有Android项目。备忘录因为逻辑简单恰好是个把这两点想明白的理想载体。等你哪天做大项目遇到诡异崩溃时多半还是会回到这两个点上找答案。
返回列表