ARTICLE DETAIL

资讯详情

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

Android备忘录实战:Room、RecyclerView与Gradle入门

Android备忘录实战:Room、RecyclerView与Gradle入门 很多人一提到“Android项目开发实战”脑子里立刻浮现出电商、短视频、即时通讯这种大工程然后打开 Android Studio 被 Gradle 同步卡住三天后放弃。我带过几届新人也见过不少自学的人绕远路最后真正坚持下来的几乎都是从“简单备忘录”这种小东西起步的。原因很朴素备忘录的需求边界足够清晰一个输入框、一个列表、一个数据库但它把 Android 开发里最核心的几件事全串起来了——Activity 生命周期、列表复用、数据持久化、异步线程、文件读写、通知与权限一样都不少。你把这个项目做透再回头看 MVVM、Hilt、Compose 这些进阶内容会有种“原来讲的是同一件事”的感觉。这篇文章写给三类人会一点 Java 或 Kotlin 但没写过 Android 的初学者、写过 Demo 却没做过完整应用的在校生、以及想找个模板快速搭工具类 App 的独立开发者。我尽量把每一步“为什么这么做”讲明白代码能直接抄坑也提前给你标出来。1. 选“备忘录”当练手项目我是怎么想的1.1 需求拆解一个备忘录最少要有哪几件事先把需求摊开别急着敲代码。备忘录这种东西看着简单真做起来功能会像滚雪球一样膨胀所以第一件事是砍需求定一个“第一版必须做完”的清单。我给初学者的第一版清单通常是这样新建笔记、编辑笔记、删除笔记、按修改时间倒序展示、空状态提示。这五条做完你已经跑通了一个完整的“增删改查 列表渲染”闭环。第二版再加搜索、置顶、颜色标签、字数统计、长按多选。第三版才考虑提醒通知、导出备份、深色模式、云同步云同步我建议放到最后甚至会劝你先别做因为它会把网络、鉴权、冲突合并一并带进来复杂度是前十项加起来的总和。为什么要这样分批因为新手最常见的失败模式是“一边写一边加需求”写着写着数据库字段改了七八次最后自己都不知道表结构长什么样。定清单的另一个好处是能倒推技术选型。比如你要“按修改时间倒序”那数据库里就必须有 updated_at 字段而且要在每次保存时更新它你要“搜索”就决定了查询语句得支持 LIKE 或 FTS而不是在内存里过滤整个列表。还有一个隐形需求很多人第一天想不到输入过程中的临时保存。用户写了一屏字来了个电话Activity 被系统回收回来发现内容空了这种体验足以让一个人卸载你的 App。所以“onSaveInstanceState 兜底”和“草稿自动保存”应该被算进第一版而不是当成优化项。1.2 技术选型Room 还是裸 SQLiteRecyclerView 还是别的选型这块我踩过坑说几个结论理由也一并给你。数据持久化我给的建议是直接上 Room。裸 SQLite 不是不能用但你得自己写 SQLiteOpenHelper、自己拼 Cursor、自己处理列索引越界写着写着就变成“SQL 字符串工程师”。Room 是 Google 官方在 SQLite 上包的 ORM编译期校验 SQL 语句你写错字段名直接编译不过这对新手太友好了。代价是要理解注解处理器和一点协程但这点学习成本比起手写 Cursor 的调试时间划算得多。如果你甚至不想碰数据库SharedPreferences 或 DataStore 也能存但它们不适合存列表笔记一多就卡别走这条路。列表控件用 RecyclerView不要用 ListView。ListView 是老东西了Adapter 模式繁琐、没有默认的局部刷新、动画也弱。RecyclerView 配合 ListAdapter DiffUtil能自动算出哪几项变了只刷新变化的那一行滑起来很顺。热搜里经常能看到“android中协调布局banner”这类词本质上都是 RecyclerView 的变体用法把这一个控件吃透后面的 Banner、瀑布流、折叠卡片都只是换 LayoutManager 的事。架构层面第一版我建议最朴素的写法Activity 直接持有 ViewModelViewModel 持有 RepositoryRepository 持有 DAO。别一上来就上 Hilt 依赖注入你得先体会“手动 new 对象很烦”这件事才会理解 Hilt 解决了什么问题。这个顺序反过来学很容易变成“背注解但不理解”。语言选择上如果你是新学直接用 Kotlin。官方文档、示例、新库基本都是 Kotlin 优先协程处理异步比 Java 的 Thread Handler 干净太多。1.3 工程结构包怎么分才不别扭新手常见的是所有文件堆在一个包下二三十个类之后就没法看了。我习惯按“职责”分不按“类型”分com.example.memo/ ├── data/ 数据层 │ ├── Note.kt 实体 │ ├── NoteDao.kt 查询接口 │ ├── AppDatabase.kt 数据库入口 │ └── NoteRepository.kt 仓库 ├── ui/ │ ├── list/ 列表页 │ ├── edit/ 编辑页 │ └── common/ 通用组件 ├── util/ 工具类 └── MemoApp.kt Application为什么不按 Activity/Fragment/Adapter 分因为当你改一个功能时改动通常集中在某一个业务里按职责分包能让你一眼找到相关文件而不是在三个包之间来回跳。包名尽量短而清晰别搞com.example.myfirstandroidapplicationproject这种超长前缀改起来烦。有一点要提醒Application 类和数据库实例必须做成单例。Room 的RoomDatabase.Builder每次调用都会新建一个连接池如果你在每个 Activity 里都 build 一次很快就会看到“数据库连接泄漏”的日志甚至崩溃。标准做法是放在 Application 里或者用一个object持有。2. 环境搭建与工程初始化新手最容易卡住的地方2.1 Android Studio 安装与 SDK 组件取舍环境这块是劝退重灾区我把关键点列一下。下载 Android Studio 走官网就行安装时会有个 SDK 组件勾选页面默认全勾会占好几个 G其实第一版备忘录用不到 NDK、用不到系统镜像除非你要跑模拟器。我的建议是SDK Platforms 勾一个你目标版本的系统比如 Android 14/API 34SDK Tools 里勾 Android SDK Build-Tools、Android SDK Platform-Tools、Android SDK Command-line Tools 三项就够了。模拟器如果电脑内存够16G 以上可以装一个不够就老老实实真机调试速度反而更快。关于“android studio怎么设置中文”和“android studio汉化”这类热搜词我说一下态度初学阶段不建议汉化。原因很现实——你遇到报错去搜资料看到的中文教程、官方文档、StackOverflow 答案里全是英文菜单名汉化之后你反而对不上号。菜单就那么几个词用一周就熟了。真要看不懂配个翻译插件比整体汉化靠谱。还有“android sdk 官网下载”“android sdk command line tools runs”这类需求通常是因为有人想脱离 IDE 用命令行构建。备忘录这种项目前期没必要等你要做 CI 自动打包时再研究 cmdline-tools 也不迟。2.2 新建工程时的几个关键选项点 New Project 之后模板选Empty Views Activity老版本叫 Empty Activity别选 Compose 模板也尽量别用带 Bottom Navigation 的那种复杂模板它会塞一堆你用不上的代码反而干扰理解。接下来几个选项很关键选项建议值理由LanguageKotlin官方主推协程好用Minimum SDKAPI 24 或 API 26覆盖率高且能用大部分新 APIBuild configuration languageKotlin DSLbuild.gradle.kts新工程默认未来趋势Package name简短小写避免中文和数字开头后期改名成本极高minSdk 的选择我再展开说一下。选 API 21 覆盖率最高但你会失去很多新 API每次写新特性都要加版本判断很烦。选 API 26Android 8.0能覆盖绝大多数在用设备且 Java 8 的很多特性、通知渠道、后台限制模型都是从这个版本开始标准化的学到的知识和现在的主流写法一致。我一般让学生直接选 26省心。包名一定要在新建时就定好因为一旦发布上线包名就是应用的唯一身份改包名等于换一个 App。曾经见过有人做到一半觉得包名难看手动全局替换结果 FileProvider 的 authority、签名配置、深链全部失配查了两天。2.3 Gradle 依赖管理与镜像加速新建完工程第一次同步很多人会卡在“Downloading xxx”上不动这不是你的错是依赖仓库在境外。解决办法是配置国内镜像源把仓库地址换成国内主流 maven 镜像同步速度会从“泡杯咖啡回来还在转”变成几秒钟。改的地方有两处老工程在项目根目录的build.gradle新工程在settings.gradle.kts的pluginManagement和dependencyResolutionManagement里。下面是 Kotlin DSL 的写法示例把镜像地址加在 google() 之前dependencyResolutionManagement { repositories { maven { url uri(https://maven.aliyun.com/repository/public) } maven { url uri(https://maven.aliyun.com/repository/google) } google() mavenCentral() } }凡是搜到“android studio init.gradle”的多半是想要全局生效的镜像配置——把 init 脚本放到~/.gradle/init.gradle下所有工程都会自动走镜像适合同时维护多个项目的人。依赖方面备忘录第一版大概需要这几样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) implementation(androidx.lifecycle:lifecycle-viewmodel-ktx:2.7.0) implementation(androidx.lifecycle:lifecycle-runtime-ktx:2.7.0) implementation(androidx.room:room-runtime:2.6.1) implementation(androidx.room:room-ktx:2.6.1) ksp(androidx.room:room-compiler:2.6.1) }注意 Room 的编译器现在推荐用KSP而不是老的 kapt编译更快。用 KSP 需要在 plugins 里加上com.google.devtools.ksp版本号要和你的 Kotlin 版本对应这一步版本对不上是最常见的报错来源配错了会看到一堆 “Unresolved reference” 但报错信息完全不提版本问题。注意升级依赖时一次只升一个改完同步一次、跑一次不要一口气全升到最新否则出问题你根本不知道是谁干的。2.4 真机调试adb 与无线连接模拟器能跑但真机体验更真实尤其是软键盘弹出、通知、文件路径这些模拟器和真机差异不小。真机调试要先开开发者选项设置里连点“版本号”七次然后打开 USB 调试插上线手机上会弹授权框勾“始终允许”。adb这个工具藏在 SDK 的 platform-tools 目录下建议把它加进系统环境变量之后命令行里能直接用adb devices # 看设备有没有连上 adb install app-debug.apk # 手动装包 adb logcat -v time *:E # 只看错误日志排查崩溃热搜里“如何使用 android studio 无线连接调试 vivo 手机”问的人很多思路是通用的手机和电脑连同一个 Wi-FiAndroid 11 以上可以直接在开发者选项里开“无线调试”拿到配对码后用adb pair配对再adb connect ip:端口连接。Android 10 以下的老设备得先用 USB 执行adb tcpip 5555拔线后再adb connect。实测下来无线调试偶尔会断写代码时还是插线稳无线适合演示和调试不方便插线的场景。3. 数据层Room 实体、DAO 与版本迁移3.1 表结构设计字段背后的取舍表结构是整个项目的地基改起来最痛所以一开始就要想清楚。我的备忘录实体是这么设计的Entity(tableName notes) data class Note( PrimaryKey(autoGenerate true) val id: Long 0L, 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, ColumnInfo(name pinned) val pinned: Boolean false, ColumnInfo(name color_tag) val colorTag: Int 0, ColumnInfo(name remind_at) val remindAt: Long? null )逐个说取舍。id用自增 Long 而不是 UUID因为自增主键在 SQLite 里是 rowid 的别名索引效率最高本地单机应用不需要全局唯一。createdAt和updatedAt都用毫秒时间戳Long而不是字符串因为排序、比较、格式化都方便展示时再用 SimpleDateFormat 转成人类可读的样子。有人图省事存 “2024-05-01 12:00” 这种字符串结果排序时 “10月” 会排在 “2月” 前面这种坑早点避开。pinned用 BooleanRoom 会自动映射成 INTEGER 的 0/1。colorTag用 Int 存颜色索引而不是直接存颜色值这样换主题时颜色能跟着变不会写死。remindAt是可空的 Long空表示没设提醒这里必须用可空类型否则没法表达“没有提醒”这个状态用 0 当哨兵值迟早会出错。提示字段命名统一用下划线风格created_at用ColumnInfo显式指定。这样以后你换语言、换框架数据库文件里的列名是稳定的不会因为 Kotlin 属性改名而被动迁移。3.2 DAO 与查询模糊搜索、排序、分页DAO 接口是整个数据层的门面写得好的话上层几乎不用关心 SQL。先看核心查询Dao interface NoteDao { Query( SELECT * FROM notes WHERE (:keyword OR title LIKE % || :keyword || % OR content LIKE % || :keyword || %) ORDER BY pinned DESC, updated_at DESC ) fun observeAll(keyword: String): FlowListNote Query(SELECT * FROM notes WHERE id :id LIMIT 1) suspend fun findById(id: Long): Note? Insert(onConflict OnConflictStrategy.REPLACE) suspend fun upsert(note: Note): Long Delete suspend fun delete(note: Note) Query(DELETE FROM notes WHERE id IN (:ids)) suspend fun deleteByIds(ids: ListLong) }几个细节值得说。第一返回值用FlowListNote而不是ListNote这样数据库一有变化界面自动收到新数据不用手动调 refresh这是 Room 最爽的地方之一。第二排序里pinned DESC在前、updated_at DESC在后意思是置顶的排最上面置顶组内部再按修改时间倒序符合直觉。第三搜索用LIKE % || :keyword || %拼接||是 SQLite 的字符串连接符这样写比在代码里拼%$keyword%更安全能避免一部分注入风险。第四批量删除用IN (:ids)Room 会自动展开成占位符列表比循环单条删快得多。如果以后笔记量到几千条LIKE %xxx%会慢因为前置通配符用不上索引。那时候可以上FTS全文检索Room 有Fts4注解支持虚拟表搜索速度能快一个量级。但笔记几百条以内普通 LIKE 完全够不要过早优化。3.3 版本迁移别让用户的数据丢在升级里数据库迁移是新手最容易忽略、上线后最要命的东西。你在开发阶段改了实体字段如果不写迁移运行时会直接抛IllegalStateException: Room cannot verify the data integrityApp 一启动就崩。开发阶段有个省事的办法fallbackToDestructiveMigration()意思是版本对不上就删库重建。但这个只能用在开发期发布版本绝对不能用它会清空用户所有笔记。发布版必须老老实实写 Migrationval MIGRATION_1_2 object : Migration(1, 2) { override fun migrate(db: SupportSQLiteDatabase) { db.execSQL(ALTER TABLE notes ADD COLUMN remind_at INTEGER) } } Room.databaseBuilder(context, AppDatabase::class.java, memo.db) .addMigrations(MIGRATION_1_2) .build()写迁移有几条铁律新增列必须给默认值或允许为空否则老数据那行没法填不要删列也不要改列类型SQLite 支持有限实在要改就建新表、拷数据、删旧表、改名四步走每次改实体版本号必须 1 并补一条迁移忘了加就会崩。我曾经为了图快开发时一直用 destructive migration结果发内测包之前才补迁移忘了有一版改过类型用户的内测数据全丢了被骂得很惨。4. UI 层列表、编辑页与交互细节4.1 RecyclerView 与 DiffUtil 的正确打开方式列表页是整个 App 的门面用 RecyclerView 配合 ListAdapter 是最省心的组合class NoteAdapter( private val onClick: (Note) - Unit ) : ListAdapterNote, NoteAdapter.VH(Diff) { object Diff : DiffUtil.ItemCallbackNote() { override fun areItemsTheSame(old: Note, new: Note) old.id new.id override fun areContentsTheSame(old: Note, new: Note) old new } class VH(val binding: ItemNoteBinding) : RecyclerView.ViewHolder(binding.root) override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): VH { val binding ItemNoteBinding.inflate( LayoutInflater.from(parent.context), parent, false) return VH(binding) } override fun onBindViewHolder(holder: VH, position: Int) { val note getItem(position) with(holder.binding) { title.text note.title.ifBlank { 无标题 } preview.text note.content.take(60) time.text DateUtils.format(note.updatedAt) root.setOnClickListener { onClick(note) } } } }这里两个关键点。areItemsTheSame比的是 id告诉 RecyclerView “这两个是不是同一条数据”areContentsTheSame比的是整个对象data class 自动实现的 equals判断“内容有没有变”。两个方法写错了会出现要么列表不刷新、要么整屏闪的诡异现象。另一个容易忽略的是submitList()的异步特性。有人调完submitList立刻去取getItem(0)拿到的是旧数据因为 DiffUtil 是在后台线程算的。正确的做法是在onBindViewHolder里取或者用currentList而不是自己维护一份 List。还有个高频需求是空状态。列表为空时要显示“还没有笔记点右下角新建”而不是一片空白。做法是监听 Flowif (list.isEmpty()) emptyView.isVisible true。这个细节很影响观感别省。4.2 编辑页与软键盘那些事编辑页看着简单实际上细节最多。首先进入编辑页时如果是从列表点进来的已有笔记标题栏应该显示“编辑”如果是新建显示“新建”并且新建状态要自动弹出键盘、把光标放到正文。这些用windowSoftInputMode和requestFocus()配合能实现。最大的坑是软键盘遮挡输入框。默认情况下键盘弹出来会把底部输入框盖住你会看到用户打字看不见自己打的内容。解决办法是在 Manifest 里给编辑页的 Activity 加activity android:name.ui.edit.EditActivity android:windowSoftInputModeadjustResize /adjustResize会让布局在键盘弹出时压缩配合 ConstraintLayout 让输入区靠底部约束就能自动顶上去。注意别同时用 adjustPan 和 adjustResize两个一起写行为会互相打架。另外如果你用了全屏、沉浸式状态栏adjustResize可能失效这时要用WindowInsetsCompat手动处理底部内边距这是 Android 高版本适配的一个经典难点。第三个细节是离开时自动保存。我的做法有三层一是onPause时存草稿二是输入停止 800ms 后自动保存用 Handler 或协程的 debounce三是点返回键时如果内容有变化直接保存不弹框备忘录又不是社交软件不需要“是否放弃编辑”这种打断。这三层叠起来基本不会丢数据。4.3 滑动删除、多选与撤销删除操作的手感直接决定用户觉得这个 App “专业”还是“玩具”。我的推荐组合是左滑显示删除背景 松手删除 Snackbar 提供撤销。滑动用ItemTouchHelper实现它负责手势识别你只管在回调里执行业务逻辑val callback object : ItemTouchHelper.SimpleCallback( 0, ItemTouchHelper.LEFT ) { override fun onMove(rv: RecyclerView, vh: RecyclerView.ViewHolder, target: RecyclerView.ViewHolder) false override fun onSwiped(vh: RecyclerView.ViewHolder, direction: Int) { val note adapter.currentList[vh.bindingAdapterPosition] viewModel.delete(note) // 先乐观删除 Snackbar.make(binding.root, 已删除, Snackbar.LENGTH_LONG) .setAction(撤销) { viewModel.restore(note) } .show() } }这里有个设计决策要解释是“先删数据库再提示”还是“先提示再删”我选先乐观删除界面立刻响应用户体验顺滑同时把 note 对象留在内存里点撤销就重新插回数据库。这样做的代价是撤销有极短的时间窗口内数据其实不在库里如果 App 在那一瞬间被杀数据就真没了。对备忘录这种场景可以接受用户主动删除的本来就要删但如果是草稿类应用就要慎重。多选模式我用长按触发进入后每个 item 左侧出现 CheckBox顶部标题栏变成“已选 N 项”。实现上用 Adapter 里维护一个selectedIds: MutableSetLong每次切换notifyItemChanged刷新选中态。注意bindingAdapterPosition和layoutPosition的区别滑动删除的回调里一定要用前者因为删除过程中位置会变用后者会取到错的数据——这个 bug 我调了整整一个下午。4.4 深色模式适配深色模式现在基本是标配Android 10 之后系统级支持用户切了主题你的 App 也跟着变好感度直接拉满。适配的核心是颜色全部走资源引用不要在代码里写死!-- values/colors.xml -- color namesurface#FFFFFF/color color nameon_surface#1A1A1A/color !-- values-night/colors.xml -- color namesurface#121212/color color nameon_surface#E6E6E6/color然后布局里统一写android:background?attr/colorSurface或引用自定义 color系统会自动根据当前模式挑对应文件。要检查有没有漏网之鱼最简单的办法是在开发者选项里开“强制深色”然后在 App 里边走边看哪里出现白底黑字配深色背景那里就是漏了硬编码。主题继承我建议从Theme.Material3.DayNight.NoActionBar起步Material3 自带一套语义化颜色primary、surface、onSurface深色模式下会自动算对比度比自己配颜色省事。别用Theme.MaterialComponents.Light这种写死亮色的主题那样深色模式根本不会生效。5. 功能增强搜索、提醒、导出与签名5.1 搜索过滤与输入防抖搜索框用SearchView或者 Material 的TextInputLayout都行重点是别每敲一个字就查一次库。做法是用协程的 debouncesearchInput.textChanges() .debounce(300L) .distinctUntilChanged() .flatMapLatest { kw - viewModel.search(kw.toString()) } .collect { list - adapter.submitList(list) }debounce(300)的意思是 300 毫秒内没有新输入才真正执行用户快速打字时只会触发最后一次数据库压力小很多。flatMapLatest保证新查询来了就取消上一个避免旧结果后到覆盖新结果。这两个操作符配合起来是搜索体验的关键我强烈建议一开始就用上别等卡了再回来改。搜索还有个体验细节高亮关键字。在 title 和 content 里把匹配的部分用ForegroundColorSpan标出来用户一眼就知道为什么这条被搜出来了。实现上在onBindViewHolder里做注意先设文本再设 Span顺序反了会失效。5.2 提醒通知AlarmManager 与 WorkManager 怎么选提醒功能是备忘录从“记事本”升级成“助手”的分水岭但也是最容易翻车的模块因为涉及后台执行和通知权限。先看选型方案适用场景精度说明AlarmManager精确到分钟的用户提醒高可用 setExactAndAllowWhileIdleWorkManager延迟、约束性任务低系统调度可能延后前台服务长时间运行高有常驻通知体验差给笔记设“明天早上 9 点提醒我”我用AlarmManager因为用户预期是准点响。用setExactAndAllowWhileIdle能穿过 Doze 模式系统省电状态但注意从 Android 12 开始精确闹钟需要声明SCHEDULE_EXACT_ALARM权限且用户可能拒绝拒绝后要降级成setAndAllowWhileIdle这种不精确的方式。通知本身从 Android 8.0 起必须走通知渠道NotificationChannel不建渠道的通知根本不会显示。渠道要在 Application 启动时创建importance选 HIGH 才会有横幅和声音。Android 13 之后还要动态申请POST_NOTIFICATIONS权限不申请就发不出去而且这个权限被拒后不能反复弹得引导用户去设置页手动开。5.3 导出备份SAF 与 FileProvider“我的笔记会不会丢”是用户最深的焦虑加个导出功能能极大提升信任度。导出的正确姿势是用SAFStorage Access Framework让用户自己选存到哪里你完全不需要申请存储权限val intent Intent(Intent.ACTION_CREATE_DOCUMENT).apply { addCategory(Intent.CATEGORY_OPENABLE) type text/plain putExtra(Intent.EXTRA_TITLE, memo_backup_${System.currentTimeMillis()}.txt) } launcher.launch(intent)用户选完之后你拿到的是一个content://开头的 URI直接contentResolver.openOutputStream(uri)往里写就行。这套机制的好处是权限由系统托管你拿到的只是一次性的授权用完即失效安全又省事。同理导入时用ACTION_OPEN_DOCUMENT拿 URI 读取。热搜里那些content://路径根本不用你手动解析交给 ContentResolver 就好自己拼路径在不同厂商的系统上几乎必挂。如果你要把文件分享给别的 App比如分享一条笔记到聊天软件那就得配FileProvider在 Manifest 里声明 provider写一个file_paths.xml指定可分享的目录然后用FileProvider.getUriForFile()生成 URI 并加FLAG_GRANT_READ_URI_PERMISSION。这里最常见的报错是FileUriExposedException原因就是直接传了file://的路径Android 7.0 之后不允许了。5.4 签名与 SHA1上线前必做调试阶段用的都是 debug 签名一旦要发布或者接入某些需要校验签名的第三方能力就必须配 release 签名。生成 keystore 用 Android Studio 的 Build Generate Signed Bundle/APK 向导最省事也可以命令行keytool -genkeypair -v -keystore memo.jks -keyalg RSA -keysize 2048 -validity 10000 -alias memo拿到 keystore 后在 build.gradle.kts 里配 signingConfigs密码不要硬编码提交到仓库放在local.properties或环境变量里用keystoreProperties.getProperty(storePassword)读。我见过有人把密码直接写进 gradle 然后推到公开仓库等于把应用的控制权送人了。获取 SHA1 是另一个高频需求热搜里“android 应用签名 sha1 值”Gradle 任务里自带./gradlew signingReport输出里会列出 debug 和 release 两种签名的 MD5、SHA1、SHA256。注意debug 和 release 的 SHA1 是不同的很多人接第三方时用 debug 的 SHA1 去配结果 release 包一装就报签名校验失败。另外如果你的应用开了 Play 应用签名那么线上包的签名和本地上传的 keystore 又不一致要去后台取真正的签名指纹这个坑值得提前记住。6. 常见问题排查实录6.1 崩溃与 ANRlogcat 到底怎么看App 一闪退就打退堂鼓是最可惜的。其实 90% 的崩溃看一眼日志就能定位。打开 Logcat 面板过滤级别选 Error找FATAL EXCEPTION那一行往下看异常类型和第一行业务代码的堆栈adb logcat -v time *:E | grep -A 30 FATAL EXCEPTION常见的几类NullPointerException多半是 lateinit 或可空类型没判空IllegalStateException: Fragment not attached是异步回调回来时页面已经销毁android.database.sqlite.SQLiteConstraintException是主键冲突Resources$NotFoundException通常是布局里引用了不存在的资源或维度单位写错。ANR应用无响应更隐蔽原因是主线程被卡住超过 5 秒。最常见的是在主线程查数据库或做大数据量遍历。记住一条铁律所有 IO 和数据库操作都放协程的Dispatchers.IO里viewModelScope.launch { val notes withContext(Dispatchers.IO) { dao.getAll() } adapter.submitList(notes) }如果 ANR 已经发生拉取 traces 文件分析adb shell cat /data/anr/traces.txt看主线程的堆栈停在哪个方法上问题就在哪。6.2 列表数据错乱与不刷新RecyclerView 的数据错乱是新手绕不开的坎我总结了一个速查表现象常见原因解决滑动后内容串行ViewHolder 复用没重置状态onBindViewHolder 里重设所有可变字段点了没反应用了 layoutPosition 取错数据改用 bindingAdapterPosition改了数据不刷新传了同一个 List 实例传新对象或走 DiffUtil列表跳动submitList 时机在滚动中先停滚动或改用 setHasStableIds图片错位没有做图片加载的 tag 校验用 Glide/Picasso 自带机制第一行特别典型Item 里有“置顶小图标”“选中勾选框”这类可选元素你只在满足条件时设置isVisible true但没写else分支复用到另一个不带图标的 item 上图标还在。这类 bug 的修复方式很机械——onBindViewHolder 里每一个可能变化的 View 都要显式赋值不留“默认状态”的幻想。6.3 release 包与混淆的坑debug 跑得好好的打了 release 包就崩八成是混淆R8干的好事。R8 会把没被“看见”引用的类删掉或改名反射、序列化、Room 生成的实现类、数据类的字段名都可能中招。解决办法是加 keep 规则-keep class com.example.memo.data.** { *; } -keepclassmembers class * { androidx.room.* methods; }不过现在 Room 和大部分 Jetpack 库都会自带 consumer rules真正需要你手写的部分不多。我的建议是第一版直接关闭 minifyEnabled 发布等应用稳定、体积真的成问题时再开混淆并逐个测试。很多人为了省几百 KB 把功能搞崩得不偿失。真要开就一定要测全流程新建、编辑、删除、搜索、导入导出、提醒一个都不能少。还有一个 release 专属坑是签名不一致导致无法覆盖安装。手机上装过 debug 包再装 release 包会报 “App not installed”得先卸载旧包。测试同事发来“装不上”的截图时先问这个。我自己做完这个备忘录之后最大的体会是Android 项目开发的门槛从来不在于某个 API 有多难而在于一堆“没人明说”的小规矩主线程不能碰数据库、Adapter 里每个可变状态都得显式重置、发布前一定要补迁移、密码不能进仓库。这些东西教程里往往一笔带过但真正决定你的应用是“能跑”还是“能用”。如果你也在做类似的小项目我的建议是把它真正装到自己手机上用一周每天记录哪里别扭那份清单比任何教程都值钱。
返回列表