
简介这份资源是一套基于Android开发的焦点新闻App完整项目面向Android入门开发者、课程设计学生及希望提升项目实战经验的程序员。资源包共包含3859个文件体积约61.78MB涵盖Java源码、SQLite数据库、演示视频、APK安装包以及大量图片、XML布局、CSS等前端展示文件从功能实现到界面资源一应俱全。目前已有323人学习浏览是课程设计、毕业设计或自学练手的可靠参考。项目完整覆盖Android生命周期、网络请求与JSON解析、SQLite存储、RecyclerView列表加载、Fragment多屏适配、AsyncTask异步处理及Glide图片加载等高频知识点还涉及运行时权限和性能优化非常适合对照源码与视频逐模块研读是快速打通Android开发全流程的实战型资源。1. 一个新闻 App 项目从解压到验收的关键路径拿到基于Android的焦点新闻App开发(源码数据库演示视频).rar这类压缩包最典型的场景是安卓app开发期末大作业或数据库课程设计的最后冲刺。焦点新闻的重点不在新闻而在焦点二字栏目怎么切分、哪些内容置顶、收藏与阅读记录落在哪张表、界面与数据库如何对齐。一套能交差的项目通常由 Android Studio 工程、一个 SQLite 数据库文件和演示视频组成评测老师第一眼看的是数据能不能查到第二眼才看界面。下面我按接手这类项目的顺序展开先立数据库再走网络层然后做界面最后落在打包与录屏验收。2. 数据库设计焦点新闻的本地数据模型与增删改查2.1 news 表结构栏目、标题、时间、收藏状态怎么落库焦点新闻的核心表只有一张 news其余 user、favorite 表在课程设计里通常做成冗余或直接并入主表。把焦点语义拆成两个字段is_top 表示置顶is_favorite 表示收藏这是最常见的做法。publish_time 用 TEXT 类型存yyyy-MM-dd HH:mm:ss排序时按字符串比较即可避免日期转换带来的时区问题。2.1.1 news 表的字段说明字段类型说明idINTEGER主键自增categoryTEXT栏目名称如本地、国内、科技titleTEXT新闻标题列表页直接展示summaryTEXT摘要列表副标题contentTEXT正文详情页展示sourceTEXT来源默认焦点新闻publish_timeTEXT发布时间用于倒序排列is_topINTEGER置顶标记1 置顶0 普通is_favoriteINTEGER收藏标记1 收藏0 未收藏read_statusINTEGER阅读状态用于列表灰显image_urlTEXT封面图地址为空时显示占位图建表 SQL 中索引和默认值要提前想好下面这段是可直接放进数据库课程设计报告里的写法。CREATE TABLE news ( id INTEGER PRIMARY KEY AUTOINCREMENT, category TEXT NOT NULL, title TEXT NOT NULL, summary TEXT DEFAULT , content TEXT DEFAULT , source TEXT DEFAULT 焦点新闻, publish_time TEXT DEFAULT (datetime(now, localtime)), is_top INTEGER DEFAULT 0, is_favorite INTEGER DEFAULT 0, read_status INTEGER DEFAULT 0, image_url TEXT DEFAULT ); CREATE INDEX idx_news_category ON news(category, publish_time DESC); CREATE INDEX idx_news_favorite ON news(is_favorite);注意 category 加了 NOT NULL因为 TabLayout 的每个栏目都要靠这个字段过滤数据空值会导致列表查询结果错位。索引 idx_news_category 按栏目和时间联合建立栏目切换时执行WHERE category ? ORDER BY publish_time DESC能直接走索引。is_favorite 单独建索引是为了收藏页WHERE is_favorite 1的查询不至于全表扫描。2.2 用 SQLiteOpenHelper 建库与预置条目的最小代码这个场景下我一般不用 Room原因很直接SQLiteOpenHelper 生成的 .db 文件可以直接从data/data/包名/databases/下拉出来用 DB Browser 打开给老师看数据而 Room 默认在抽象层内答辩时多一层解释成本。另一个原因是 Room 要写 Entity、DAO、Database 三套文件对期末大作业来说代码量反而变大。public class NewsDbHelper extends SQLiteOpenHelper { private static final String DB_NAME focus_news.db; private static final int DB_VERSION 1; public NewsDbHelper(Context context) { super(context, DB_NAME, null, DB_VERSION); } Override public void onCreate(SQLiteDatabase db) { db.execSQL(CREATE_NEWS_SQL); insertPresetNews(db); } Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { if (oldVersion 2) { db.execSQL(ALTER TABLE news ADD COLUMN label TEXT DEFAULT ); } } private void insertPresetNews(SQLiteDatabase db) { ContentValues cv new ContentValues(); cv.put(category, 焦点); cv.put(title, 今日焦点新闻App演示数据); cv.put(is_top, 1); db.insert(news, null, cv); } }DB_VERSION 从 1 开始递增onUpgrade 里用 ALTER TABLE 加列而不是 DROP 重建这样用户升级时收藏数据不会丢。insertPresetNews 的作用是让冷启动时列表不为空演示视频里断网状态下也能展示数据。如果预置数据量达到几百条建议把 .db 文件放进 assets 目录首次启动时用 InputStream 拷贝到 databases 目录这里不再展开。2.2.1 增删改查四个方法与返回值数据库增删改查四个方法的写法虽然基础但返回值是评审老师常问的点。insert 返回新行的 rowId失败返回 -1update 和 delete 返回受影响行数为 0 说明条件没匹配上query 返回 Cursor必须在使用后关闭。public long insertNews(News item) { ContentValues cv new ContentValues(); cv.put(category, item.getCategory()); cv.put(title, item.getTitle()); cv.put(is_top, item.isTop() ? 1 : 0); return db.getWritableDatabase().insert(news, null, cv); } public int toggleFavorite(long id, boolean favorite) { ContentValues cv new ContentValues(); cv.put(is_favorite, favorite ? 1 : 0); return db.getWritableDatabase().update(news, cv, id ?, new String[]{String.valueOf(id)}); }insert 的第二参数 nullColumnHack 传 null 即可它只在 ContentValues 为空时才有意义。update 的第三个参数是 where 条件注意用?占位不要直接拼字符串否则新闻标题里带单引号时 SQL 会直接报错。Cursor 查询结果建议封装成 List 列表页 Adapter 里直接 getItem(position) 取数据。2.3 数据库课程设计评审时最容易挑出的三个参数问题第一个是 WAL 模式。新闻 App 的收藏操作发生在列表滑动过程中主线程在读数据库如果写操作直接落在主库文件上可能出现短暂卡顿。在 SQLiteOpenHelper 的 onConfigure 里调用db.setWriteAheadLoggingEnabled(true)读写分离到 -wal 文件列表滚动和收藏写入互不阻塞。第二个是外键约束。如果单独做了 favorite 表外键默认是关闭的需要在 onConfigure 里执行db.setForeignKeyConstraintsEnabled(true)并在建表时写FOREIGN KEY (news_id) REFERENCES news(id)。不开启的话删除新闻时收藏表会留下孤儿数据。第三个是版本号与降级处理。onDowngrade 默认抛异常如果老师把数据库文件从高版本换回低版本会直接崩溃。常见做法是改成db.setVersion(oldVersion)并清空缓存表保证旧文件能重新走到 onUpgrade 逻辑。这三个点写进课程设计报告的数据库优化小节里比贴十行代码更有说服力。3. 数据层到界面Retrofit 拉流、JSON 解析与列表渲染3.1 网络方案选择HttpURLConnection、OkHttp、Retrofit 在新闻项目里的分工新闻 App 的联网部分有三个层次。原生 HttpURLConnection 只适合两三分钟讲完的简单 demo因为读流、超时、线程切换全要手写OkHttp 解决了连接复用和超时控制但回调处理仍然繁琐Retrofit 在 OkHttp 之上做接口声明式映射一个注解对应一个方法JSON 自动转对象。三者的取舍如下表。方案代码量优点适合场景HttpURLConnection大无第三方依赖纯本地 SQLite 演示OkHttp中连接池、拦截器少量接口的自学项目Retrofit小注解映射、Gson 自动转换带栏目的焦点新闻 App推荐直接用 Retrofit因为新闻列表的路径和参数是固定的用注解写比手拼 URL 直观得多。需要说明的是 Retrofit 底层依赖 OkHttp所以 gradle 里只需要引 Retrofit 和 converter-gson 两条依赖。3.2 用 Retrofit 定义焦点新闻接口的最小配置在 build.gradle 的 dependencies 里添加依赖版本号建议选 2.9.0这是目前项目里最稳定的一版不需要额外配转换器。implementation com.squareup.retrofit2:retrofit:2.9.0 implementation com.squareup.retrofit2:converter-gson:2.9.0 implementation com.squareup.okhttp3:logging-interceptor:4.9.3然后是接口定义与 Retrofit 实例化。public interface NewsApi { GET(api/focus/news) CallNewsResponse fetchNews(Query(category) String category, Query(page) int page, Query(pageSize) int pageSize); } Retrofit retrofit new Retrofit.Builder() .baseUrl(https://api.example.com/) .addConverterFactory(GsonConverterFactory.create()) .client(okHttpClient) .build(); NewsApi api retrofit.create(NewsApi.class);baseUrl 必须以/结尾接口里的相对路径不能以/开头这是 Retrofit 最容易踩的坑。Query 会把参数拼到 URL 后面对应服务端接收的 category、page、pageSize。okHttpClient 里建议配connectTimeout(10, TimeUnit.SECONDS)和readTimeout(15, TimeUnit.SECONDS)教室 Wi-Fi 不稳定时10 秒连接超时比默认值更快进入失败重试分支。3.2.1 回调中更新列表的注意点api.fetchNews(科技, 1, 20).enqueue(new CallbackNewsResponse() { Override public void onResponse(CallNewsResponse call, ResponseNewsResponse response) { if (response.isSuccessful() response.body() ! null) { ListNews list response.body().getItems(); adapter.submitList(list); } } Override public void onFailure(CallNewsResponse call, Throwable t) { listState.setValue(STATE_ERROR); } });enqueue 的回调默认切回主线程不需要手动 runOnUiThread。onResponse 不代表业务成功HTTP 200 但 body 为 null 的情况也要判空。onFailure 里要同时处理断网和超时建议把错误状态塞进 LiveData 或直接控制进度条的 visibility让 UI 层根据状态切换转圈、空列表、重试按钮三种形态。3.3 列表加载状态机进度条、空态、失败重试的切换新闻列表的加载状态如果只用一个 ProgressBar演示时一旦断网就会露怯。我习惯用一个整形状态码加三套视图来控制。int state STATE_LOADING; // STATE_LOADING / STATE_SUCCESS / STATE_EMPTY / STATE_ERROR private void applyState(int state) { progressBar.setVisibility(state STATE_LOADING ? View.VISIBLE : View.GONE); recyclerView.setVisibility(state STATE_SUCCESS ? View.VISIBLE : View.GONE); emptyView.setVisibility(state STATE_EMPTY ? View.VISIBLE : View.GONE); errorView.setVisibility(state STATE_ERROR ? View.VISIBLE : View.GONE); if (state STATE_LOADING) rotateProgress(); }注意进度条在列表已有数据时不要显示否则下拉刷新会和转圈重叠。常见做法是加载更多时单独用列表底部的 footerProgress首屏才用整页进度条。页面顶部的协调布局如果带了 banner这个进度条最好放在 banner 下方避免遮挡轮播图。4. 栏目切换与详情页焦点逻辑的前端落地4.1 TabLayout ViewPager2 联动栏目时的三个参数焦点新闻的栏目切换用 TabLayout 加 ViewPager2 是标准答案。ViewPager2 替代了旧 ViewPager 的 FragmentStatePagerAdapter创建 Fragment 的方式变成继承 FragmentStateAdapter。三个参数值得单独说第一个是viewPager2.setOffscreenPageLimit(1)只预加载左右各一页栏目多时不会一次性把所有 Fragment 全建出来第二个是tabLayout.setTabMode(MODE_SCROLLABLE)栏目超过 5 个必须用滚动模式固定模式会把 tab 挤变形第三个是tabLayout.addOnTabSelectedListener里做埋点或滚动定位不要在这里重复请求数据。TabLayoutMediator mediator new TabLayoutMediator(tabLayout, viewPager2, (tab, position) - tab.setText(categoryList.get(position))); mediator.attach();TabLayoutMediator 是 material 库提供的联动器手动设置 tab 文案即可。不要在 tab 的 onSelect 回调里手动 setCurrentItemMediator 已经处理过联动重复设置会导致 Fragment 重建两次。4.1.1 Fragment 与 ViewModel 的边界每个栏目 Fragment 里如果各自持有 RecyclerView 和 Adapter切走再切回时会丢失滚动位置。常见做法是 ViewModel 以栏目名为 key 缓存列表数据Fragment onDestroyView 时清空 Adapter 引用但保留 ViewModel。这样 ViewPager2 预加载切换时数据不会重新请求界面也不会闪白。4.2 焦点置顶排序与协调布局 banner 的实现焦点二字的直接体现是置顶。列表查询时把 is_top 放在时间之前排序SQL 写成ORDER BY is_top DESC, publish_time DESC置顶新闻永远排在前面时间倒序只对同级别生效。我的建议是置顶逻辑放在 SQL 层而不是在 Java 里拆成两个列表再合并后者的排序在分页时会出现重复项。SELECT * FROM news WHERE category ? ORDER BY is_top DESC, publish_time DESC LIMIT 20 OFFSET 0;banner 部分用协调布局加 ViewPager2 是最常见的效果。AppBarLayout 包裹 CollapsingToolbarLayout标题栏随列表上滑折叠banner 图片跟着缩放。轮播不能只依赖 ViewPager2 的自动翻页还要在 onPageSelected 里同步指示器圆点并用handler.postDelayed做 3 秒一帧的循环。注意在 onPause 时移除所有回调否则离屏后 handler 仍持有上下文演示过程中容易触发内存泄漏提示。4.3 详情页WebView 渲染与原生布局的取舍新闻正文有两种展示路径。一种是后端返回 HTML直接 WebView 加载开发量最小但 demo 里通常没有现成的网页服务另一种是 content 字段存纯文本详情页用 TextView 配合滚动布局。课程设计场景我建议混搭富文本用 WebView 的 loadDataWithBaseURL纯文本走原生布局两种页面共用同一个 Activity通过 Intent 传 id 区分。Intent intent new Intent(this, NewsDetailActivity.class); intent.putExtra(news_id, item.getId()); intent.putExtra(is_favorite, item.isFavorite()); startActivity(intent);详情页里收藏按钮的回写走第 2 章的 toggleFavorite 方法成功后把按钮图标切换成实心状态并更新列表页对应 item 的数据。演示视频里完整展示收藏、退出、重新进入、收藏仍在这个闭环比展示任何动画效果都有说服力。5. 打包与演示视频把数据库和界面串成评审证据5.1 录制前先做三类数据验证演示最容易翻车的是断网状态。录制前先在 Android Studio 里关闭网络冷启动 App确认焦点栏目能显示 SQLite 预置数据这说明数据库文件确实被打进了应用然后打开网络拉取接口数据验证 JSON 字段与列表渲染一致最后收藏一条新闻杀掉进程重新打开确认 is_favorite 持久化成功。这三步走完数据库、网络、界面三个环节就都覆盖了。5.2 模拟器录屏的三个必调参数用模拟器录屏时分辨率、动画、时长三个参数直接影响观感。adb shell wm size 1080x1920 adb shell settings put global window_animation_scale 0 adb shell settings put global transition_animation_scale 0 adb shell settings put global animator_duration_scale 0 adb shell screenrecord --time-limit 60 --size 1080x1920 /sdcard/demo.mp4 adb pull /sdcard/demo.mp4关闭系统动画后列表滑动和 banner 切换不会拖泥带水。screenrecord 默认只能录 180 秒加 --time-limit 60 控制在一分钟以内方便直接嵌入说明文档。5.3 源码、数据库、演示视频的归档检查文件检查点Android Studio 工程gradle 版本号与 dependencies 是否齐全databases 目录focus_news.db 能否用 DB Browser 打开演示视频是否包含冷启动、栏目切换、收藏闭环三段数据库设计文档ER 图与 news 表字段是否一致签名打包用 Build 菜单的 Generate Signed APKkeystore 文件建议和源码放同一层目录避免换电脑后签名丢失无法安装。最后一件事是把模拟器里的数据库文件导出到项目根目录的 database 文件夹老师拿到压缩包后边看视频边验证数据对应关系这个项目的验收基本就稳了。本文还有配套的精品资源点击获取