Android播放记录界面开发:从数据库设计到ListView实战

发布时间:2026/10/15 15:02:03
Android播放记录界面开发:从数据库设计到ListView实战 作为一个从 Eclipse ADT 年代一路走过来的 Android 开发者看到“博学谷”这个系列还在更新我其实挺感慨的。当年用 Eclipse 写 Androidbuild 一次要喝杯水等半天R.java动不动就消失现在 Studio 里一键同步的速度简直像换了台电脑。但这并不代表 Eclipse 时代的东西没有价值——恰恰相反很多老项目的核心代码思路尤其是界面 数据库这种最常见的组合放在任何时代、任何 IDE 里都是通用的基本功。这一次的主题是“博学谷”项目实战的第十七篇播放记录界面。如果你跟着这个系列走到现在应该已经接触过课程列表、详情页、视频播放这些模块了。而播放记录这个界面说白了就是把用户“看过哪些视频、看到哪一分钟”这件事管理起来做展示、做排序、做点击续播。看起来简单但真做起来里面牵扯到建表、DAO、Adapter、ListView、刷新时机、空状态处理这些零零碎碎的东西每一步都有坑。这篇文章我会按自己当年做这个模块时的顺序把整个设计思路和代码实现从头到尾捋一遍。数据库表怎么建、记录在什么时候写入、列表用什么 Adapter、点击之后怎么恢复播放进度、清空记录的时候该注意什么这些都会讲到。从事 Android 开发但没用过 Eclipse 的朋友或者正在跟“博学谷”系列做练习的同学都可以拿这篇当一个完整的参考。我会尽量把那些“看上很简单、实际一跑就出错”的细节也写出来毕竟这些才是真正省时间的部分。1. 先理清播放记录界面的需求核心1.1 播放记录界面到底要解决什么问题在动手写代码之前我习惯先自己问一遍这个界面是给谁用的用户在这个界面上会做什么播放记录有些 App 里叫“观看历史”是一个很典型的内容型 App 功能模块。用户看视频可能今天看到一半有事退出了明天回来想接着看也可能昨天看了一个系列课今天想快速找到之前看到第几集。这时候如果还让用户去课程列表里一页一页翻体验是非常差的。所以播放记录界面承担的核心任务是把零散的播放行为变成有序的历史列表让用户用最短路径回到上次离开的画面。具体到“博学谷”这个项目场景里它是学习教育类应用视频是课程的核心载体。用户可能同时在看多门课每门课的进度都不一样。播放记录如果做得好就是“帮我记住我看到哪儿了”的私人助手做得不好就是一个摆满废弃条目的垃圾列表看不到进度也点不出任何有效操作。基于这个思考我给播放记录界面定了几个基本需求展示所有播放过的视频条目按最近播放时间倒序排列。每条记录要能显示课程标题、视频封面、上次播放进度比如“看到 05:30”、播放时间。点击某条记录能跳回播放页并且自动从上次进度继续播放。支持一键清空所有记录。当没有记录时界面要给出明显的“暂无记录”的提示而不是一片空白。这里面第五点很容易被新手忽略。很多初学项目的人在拿到“播放记录”这个需求后第一反应就是“列表 数据库”结果数据一空界面直接白屏用户会以为页面卡死了。所以做任何列表页我建议都把空状态当成一个正式需求来设计而不是最后顺手一加。1.2 功能边界划分需要做哪些不做什么定清楚需求之后还要再划一下功能边界避免越做越复杂。“博学谷”这个实战项目是教学用的所以播放记录模块不需要做到商业级 App 那种复杂程度但核心链路必须完整。我当时的做法是只管“记录”本身不管“推荐”。也就是说界面只展示播放历史不需要根据历史记录去做个性化推荐或者猜你喜欢。这样功能边界就非常清晰代码量可控也方便系列教程一步步来。另外关于视频封面博学谷的课程数据本身来自网络接口在列表里展示的封面是一个 URL 字符串。如果按正式项目的思路应该用图片加载框架比如当年的 ImageLoader、Glide或者后来的 Coil去异步加载但考虑到 Eclipse 年代的项目普遍比较“朴素”而且这个系列前面的章节很可能还没有引入图片加载库所以我选择了只存 URL、在 Adapter 里用 LruCache 子线程做一个简单加载的做法。这样既不引入第三方依赖也能说清楚图片异步加载的原理。还有一点是关于数据库里存什么、不存什么。播放记录表只存“和记录行为强相关的字段”比如用户 ID、课程 ID、视频 ID、标题、封面、进度、时长、更新时间。像课程详情的长描述、讲师信息这些不应该塞进记录表因为那属于课程模块的数据播放记录界面根本用不到存了只会占空间还会带来数据同步的麻烦。在设计阶段把这些边界划清楚后面写数据库和 UI 时思路会顺畅很多不会写着写着发现“这个字段我好像当初没建”。2. 数据库设计与播放记录存取2.1 建表设计与字段取舍这个项目一直用的是 SQLite 来存本地数据播放记录自然也是落到 SQLite 里。为什么不用 SharedPreferences因为播放记录是一个“会不断追加、还会按时间排序、又要更新进度”的数据集合SharedPreferences 只适合存键值对和配置项拿它存列表类数据读写都得序列化整个集合性能和代码复杂度都是灾难。SQLite 在这类需求里是完完全全的正解。播放记录表我设计成了下面这样字段名类型说明idINTEGER 主键自增长本地唯一标识course_idINTEGER课程 ID用于跳转video_idINTEGER视频课时IDtitleTEXT视频标题冗余存储image_urlTEXT封面 URLprogressINTEGER上次播放到的位置毫秒durationINTEGER视频总时长毫秒last_timeINTEGER最后播放的时间戳毫秒这里我想单独说一下title 和 image_url这两个“冗余字段”。在正规的表设计理论里这种能通过联表查出来的字段一般不建议冗余存储因为可能和课程表数据不一致。但播放记录是一个非常典型的“快照”场景用户播放时看到的标题是什么记录里就应该永远保留什么。如果课程后来改了标题历史记录里显示新标题反而会让用户觉得莫名其妙。所以我在实战项目里保留冗余字段并且明确告诉读者这是有意为之不是不懂范式的偷懒。progress和duration都存毫秒而不是秒。这是一个很值得提醒的细节。很多播放器 SDK 返回的进度就是毫秒值如果你存秒在计算剩余时间或者百分比时还要再换算而且取整时容易造成 1 秒误差。直接把毫秒原样存下来展示时再做格式化精度最高逻辑也最简单。last_time这个时间戳也很关键。它是排序的依据不能存成字符串格式的2025-01-01 12:00:00因为字符串排序在跨年、跨月、补零问题上非常容易出幺蛾子。存 long 类型的时间戳排序时直接ORDER BY last_time DESC就行干净利落。2.2 DAO 层封装把 SQL 藏起来数据库操作我习惯封装成一个 DAOData Access Object类叫做PlayRecordDAO。为什么一定要单独封装一层因为如果你直接在 Activity 里写db.execSQL(...)第一次没问题第二次你可能忘了关闭数据库连接第三次你可能在一个协程/线程里直接操作 UI 线程上的数据库代码会越来越乱。DAO 层通过提供“插入记录”“更新记录”“查询列表”“清空记录”这么几个方法把 SQL 语句和连接生命周期都收拢在一处界面层根本不需要关心 SQLite 到底怎么执行。先看建表语句和数据库升级逻辑。因为“博学谷”项目有统一的数据库帮助类我在这里只需要往onCreate里追加建表语句然后记得把DATABASE_VERSION往上加一位。如果版本号不变SQLite 不会执行onUpgrade新加的播放记录表就不会创建这也是新手最容易踩的坑之一。public class PlayRecordDAO { // 表名 public static final String TABLE_NAME play_record; // 建表语句 public static final String CREATE_TABLE CREATE TABLE IF NOT EXISTS TABLE_NAME ( id INTEGER PRIMARY KEY AUTOINCREMENT, course_id INTEGER, video_id INTEGER, title TEXT, image_url TEXT, progress INTEGER, duration INTEGER, last_time INTEGER); }插入和更新我尽量走同一条路。设计思路是进入播放页时根据 course_id video_id 查一下是否已经存在记录如果存在更新它的 progress 和 last_time如果不存在才执行插入。这样能避免同一节课产生多条历史记录用户看着也不乱。public void saveOrUpdateRecord(CourseVideo video, int progress, int duration) { SQLiteDatabase db dbHelper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(course_id, video.getCourseId()); values.put(video_id, video.getVideoId()); values.put(title, video.getTitle()); values.put(image_url, video.getImageUrl()); values.put(progress, progress); values.put(duration, duration); values.put(last_time, System.currentTimeMillis()); long id queryRecordId(video.getCourseId(), video.getVideoId()); if (id 0) { db.update(TABLE_NAME, values, id?, new String[]{String.valueOf(id)}); } else { db.insert(TABLE_NAME, null, values); } }查询列表时用ORDER BY last_time DESC倒序把最近播放的记录排在最前面。这里有一个查询细节SimpleCursorAdapter虽然可以直接绑定 Cursor 到 ListView但我后来还是选择了先查成 List 再传给自定义 Adapter。原因是 Cursor 的生命周期和 ListView 绑定有一些坑比如changeCursor之后旧 Cursor 的关闭问题在 Eclipse 时代很容易写出内存泄漏。自定义 Adapter 配合 List 更直观也更好讲解。2.3 记录写入的时机不是随时都写很多做播放记录功能的人有一个误区播放器每回调一次进度就写一次数据库。如果你真这么干就会发现应用卡顿、数据库文件疯长、日志里全是 insert/update。因为播放器通常几百毫秒就回调一次进度你跟着写库等于让 SQLite 高频抖动性能自然好不了。正确做法是只在合适的时机写入一次播放器初始化时拿到视频总时长后如果有历史记录先恢复进度这次不写。在onPause/onDestroy里保存一次当前进度。如果在播放中用户主动退出监听播放器的“播放结束”“暂停”回调也写一次。可以加一个简单的节流逻辑比如进度变化超过 5 秒才更新内存中的值生命周期回调时才真正落库。在“博学谷”这个实战项目里我当时就是在播放页的onPause里调用了saveOrUpdateRecord()。这样既不会丢失进度也不会造成性能问题。别小看这个设计它直接决定了你的播放记录功能在用户眼里是“好用”还是“卡到爆”。顺带提一句Eclipse 年代的 Android 所有数据库操作都发生在主线程我建议在真实项目中至少用AsyncTask或者线程池把写库操作挪到子线程。不过教学项目为了看得清楚可以保持同步写法但要在注释里说明白这个性能隐患等以后迁移到 Android Studio Kotlin 时再改成协程或 Room。3. UI 层的搭建ListView 还是 RecyclerView 的选型3.1 为什么这个项目继续用 ListView我知道很多人看到这里会问都什么年代了怎么还在用 ListView这不是新技术RecyclerView 才是主流。这个问题的答案要结合项目背景来说。“博学谷”是在 Eclipse ADT 环境下开发的系列项目。Eclipse 时代要使用 RecyclerView需要手动引入 support-v7 库还要处理一堆资源引用问题对初学者来说成本不低。而 ListView 是 Android 自带的控件不需要任何额外依赖只需要写一个布局、一个 Adapter就能跑起来。在一个教学项目里用最少的依赖把核心逻辑讲清楚比盲目追新更重要而且 ListView 的AdapterView体系、getView()复用机制这些底层思想在 RecyclerView 里依然适用。所以如果你是跟着教程练习我建议你先用 ListView 实现一遍理解ViewHolder和convertView的作用。等你把原理吃透了迁移到 RecyclerView 也就是换个 Adapter 的事情思路完全一样。3.2 主界面布局下拉刷新可以以后再谈播放记录主界面我用了一个垂直方向的LinearLayout作为根布局里面放一个 ListView再在 ListView 下面放一个“清空记录”的 TextView 作为底栏。为什么不用 ListView 的addFooterView因为底栏按钮如果作为 FooterView在列表为空时它也跟着不见了用户就没法看到任何操作入口。而放在 ListView 外面作为普通布局不管列表有没有数据它都固定在底部视觉上也更像一个工具栏。LinearLayout android:layout_widthmatch_parent android:layout_heightmatch_parent android:orientationvertical ListView android:idid/lv_play_record android:layout_widthmatch_parent android:layout_height0dp android:layout_weight1 android:divider#E8E8E8 android:dividerHeight0.5dp android:listSelector#22000000 / TextView android:idid/btn_clear_record android:layout_widthmatch_parent android:layout_height48dp android:gravitycenter android:text清空播放记录 android:textColor#FF4C4C android:background#FFFFFF android:textSize14sp / /LinearLayout这个布局里有一个很实用的设置android:layout_weight1让 ListView 占满除了底栏以外的剩余空间底栏始终固定在页面底部。如果你不做这个设置ListView 可能会把底栏挤到屏幕外面去这是新手经常遇到的情况。3.3 item 布局三要素一个都不能少每条播放记录的 item 布局我设计成左右结构左边一个封面图右边两行文字最后右边还有一个进入箭头或者时间信息。这是移动端列表最经典的结构用户阅读成本最低。item 布局的关键点封面图用固定宽高比如 100dp x 70dpscaleTypecenterCrop。不固定宽高的话网络图片加载过程中高度会跳变列表会乱跳。第一行是标题maxLines1ellipsizeend超长打省略号否则会挤压第二行。第二行放“观看进度”和“播放时间”这两个信息可以用一个RelativeLayout排左右两边也可以用两个TextView分别对齐。图片加载方面我前面提到不引第三方库用 LruCache 子线程加载。这里的细节是加载图片时一定要带一个图片 URL 的边界判断防止 ListView item 复用时旧图片闪烁。viewHolder.ivCover.setTag(video.getImageUrl()); // 加载图片的 Runnable 里再比较一次 tag不一致就丢弃这是 ListView 时代最经典的一个坑不处理的话列表快速滑动时你会看到图片闪来闪去像放幻灯片一样。4. Adapter 与数据显示的实现4.1 自定义 BaseAdapter 的正确姿势播放记录界面我选择自定义PlayRecordAdapter继承BaseAdapter。为什么不用ArrayAdapter因为它只能展示单文本列表播放记录这种多字段 item 只能自定义。为什么不用SimpleCursorAdapter前面说过了Cursor 生命周期管理麻烦而且数据字段一多绑定代码也啰嗦。BaseAdapter 最核心的是getView()方法这里必须做两件事convertView复用和ViewHolder模式。复用的意思是ListView 滚动时滚出屏幕的 item 视图会被系统回收传给convertView你不需要重新inflate整个布局只需要往里面填新数据。ViewHolder则是把每个 item 里的子控件引用缓存起来避免每次都findViewById。Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView null) { convertView LayoutInflater.from(mContext).inflate(R.layout.item_play_record, parent, false); holder new ViewHolder(); holder.ivCover (ImageView) convertView.findViewById(R.id.iv_cover); holder.tvTitle (TextView) convertView.findViewById(R.id.tv_title); holder.tvProgress (TextView) convertView.findViewById(R.id.tv_progress); holder.tvTime (TextView) convertView.findViewById(R.id.tv_time); convertView.setTag(holder); } else { holder (ViewHolder) convertView.getTag(); } PlayRecord record mList.get(position); holder.tvTitle.setText(record.getTitle()); holder.tvProgress.setText(已观看 formatDuration(record.getProgress())); holder.tvTime.setText(formatPlayTime(record.getLastTime())); // 封面加载与 tag 保护 loadCoverImage(holder.ivCover, record.getImageUrl(), record.getTitle()); return convertView; }这里有一个我特别想拎出来讲的细节inflate时第三个参数传了parentattachToRoot传false。如果你写成inflate(R.layout.item_play_record, null)item 的根布局宽高属性就会被忽略实际显示时会按wrap_content或者match_parent的默认值来处理经常导致 item 宽度异常。这个问题在老教程里非常常见传parent且attachToRootfalse才是正确用法。4.2 数据加载与 EmptyView 空状态数据加载我写了一个名为loadData()的方法。查询数据库是在子线程做的查到结果后回到主线程更新 Adapter。在 Eclipse 原生 Android 的写法里最快的方案就是AsyncTask虽然它后来被官方标记为废弃但在理解“子线程查询 主线程刷新 UI”的模型上还是很直接。private void loadData() { new AsyncTaskVoid, Void, ListPlayRecord() { Override protected ListPlayRecord doInBackground(Void... params) { return dao.queryAllRecords(); } Override protected void onPostExecute(ListPlayRecord records) { mList.clear(); mList.addAll(records); mAdapter.notifyDataSetChanged(); if (mList.isEmpty()) { mEmptyView.setVisibility(View.VISIBLE); mListView.setVisibility(View.GONE); } else { mEmptyView.setVisibility(View.GONE); mListView.setVisibility(View.VISIBLE); } } }.execute(); }空状态的展示有很多种写法比较笨的是在布局里写死一个“暂无记录”的 TextView然后在代码里控制它的显隐。稍微聪明一点的做法是把空状态布局放到 ListView 对应的位置但 ListView 自己不带 emptyView 绑定你需要手动控制 Visibility。我见过不少人在这一步偷懒觉得“空不空无所谓反正有数据”。但用户第一次安装后打开播放记录页面一定是什么都没有的。空状态文案写得好甚至能降低用户对功能的困惑感。我在“博学谷”项目里写的空状态是一个居中的 TextView文案是“您还没有观看记录哦”再配一个半透明的小图标简单但至少不会让用户觉得页面坏了。4.3 时间格式化的细节别输在字符串上播放记录的列表里要显示“最后播放时间”。这个数据在库里的类型是 long 时间戳但你不可能在界面上直接显示1735689600000这种数字。需要格式化。最简单的做法是SimpleDateFormat比如把时间戳转成MM-dd HH:mm。但一个比我更细心的点在于如果记录是今天产生的显示“今天 12:30”会更亲切如果是昨天显示“昨天”更早的才显示日期。这种人性化格式在很多 App 里都有代码实现也不复杂。public static String formatPlayTime(long timestamp) { long now System.currentTimeMillis(); if (isSameDay(timestamp, now)) { return 今天 new SimpleDateFormat(HH:mm, Locale.getDefault()).format(new Date(timestamp)); } else if (isYesterday(timestamp, now)) { return 昨天 new SimpleDateFormat(HH:mm, Locale.getDefault()).format(new Date(timestamp)); } else { return new SimpleDateFormat(MM-dd HH:mm, Locale.getDefault()).format(new Date(timestamp)); } }还有一个经验是SimpleDateFormat是线程不安全的如果在多线程里被共用很容易出现格式化出来的分钟数漂移这种诡异问题。在 Activity 内部使用或者在 DAO 里使用时尽量每次 new 一个或者用ThreadLocal包一下。项目规模小可以直接 new不影响性能。5. 交互逻辑与页面跳转5.1 点击 item 跳转并恢复播放进度播放记录界面的核心价值就是用户点击某条记录后能直接回到原来的播放页面并继续上次的进度。所以点击事件的实现不能只传递一个“课程 ID”还需要把视频 ID、标题、上次进度都带上。listView.setOnItemClickListener(new AdapterView.OnItemClickListener() { Override public void onItemClick(AdapterView? parent, View view, int position, long id) { PlayRecord record mList.get(position); Intent intent new Intent(PlayRecordActivity.this, VideoPlayActivity.class); intent.putExtra(course_id, record.getCourseId()); intent.putExtra(video_id, record.getVideoId()); intent.putExtra(title, record.getTitle()); intent.putExtra(resume_position, record.getProgress()); startActivity(intent); } });在 VideoPlayActivity 那边接收resume_position后调用播放器的 seekTo 方法。这里有一个需要注意的容错判断如果传过来的 progress 大于视频总时长或者小于 0就直接从 0 开始播不要盲目 seek否则播放器可能会卡在某个错误状态。5.2 播放结束后记录应该怎么处理一个课堂知识点当视频播放到 99% 或者播放结束回调触发时这条记录是不是应该从列表里“移除”或者标记为“已看完”从产品角度来说已经完整看完的视频继续留在播放记录里其实参考价值已经不大了因为它不再需要“续播”。但直接删掉也不行用户可能想回顾。我在这个项目里做的是一个很折中的方案记录仍然保留但正常播放结束时progress 重置为 0。这样在列表里显示“已观看 00:00”用户一眼就能判断“这部已经看完了”。你不必在播放记录界面上专门做“删除单条”的手势滑动手势那是另一个比较复杂的话题。清空按钮 自动重置进度已经是教学项目里性价比很高的设计了。5.3 返回界面后的自动刷新用户从播放页返回播放记录界面时列表必须刷新。如果不刷新你看到的还是进入播放页之前的旧数据用户会觉得自己刚才的观看行为根本没有被记录体验很糟糕。在 Activity 里把loadData()放在onResume()里调用而不是onCreate()里只调一次就可以解决这个问题。这也是 Android 生命周期的一个典型应用场景onResume代表界面重新可见适合做数据刷新onCreate只做初始化不适合承载需要每次可见时都执行的操作。我见过有人把刷新放到onRestart()里也能用但覆盖场景不全。比如首次启动时onRestart不会被调用还是得单独在onCreate里再调一次绕来绕去不如直接在onResume里做逻辑最统一。6. 常见问题与排查技巧实录6.1 列表数据加载不出来先检查数据库版本“博学谷”这个系列是分章节推进的数据库可能在前面章节里已经建好了几个表。如果你把播放记录表和 DAO 的代码写完了运行后列表却是空的而且 LogCat 里没有任何异常第一步应该检查的是DATABASE_VERSION有没有升级很多初学者用的是原来的数据库帮助类版本号还是 1onUpgrade永远不会触发新建的表自然不会被创建。你查看应用的数据库文件时会发现里面根本没有play_record表查询方法返回空列表就是必然结果。解决方法是把数据库版本号加 1比如从 1 改成 2然后在onUpgrade里执行db.execSQL(PlayRecordDAO.CREATE_TABLE)。如果你还在开发阶段也可以直接卸载应用重装但养成升级版本号的习惯对以后维护老项目更重要。排查 SQLite 表结构我建议用一个可视化的工具直接打开应用数据库文件看看确认表是否存在、表字段是不是和 DAO 里的常量一致。有些时候你只是字段名拼错了运行时的异常被 UI 层吞掉了表面看就是“数据没出来”。6.2 ListView 图片闪烁与数据错乱播放记录列表有封面图用 LruCache 子线程加载时最常遇到的就是快速滑动后图片错乱。原因是 ListView 的 item 复用时老的 item 被滚动回收位置被新数据复用但图片加载是异步的旧的加载任务回来后把图片设置到了新位置上。解决思路我在前面已经提过给 ImageView 设置一个 tag 记录当前 item 的图片 URL加载完成的回调里比较 tag 是否一致不一致就不设置。如果你用的是 Glide 或 Coil它们内部已经处理了这些问题所以后来在 Android Studio 项目里这些坑会少很多但理解原理仍然很重要。6.3 进度条恢复了但画面黑白屏了几秒点击记录跳回播放页后你调用seekTo(progress)会发现画面先是黑一下或者卡住一两秒然后才亮起来。这个现象在直播流或网络视频里尤其明显原因是进度条跳转后需要重新 buffering而不是所有播放器都能“秒恢复”。针对这个现象我的建议是进入播放页时先播放视频等到onPrepared回调触发后再seekTo。不要在播放器还没准备好时就调 seekTo那会导致状态机混乱。正确顺序是初始化播放器 - 监听onPrepared- 在onPrepared里判断是否有resume_position有就seekTo没有就正常播放。还有一个小技巧恢复进度后播放器从静止画面开始播放用户体验可能有点愣。你可以在 seek 之后立刻显示一个半透明的“正在跳转”提示等 onPrepared 后隐藏这个交互会让用户觉得应用很聪明而不是卡了。6.4 Eclipse 环境下的三个特殊注意点这个系列毕竟是 Eclipse 项目的实战最后我再聊几个 Eclipse 特有的坑。第一添加新类或新布局后必须 Project Clean。Eclipse 的自动编译经常抽风新加的R.id引用报了红叉很多时候不是代码问题而是 ADT 没有正确增量编译。右键项目 - Clean - Clean all projects再重新编译基本能解决大部分“找不到符号”诡异问题。第二导包时注意android.R和项目包名.R的冲突。在 Eclipse 里写代码自动导入android.R是常有的事而你实际要用的是项目自己的R类。如果R.layout.item_play_record报错找不到你可以按 CtrlShiftO 整理引包让错误自动暴露出来。第三模拟器内存不足导致应用直接闪退。播放记录界面涉及图片加载和数据库操作如果你还在用老的 AVG 模拟器512MB 内存很容出现 OOM 或者 ANR。建议用一个真机调试或者把模拟器内存调大。真机上 SQLite 的速度和流畅度是模拟器完全没法比的很多只在模拟器上出现的“灵异现象”换个真机就消失不见了。说到底播放记录这个界面不复杂复杂的是它和环境之间的各种交互。数据库、Adapter、生命周期、异步加载每一块单独拎出来都不大但它们拼在一起就和用户的真实体验直接相关。我个人的体会是做这种“功能完整但不算耀眼”的模块时最容易出彩的反而不是什么高级技巧而是那些细节进度格式化是不是够人性化、空状态是不是够友好、跳转回来能不能续播、清空操作有没有二次确认。把这些细节做稳了整个功能的完成度就上来了。如果你正在跟“博学谷”系列写这个页面写完别急着看终效果试着把自己当用户走一遍流程看视频到一半退出再进入记录页点击记录确认进度恢复了再清空记录确认空状态出现了。这一趟流程能跑通这个模块就算真的完成了。

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询