多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

安卓单词本App开发实战:SQLite存储与RecyclerView列表实现

安卓单词本App开发实战:SQLite存储与RecyclerView列表实现 简介这是一份面向Android开发初学者与高校课程设计学生的单词本APP实战项目源码适用于安卓期末大作业、移动应用开发实训或Java/Android基础能力综合检验场景。项目基于Android Studio完整构建涵盖单词增删改查、本地SQLite数据库now_word.db持久化存储、发音播放wav资源、图文混合界面xml布局jpg/png素材及Gradle构建配置代码结构清晰、注释充分下载解压后可直接导入AS运行。压缩包共275个文件含155个XML布局与资源定义文件、75个Java核心逻辑类、11张JPG/PNG图片素材、7个JAR依赖库及Gradle相关配置文件等整体大小仅6.36MB轻量易部署。已有3027人学习下载提供开箱即用的高分方案95分以上包含完整项目目录结构、可执行数据库、音效资源及跨平台构建脚本gradlew.bat/gradlew显著降低环境配置与调试门槛。1. 为什么期末大作业选单词本APP需求定位与功能边界先聊点实在的。每到期末安卓开发课的作业题目就那么几类图书管理系统、备忘录、记账本、单词本。我当年选单词本不是因为别的而是因为它看起来简单做起来有得写答辩时有的聊。为什么这么说因为单词本这个题目天然覆盖了安卓开发的核心知识面界面布局、列表展示、数据持久化、增删改查、事件监听、搜索过滤甚至还能往用户体验、动画、数据统计方向延伸。一个题目能同时撑起课程设计说明书、答辩PPT和代码演示三个环节这种投资回报率在期末这个节骨眼上非常可观。但这里必须先给一个清醒的提醒单词本APP和能做出来的单词本APP是两回事。很多同学一上来就想着要做一个完美的产品加了一堆花里胡哨的功能最后卡在某个 bug 上通宵。期末大作业的正确答案不是功能多而是能跑、能演示、能讲清楚。所以拿到题目后第一步不是打开 Android Studio 敲代码而是把需求边界划清楚。我建议把功能分成三个层级功能层级具体功能优先级核心功能必须做单词的增删改查、列表展示、搜索必做做不好会挂科增强功能尽量做生词本标记、未掌握单词筛选、复习模式加分项代码量不大进阶功能有余力再做超级简单背单词测验、统计图表、主题切换亮点但风险高容易出bug我的建议是第一版先把核心功能做到极致确保每一步都有十足的把握第二版再加增强功能进阶功能只有在前面全部跑通之后才碰。因为期末大作业挂科的原因往往不是功能不够而是最后一天代码跑不起来。功能再炫演示时崩溃一次老师对你的印象会大打折扣。另外还有一个很多人忽略的维度老师批改作业时看什么根据我了解的情况老师通常按以下顺序看项目一打开工程能不能编译通过二能不能正常安装运行三核心功能是否完备四界面是否美观、交互是否自然五代码结构是否清晰、注释是否到位六答辩时你能不能讲明白关键代码。如果你告诉我界面漂亮但编译报错那等于交了个白卷。所以我在整个项目里始终把稳定可运行放在最高优先级其次才是功能和界面。这篇博文也完全按照这个思路来写。2. 技术选型与架构设计SQLite存储、RecyclerView列表和分层思路2.1 存储方案为什么选SQLite而不是别的单词本APP最核心的数据是单词及其释义这个数据量不大但涉及增删改查、条件筛选而且希望应用重启后数据不丢。我第一次做的时候想过很多存储方案直接用共享偏好存储SharedPreferences、用文件存储、用数据库。最后选了SQLite原因很简单这个项目用SQLite是教科书标准的做法对期末作业有天然的合理性答辩时一个为什么选SQLite的问题你能答得有理有据。这几个方案做个对比就清楚了方案优势劣势适用场景SharedPreferences简单适合存少量键值对不适合存列表数据、无法条件查询存用户设置、登录状态文件存储JSON/文本灵活不用学SQL读写复杂、数据一多性能差、容易出错小型配置或离线数据交换SQLite数据库结构化、支持SQL查询、性能稳定需要写SQL语句有一定学习成本数据量大、需要增删改查的正式功能Room官方ORM比SQLite更好写编译期检查SQL需要引入依赖有学习成本更接近实际项目的做法如果你是基础比较扎实的同学也可以直接上 Room它封装了 SQLite代码写起来会优雅很多。但如果你的期末时间紧SQLite 用SQLiteOpenHelper也完全够而且调 SQL 时对原理的理解更深答辩被问到底层存哪里这类问题反而更稳。我下文的核心实现均基于原生 SQLite最后再提一下可以迁移到 Room。2.2 为什么列表用RecyclerView而不是ListView绝大多数安卓课的教材还在讲 ListView但实际开发中 RecyclerView 早就取代了 ListView。用 RecyclerView 在期末作业里是加分项而且它本身没有难到哪去核心就是三步适配器Adapter、布局管理器LayoutManager、数据源List。它是 Google 官方主推的列表控件性能和扩展性都比 ListView 好很多。如果你只想能用ListView 当然也能跑但我建议你至少试一下 RecyclerView因为演示时如果老师问你用的是哪个控件你回答 RecyclerView 并且能讲清楚它的复用机制这个印象分差得不是一点半点。2.3 项目整体架构简单三层够用就行很多同学一上手就想着MVP、MVVM其实期末作业没必要搞那么重。我用了一个非常朴素的分层保证代码清晰、答辩讲得明白数据层DatabaseHelper继承SQLiteOpenHelper负责建表、升级、WordDao封装对单词表的所有增删改查操作。业务层WordController或者直接在Activity中调用WordDao。我建议抽出一个WordController来隔离数据层和UI层哪怕它只是简单转发也可以让你的代码结构在答辩时站得住脚。界面层MainActivity列表展示 搜索、EditWordActivity新增/编辑单词、WordDetailActivity单词详情等。这样的好处是每个类职责单一出了问题能找到明确的位置你想加新功能时比如统计学过多少单词不用改动现有结构新增一个方法就行答辩时你可以清晰地说出我的项目是数据层、界面层分离的这句话在课程设计里很好使。项目包结构大概长这样com.example.wordbook/ ├── MainActivity.java // 主界面单词列表、搜索框 ├── EditWordActivity.java // 添加/编辑单词界面 ├── WordAdapter.java // RecyclerView适配器 ├── Word.java // 实体类 ├── WordDao.java // 数据访问方法增删改查 ├── DatabaseHelper.java // 数据库帮助类 └── res/ ├── layout/ │ ├── activity_main.xml │ ├── item_word.xml │ └── activity_edit_word.xml └── values/ ├── strings.xml └── colors.xml3. 核心功能实现详解从建表到列表展示再到增删改查3.1 数据库设计与建表单词本的表设计不用太复杂但字段要有前瞻性。我的words表这样设计CREATE TABLE words ( id INTEGER PRIMARY KEY AUTOINCREMENT, word TEXT NOT NULL, -- 单词本身 translation TEXT NOT NULL, -- 释义 phonetic TEXT, -- 音标可空 example TEXT, -- 例句可空 created_time TEXT NOT NULL, -- 添加时间 mastered INTEGER DEFAULT 0 -- 是否已掌握0未掌握 1已掌握 );设计要点id是自增主键唯一标识每条记录。word和translation设置为NOT NULL这是单词本的红线没有单词和释义那条目就没有意义。mastered字段是给生词本和已掌握筛选用的这也就是上面功能层级里增强功能的数据基础。created_time存字符串格式如2025-05-20 12:00:00后面可以用来做统计比如本周新增了哪些词。在DatabaseHelper里对应写public class DatabaseHelper extends SQLiteOpenHelper { private static final String DB_NAME wordbook.db; private static final int DB_VERSION 1; public DatabaseHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE TABLE words ( id INTEGER PRIMARY KEY AUTOINCREMENT, word TEXT NOT NULL, translation TEXT NOT NULL, phonetic TEXT, example TEXT, created_time TEXT NOT NULL, mastered INTEGER DEFAULT 0)); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { // 期末考试一般不会遇到升级但写上这两行没坏处 db.execSQL(DROP TABLE IF EXISTS words); onCreate(db); } }有个细节要提一下我见过不少同学的数据库字段名写中文、有空格或者用了 SQL 的关键字比如text、name都会在运行时报错。字段名尽量用英文单数加前缀如created_time避免冲突。这种坑排查起来真的很浪费时间。3.2 数据访问层把增删改查封装成方法WordDao是控制数据库的门面。所有对单词表的操作都走这个类目的是让 Activity 里不出现 SQL 语句答辩也更好讲。核心方法通常有四个public class WordDao { private DatabaseHelper dbHelper; public WordDao(Context context) { dbHelper new DatabaseHelper(context); } // 插入单词 public boolean insertWord(Word word) { SQLiteDatabase db dbHelper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(word, word.getWord()); values.put(translation, word.getTranslation()); values.put(phonetic, word.getPhonetic()); values.put(example, word.getExample()); values.put(created_time, System.currentTimeMillis() ); // 演示时方便可换格式化时间 long result db.insert(words, null, values); db.close(); return result ! -1; } // 删除单词 public boolean deleteWord(int id) { SQLiteDatabase db dbHelper.getWritableDatabase(); int rows db.delete(words, id?, new String[]{String.valueOf(id)}); db.close(); return rows 0; } // 更新单词 public boolean updateWord(Word word) { SQLiteDatabase db dbHelper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(word, word.getWord()); values.put(translation, word.getTranslation()); values.put(phonetic, word.getPhonetic()); values.put(example, word.getExample()); values.put(mastered, word.getMastered()); int rows db.update(words, values, id?, new String[]{String.valueOf(word.getId())}); db.close(); return rows 0; } // 查询所有单词 public ListWord getAllWords() { ListWord list new ArrayList(); SQLiteDatabase db dbHelper.getReadableDatabase(); Cursor cursor db.query(words, null, null, null, null, null, created_time DESC); while (cursor.moveToNext()) { Word word new Word(); word.setId(cursor.getInt(cursor.getColumnIndexOrThrow(id))); word.setWord(cursor.getString(cursor.getColumnIndexOrThrow(word))); word.setTranslation(cursor.getString(cursor.getColumnIndexOrThrow(translation))); word.setPhonetic(cursor.getString(cursor.getColumnIndexOrThrow(phonetic))); word.setExample(cursor.getString(cursor.getColumnIndexOrThrow(example))); word.setMastered(cursor.getInt(cursor.getColumnIndexOrThrow(mastered))); list.add(word); } cursor.close(); db.close(); return list; } }补充一点查询时created_time DESC按时间倒序排新添加的单词显示在最前面这个交互反馈比较直观。如果你喜欢按字母排序改成word ASC即可。用到getColumnIndexOrThrow比getColumnIndex更安全避免列名拼错时返回 -1 导致崩溃。这个点很小但如果在演示时崩一下就很尴尬。实体类Word就是一个普通的 Java Bean字段对应表结构提供 getter/setter 和构造方法没什么好说的。但有同学直接往Word类里塞 UI 相关逻辑这是不对的。实体类就该干干净净地只装数据保持职责单一。3.3 RecyclerView列表展示与适配器编写主界面用一个EditText作为搜索框下面接一个RecyclerView。布局文件里要注意给 RecyclerView 设置layout_height0dp和layout_weight1这样搜索框固定在顶部列表占满剩余高度不会出现键盘弹起时布局顶开的问题。androidx.appcompat.widget.AppCompatEditText android:idid/et_search android:layout_widthmatch_parent android:layout_heightwrap_content android:hint输入单词进行搜索 android:padding12dp app:layout_constraintTop_toTopOfparent / androidx.recyclerview.widget.RecyclerView android:idid/rv_words android:layout_widthmatch_parent android:layout_height0dp app:layout_constraintTop_toBottomOfid/et_search app:layout_constraintBottom_toBottomOfparent /适配器WordAdapter是列表的核心。这里有一个非常重要的点适配器持有数据源ListWord所有增删改查完之后通过adapter.notifyDataSetChanged()通知列表刷新。很多同学犯的错是修改了数据库之后界面没有变化原因就是忘了刷新适配器或者换了个新 List 但没有重新设置给适配器。我通常的做法是这样的public class WordAdapter extends RecyclerView.AdapterWordAdapter.ViewHolder { private ListWord wordList; private OnItemClickListener listener; public WordAdapter(ListWord wordList) { this.wordList wordList; } // 定义点击回调接口 public interface OnItemClickListener { void onItemClick(Word word); void onItemLongClick(Word word); } public void setOnItemClickListener(OnItemClickListener listener) { this.listener listener; } NonNull Override public ViewHolder onCreateViewHolder(NonNull ViewGroup parent, int viewType) { View view LayoutInflater.from(parent.getContext()) .inflate(R.layout.item_word, parent, false); return new ViewHolder(view); } Override public void onBindViewHolder(NonNull ViewHolder holder, int position) { Word word wordList.get(position); holder.tvWord.setText(word.getWord()); holder.tvTranslation.setText(word.getTranslation()); holder.itemView.setOnClickListener(v - { if (listener ! null) listener.onItemClick(word); }); holder.itemView.setOnLongClickListener(v - { if (listener ! null) listener.onItemLongClick(word); return true; }); } Override public int getItemCount() { return wordList null ? 0 : wordList.size(); } static class ViewHolder extends RecyclerView.ViewHolder { TextView tvWord, tvTranslation; ViewHolder(View itemView) { super(itemView); tvWord itemView.findViewById(R.id.tv_word); tvTranslation itemView.findViewById(R.id.tv_translation); } } }注意onItemLongClick返回true告诉系统长按事件已被消费不然会继续触发点击事件。这个小细节老师不会主动问但如果你在演示时发现长按和点击同时触发你就能立刻知道是这里的问题。3.4 添加与编辑功能复用界面加一个模式位添加和编辑的界面完全可以复用一个Activity靠一个Intent参数区分模式。比如在MainActivity中Intent intent new Intent(this, EditWordActivity.class); // 新增模式不传 id编辑模式传 id intent.putExtra(word_id, word.getId()); startActivity(intent);在EditWordActivity的onCreate里判断long wordId getIntent().getLongExtra(word_id, -1); if (wordId -1) { // 新增模式 } else { // 编辑模式根据 wordId 从数据库查出数据回填到输入框 }保存时同样按模式处理Word word new Word(); word.setWord(etWord.getText().toString().trim()); word.setTranslation(etTranslation.getText().toString().trim()); // ... 其他字段 if (wordId -1) { wordDao.insertWord(word); } else { word.setId(wordId); wordDao.updateWord(word); }这里有个很实用的校验输入框内容为空时直接Toast提示单词和释义不能为空然后return不要继续执行保存逻辑。这种非空判断放在 Activity 里最直接不用在 DAO 层多写逻辑。3.5 搜索的实现本地模糊查询搜索功能我用一句话总结不查数据库查内存里的 List。有两种实现方式各有取舍方案一每次点击搜索或输入变化时查询数据库SQL 里加WHERE word LIKE ?。好处是数据量很大时表现好坏处是每次都要查数据库代码稍多一点。方案二一次性查出所有数据到 List用普通字符串匹配过滤得到一个新 List 再adapter.setData(filteredList)。代码量少、响应也快数据量上千条完全没压力期末作业足够了。我用的是方案二核心就一个方法private ListWord filterWords(String query) { if (query null || query.isEmpty()) { return allWords; // 搜索框为空时展示全量 } ListWord result new ArrayList(); for (Word word : allWords) { if (word.getWord().toLowerCase().contains(query.toLowerCase()) || word.getTranslation().contains(query)) { result.add(word); } } return result; }注意这里用toLowerCase()忽略大小写避免输入hello匹配不到Hello。在EditText上添加TextWatcher监听输入变化每次onTextChanged时重新过滤并刷新列表。注意TextWatcher在代码里写起来啰嗦可以用afterTextChanged方法来触发过滤etSearch.addTextChangedListener(new TextWatcher() { Override public void beforeTextChanged(CharSequence s, int start, int count, int after) { } Override public void onTextChanged(CharSequence s, int start, int before, int count) { } Override public void afterTextChanged(Editable s) { ListWord filtered filterWords(s.toString()); adapter.setWordList(filtered); adapter.notifyDataSetChanged(); } });adapter.setWordList需要你在适配器里提供一个 setter 方法把新的数据源替换进去。这个方案在期末作业量级的项目里完全够用不用去做数据库层面的模糊查询省下的时间去打磨界面多划算。3.6 删除功能别加花哨的确认对话框删除有两种触发方式点击删除按钮或长按弹菜单。我选了长按弹一个AlertDialog确认因为这样最直观、最稳也不容易误删。核心代码如下wordAdapter.setOnItemLongClickListener(word - { new AlertDialog.Builder(MainActivity.this) .setTitle(删除单词) .setMessage(确定要删除 word.getWord() 吗) .setPositiveButton(删除, (dialog, which) - { wordDao.deleteWord(word.getId()); loadWords(); // 重新加载列表 Toast.makeText(MainActivity.this, 已删除, Toast.LENGTH_SHORT).show(); }) .setNegativeButton(取消, null) .show(); });loadWords()里面做两件事重新查数据库、设置数据到适配器并刷新。注意这里一定要重新查数据库而不是从内存里 remove因为数据源必须和数据库保持一致。如果你只 remove 内存中的数据一旦界面刷新或进入详情再返回数据又会出现。4. 被低估的加分细节界面优化、动画、数据统计和生词本功能4.1 界面细节从能用到好看的几步老师看界面第一眼不是看设计有多复杂而是看你是不是认真做了。我花了大概一个小时做这几件事效果立竿见影统一配色在colors.xml里定义主题色而不是到处写死颜色值。我用一个偏蓝的绿色做主色调搭配浅灰背景。卡片式列表每一个单词条目用CardViewandroidx.cardview.widget.CardView包裹加上圆角、阴影和边距。代码很简单但视觉上立刻脱离了原始 demo的质感。控件间距所有按钮、输入框、列表项都保证至少有8dp的边距避免贴边。你去看很多同学的界面问题不在控件样式而在间距完全不讲。空状态提示列表为空时显示一个还没有单词点击右下角按钮添加的提示这个对印象分很加分。实现方式用setEmptyView或者一个TextView动态切换即可。很多同学觉得界面好不好看不影响分数这个观点在大学课程设计的评分里其实不太成立。期末演示时老师会连续看几十个项目一个排版整齐、间距合理的界面让人赏心悦目这确实是实打实的加分。我一个学弟的项目功能很简单就靠界面做得好答辩成绩达到了优秀。4.2 生词本功能一个字段带来的复利还记得我在建表时预留的mastered字段吗它的用武之地在这里。在列表界面加两个 Tab 或者两个筛选按钮全部 / 未掌握点击未掌握时过滤出mastered 0的单词。数据层加一个方法public ListWord getUnmasteredWords() { SQLiteDatabase db dbHelper.getReadableDatabase(); Cursor cursor db.query(words, null, mastered0, null, null, null, created_time DESC); ListWord list new ArrayList(); // 循环遍历填充 cursor.close(); db.close(); return list; }在列表的每个条目右侧放一个CheckBox或者一个圆形按钮点击后切换已掌握/未掌握状态。这个功能从代码量看非常小但演示时的效果和讲解空间完全不一样——你可以说我的应用支持用户把已经记住的单词标记为已掌握方便复习时只看生词这比单纯说我会增删改查高了一个层次。4.3 随机测验模式最有演示效果的功能如果你已经做完了核心功能和生词本还有余力我建议做一个随机测验功能从数据库里随机抽 N 个单词展示单词让用户选正确释义做完全部题目后显示得分。这个功能在演示时很抓眼球而且实现并不难生成一个Intent打开QuizActivity。在QuizActivity启动时从数据库随机取 10 条记录SELECT * FROM words ORDER BY RANDOM() LIMIT 10。每道题展示单词四个选项里一个是正确释义另外三个是从全表随机取的其他释义。用户点击选项后立即显示正确/错误并自动进入下一题。最后展示成绩。这里有一个关键的排序注意点正确的选项位置要随机不能每次都放在第一个。否则演示时就是连续答对老师会觉得功能太假。把四个释义放在一个List里用Collections.shuffle()打乱即可。这个功能大约需要 200 行代码但它是整份期末作业中最容易让老师记住你的点。如果你时间紧可以只做 5 道题代码量更少效果也一样。4.4 数据统计用created_time字段做柱状图统计功能是我比较推荐的一个进阶选项因为它性价比极高不需要额外存数据靠created_time字段就能算出每天的添加数量。但统计图表需要引入一个图表库对期末项目来说有点风险。更稳妥的做法是用RecyclerView 简单自定义 View 画一个柱状图出来其实就是画矩形代码量很小看起来又很高端。如果你不想引入第三方库教你一个巧招写一个View在onDraw里根据最近 7 天的单词数量画 7 个高度不同的矩形下方标注星期几。这个 View 用不到 100 行代码但答辩时你可以说我自定义控件画了一个统计图——这句话在很多老师眼里比调用第三方库值钱。4.5 夜间模式低成本高感知的加分项夜间模式在 Android 里其实有个很取巧的实现方式不需要完整的values-night资源目录在Application或MainActivity里保留一个SharedPreferences的开关切换时直接调用AppCompatDelegate.setDefaultNightMode(MODE_NIGHT_YES/MODE_NIGHT_NO)然后recreate()重建界面。配合 Material Components 的Theme.MaterialComponents.DayNight主题大部分控件会自动变色。这个功能加分的关键在于切换按钮的位置要做到两个状态都有入口在添加/编辑界面、主界面都放一个图标按钮切换后立即生效。演示时当着老师的面切一下效果非常直观而且实现成本极低。但注意有些同学切完之后发现自定义 View 的颜色是写死的变成了深色背景上的黑色文字这种 bug 一定要提前测试并修掉不然反而适得其反。5. 踩坑实录Android Studio和运行时那些让我头皮发麻的问题5.1 编译不过先看 Gradle 版本和依赖这是期末作业中出现频率最高的问题。很多同学从学长那里拷贝工程后打开就报Failed to resolve: ...或Could not find ...第一反应是删代码其实问题的根源一般在Gradle版本和SDK版本。经验如下拷贝来的工程先不要急着点 Sync打开build.gradleProject级和 Module级看compileSdkVersion、targetSdkVersion、buildToolsVersion是否和本机已安装的 SDK 匹配。如果报依赖找不到多半是因为依赖的库版本在当前网络环境下下载失败。可以尝试把版本号改低一点比如implementation androidx.recyclerview:recyclerview:1.2.1改成1.1.0通常低版本库更稳定、更容易下载如果本地已经缓存了对应版本直接用也行。另一个常见问题是AndroidManifest.xml里的package和 Application ID 不一致或者清单文件里没有注册Activity。新版的 Android Studio 会自动注册但如果你手写了Activity记得去清单文件里看有没有activity标签漏了就会在运行时直接崩溃。我有个同学期末前几天因为依赖下载失败把 Gradle 文件改得面目全非最后整个项目跑不起来了。这里建议如果你不确定自己改了什么会带来什么后果就一步步小改每改一次就点一次 Sync直到通过。5.2 数据改了但界面不刷新八成是忘了 notifyDataSetChanged这个坑我在 3.3 已经提过但还是要单独放一节因为它太常见了。数据库里明明 insert 成功了Log 能查出来但 RecyclerView 就是不更新。按这个顺序排查确认 adapter 确实持有的是同一个数据源 List确认调用notifyDataSetChanged()的时机在数据修改之后确认不是 UI 线程问题如果你在子线程操作数据库记得回主线程刷新。5.3 列表出现重复数据或顺序错乱这通常是因为onCreate里加载了数据然后又在onResume里再次加载而每次Activity恢复比如从详情页返回都会触发onResume。如果两次加载都直接往同一个List里 add就会出现重复。解决办法是每次加载前先list.clear()再添加或者用setWordList方法直接替换整个 List。这也是为什么我反复强调数据源和适配器要绑定同一个 List 引用的原因。5.4 EditText 弹键盘把界面顶起来/顶不上去单词本的编辑界面添加单词时需要输入单词和释义很容易遇到键盘把按钮顶到看不见的问题。要在AndroidManifest.xml里给EditWordActivity设置android:windowSoftInputModeadjustResize。这样布局会自动压缩保证底部按钮能看到。另一个常见痛点是RecyclerView里如果有EditText输入时列表会反复刷新这种情况最好把EditText单独放进一个对话框或者新页面不要在列表项里直接编辑不然焦点管理会很头疼。5.5 数据库更新后老版本用户的问题期末作业虽然不会真的上架但如果你中途改过表结构比如加了一个mastered字段一定要把DB_VERSION从 1 改成 2并在onUpgrade里写ALTER TABLE语句或者直接DROP TABLE重建。否则老版本应用升级后读取列名时找不到新字段直接崩溃。演示时最怕出现这种在老师面前闪退的场面。5.6 Android Studio 大版本迁移的坑Android Studio现在更新到 Koala 甚至更高版本从旧版项目打开时会弹 Gradle 升级提示。如果你只是想交期末作业别急着升级。升级有可能引入新的构建配置变化导致本来能跑的项目突然报错。学长的项目是Gradle 6.5的你的 Android Studio 是Gradle 8.x的直接打开大概率报错。比较稳妥的办法不开学长工程自己新建一个项目从零写代码。单词本这个项目的代码量不大自己写一遍也就是两三天的事而且能完全掌控配置比花三天研究如何迁移旧工程要稳定得多。6. 打包、演示与答辩最后一公里怎么走6.1 生成可安装的APK期末作业除了交源码通常还要现场演示或提交 APK。点Build - Build Bundle(s) / APK(s) - Build APK(s)等待构建完成后APK 在app/build/outputs/apk/debug/app-debug.apk这个可以直接安装到模拟器或真机。但要注意如果要在别人手机上安装建议生成签名包Generate Signed Bundle/APK因为 debug 包的签名是默认的部分手机可能拒绝直接安装调试包。生成签名包需要创建一个 keystore用 Android Studio 的向导即可密码记好。不要用过于复杂的密码因为老师如果想换个设备跑一下你可能要现场输入密码这时候如果忘了就非常尴尬。如果你的应用用了高版本 targetSdk比如 Android 13/14在低版本手机上安装没问题但如果你在最新版 Android Studio 上创建项目默认targetSdk较高在高版本手机上安装调试包需要额外开启安装未知应用的权限演示前最好提前在老师设备上确认能装上。6.2 演示流程设计别小看演示环节。我见过很多技术完全没问题的同学因为演示顺序混乱或者演示时手忙脚乱让老师觉得项目不成熟。这里分享一个稳妥的演示顺序打开 APP展示主页列表为空的状态如果初始化数据为空就先展示这个。点击添加按钮添加第一个单词输入单词、释义、音标、例句保存看到列表刷新。再添加第二个、第三个单词展示列表卡片效果以及不同的状态标记。用搜索框搜一个字母展示过滤效果再清空搜索框恢复列表。点击一个单词条目进入详情页展示详情。长按其中一个单词执行删除确认删除后列表刷新。如果有生词本功能切换未掌握筛选展示过滤结果。演示随机测验完成几道题展示得分。最后如果有夜间模式切换一下展示 UI 变化。总结成口诀增、删、改、查、搜、筛、测、换肤。每一步之间留出几秒停顿让老师看清操作每一步操作完都口头说一句这里我用了什么技术比如这里我调用了 SQLite 的 update 接口同时刷新了适配器数据。这不仅是演示更是一次有准备的答辩预演。6.3 答辩常问问题清单答辩时老师大概率会针对代码问这几个方向的问题提前准备比现场编要靠谱太多问题方向参考回答要点SQLite 和 SharedPreferences 的区别SQLite 适合结构化、条数多、需要查询的数据SharedPreferences 适合键值对、轻量配置RecyclerView 和 ListView 的区别RecyclerView 强制使用 ViewHolder、支持多种布局管理器、默认支持动画ListView 没有为什么用getWritableDatabase而不是getReadableDatabase插入/更新/删除操作需要写权限数据会不会丢存在应用私有目录下/data/data/包名/databases/卸载应用才删除如何优化列表卡顿布局渲染尽量简单、避免 item 里用嵌套布局、数据量大的话考虑分页或索引代码分层思想实体类、DAO、Activity 三层分离逻辑清晰、易扩展其中关于数据存储位置的回答很多同学只答存在数据库里就完事了建议补一句SQLite 其实就是一个内嵌在应用沙盒目录里的文件路径是/data/data/包名/databases/wordbook.db这种细节会显示你确实理解底层的逻辑。6.4 代码注释怎么写才加分注释不需要每行都写但关键的地方要几个有分量注释。我的标准是类头部注释说明这个类职责方法头部注释说明入参、出参、作用复杂逻辑内简短注释说明思路。比如 DAO 方法上面/** * 根据关键字模糊查询单词列表 * param keyword 关键字可为空或空串 * return 匹配的单词列表 */ public ListWord searchWords(String keyword)这种注释老师一眼就能看出来你写代码是有章法的。太多注释反而会产生背课文的感觉到时候老师随便问一个细节你答不上来反而露出破绽。6.5 如果还有时间考虑把 SQLite 升级成 Room如果你做完上面所有内容后还有两三天时间强烈建议把数据访问层从原生 SQLite 替换成 Room。理由不只是Room 更现代更重要的是期末答辩时跟老师聊起你怎么管理数据库的你回答我用的是 Google 官方推荐的 Room 框架它在 SQLite 之上做了编译期校验同时支持协程处理异步对话的深度完全不一样。Room 的核心代码其实就三个注解Entity(tableName words) public class WordEntity { PrimaryKey(autoGenerate true) public long id; public String word; public String translation; public String phonetic; public String example; public String createdTime; public int mastered; }再写一个 DAO 接口Dao public interface WordDao { Insert void insert(WordEntity word); Delete void delete(WordEntity word); Query(SELECT * FROM words ORDER BY createdTime DESC) LiveDataListWordEntity getAllWords(); }把LiveData接入 RecyclerView 之后数据一变界面自动刷新连notifyDataSetChanged都不用写了。这个方案对期末作业来说不仅代码更简洁而且答辩时聊 Room 的架构设计比聊我手写 SQLite 怎么管理游标要高出几个层次。不过要注意Room 需要引入kapt或ksp插件Gradle 配置比 SQLite 复杂一点如果时间紧就不要折腾了。写在最后关于期末大作业的几个实操建议我在这个项目上前后踩过的坑比这篇文章写出来的还要多一倍。如果你现在还没开工我的建议非常明确第一版目标不是惊艳而是完整。先把核心增删改查做出来跑通再做界面优化再做加分功能。千万别一上来就追求完美最后剩三天连一个能跑的 APK 都打不出来。另外一个心得是期末考试的项目优先求稳定。就是所有你写进项目的功能都必须是你自己在真机或模拟器上跑过两遍以上的。任何一个你只在代码里写过但没有实际验证过的功能都不应该出现在演示时。因为最可怕的情况不是功能没做而是演示时在老师面前崩了这比没做还要减分。我自己在实际做这个项目时有一个习惯把演示流程的每一步都提前写在便签上演示前按顺序走一遍。每一处点击、每一次输入、每一个跳转都确认过没问题才结束彩排。这个习惯在我后来带学弟学妹做课程设计时也一直强调。项目可以不大但演示要像一次精心准备的演讲每一步都是有意识的展示。如果这篇博客对你有帮助建议你至少把文中提到的建表语句、DAO 封装、适配器结构看明白再动手敲代码。学期末的时间就那么多少熬夜多动脑子一款够水平的单词本 APP 就能稳稳落地。本文还有配套的精品资源点击获取
返回列表