简介这是一份面向Android初学者的新闻类App完整源码适合刚接触移动开发、希望通过真实项目练手的学生与开发者。项目结构简单、代码量适中能帮助读者理解从界面布局到数据展示的基本流程是入门阶段巩固基础的不错选择。压缩包共约2000个文件整体42.01MB其中java源码76个、xml布局与配置884个、png图片资源655个、json数据267个另有jar依赖、gradle构建脚本及apk安装包等覆盖了典型Android工程的完整构成。目前已有26865人学习下载热度较高说明其作为练手Demo具备一定参考价值。读者可从中获取可运行的工程源码、清晰的模块划分与资源组织方式便于对照学习Activity跳转、列表展示与网络请求等常见实现也可在此基础上自行修改扩展快速积累项目经验。1. 从一份能跑通的新闻APP源码说起它到底能帮你省掉多少重复劳动如果你正在找一份结构完整、能直接导入 Android Studio 跑起来的新闻类 App 源码这份资源大概率能让你少走不少弯路。它不是那种只丢几个 Activity 文件的“半成品”而是一个覆盖了列表页、详情页、分类切换、本地缓存、网络请求的完整小项目。适合两类人一是刚学完 Android 基础、想找个真实场景练手的开发者二是需要快速搭一个资讯类 App 骨架、再往上叠业务的老手。我拿到源码后第一件事就是看它有没有把“请求—解析—展示—缓存”这条链路跑通结果没让我失望。下面我就按“能干什么 → 怎么跑 → 怎么改”的顺序把这份源码拆开讲清楚。2. 项目架构与核心模块拆解一个新闻列表从请求到渲染的完整链路2.1 技术选型与目录结构这份源码用的是 Java 语言整体架构偏向传统的 MVC 模式没有引入 Jetpack 组件对新手来说反而更好理解。网络层用的是 OkHttp 做请求配合 Gson 做 JSON 解析图片加载用的是 Glide列表控件是 RecyclerView。这些依赖都是 Android 开发里最常见的组合版本也比较稳定不会出现那种“照着文档配半天跑不起来”的情况。目录结构大致是这样的app/src/main/java/com/example/newsapp/ ├── adapter/ # RecyclerView 适配器 ├── api/ # 网络请求接口封装 ├── bean/ # 数据实体类 ├── db/ # 本地数据库操作 ├── ui/ # Activity 和 Fragment ├── utils/ # 工具类 └── widget/ # 自定义控件这个分层不算精细但胜在清晰。api目录下集中管理所有请求地址和回调bean里放的是和接口字段一一对应的实体类adapter负责把数据绑到列表上。我一般拿到新项目会先看bean和api因为这两个目录决定了数据长什么样、从哪来看完基本就能判断项目能不能二次开发。2.2 网络请求封装OkHttp Gson 的落地写法源码里把网络请求单独抽了一个类没有用 Retrofit而是直接对 OkHttp 做了一层薄封装。这种写法在小型项目里很常见少一个依赖就少一层学习成本。核心代码大概长这样// api/HttpUtil.java public class HttpUtil { private static final OkHttpClient client new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) // 连接超时 .readTimeout(10, TimeUnit.SECONDS) // 读取超时 .build(); // 通用 GET 请求回调返回泛型对象 public static T void get(String url, final ClassT clazz, final HttpCallbackT callback) { Request request new Request.Builder() .url(url) .get() .build(); client.newCall(request).enqueue(new Callback() { Override public void onFailure(Call call, IOException e) { callback.onError(e.getMessage()); // 失败回调 } Override public void onResponse(Call call, Response response) throws IOException { if (response.isSuccessful()) { String json response.body().string(); // 用 Gson 把 JSON 转成实体类 T result new Gson().fromJson(json, clazz); callback.onSuccess(result); } else { callback.onError(请求失败 response.code()); } } }); } }这段代码的关键点有三个一是OkHttpClient做了超时配置避免网络差的时候界面一直转圈二是回调里直接返回泛型对象调用方不用再手动解析 JSON三是onFailure和onResponse里都做了错误分支不会出现异常直接崩掉的情况。参数方面connectTimeout和readTimeout我一般会设成 10 到 15 秒太短容易误报太长用户体验差。如果你要换成 Retrofit只需要把HttpUtil替换掉上层调用逻辑基本不用动。2.3 列表页与详情页的数据流转列表页用的是 RecyclerView Adapter 的标准组合。Adapter 里持有一个ListNewsBean每次下拉刷新或者加载更多的时候更新这个列表然后调notifyDataSetChanged()。详情页则是通过 Intent 传递新闻 ID在详情页里再发一次请求拿完整内容。这种“列表拿摘要、详情拿全文”的做法很常见能减少列表接口的返回体积。// ui/NewsListActivity.java 片段 private void loadNews(int page) { String url ApiConstants.NEWS_LIST ?page page size20; HttpUtil.get(url, NewsListResponse.class, new HttpCallbackNewsListResponse() { Override public void onSuccess(NewsListResponse data) { if (page 1) { newsList.clear(); // 第一页清空旧数据 } newsList.addAll(data.getItems()); adapter.notifyDataSetChanged(); swipeRefresh.setRefreshing(false); // 停止下拉动画 } Override public void onError(String msg) { Toast.makeText(NewsListActivity.this, msg, Toast.LENGTH_SHORT).show(); } }); }这里有个细节值得注意page 1的时候会清空列表这是下拉刷新的标准逻辑。如果你要做“加载更多”只需要在滚动到底部的时候把page加一然后调同一个方法。参数size20控制每页条数改小一点加载快改大一点翻页少根据你的接口能力调就行。2.4 本地缓存与数据库设计源码里用 SQLite 做了一层简单的缓存主要是为了在没网的时候还能看到上次加载的内容。数据库表结构很简单就一张新闻表字段包括id、title、content、image_url、publish_time。每次网络请求成功后会把数据批量插入或者更新。// db/NewsDao.java 片段 public void saveNewsList(ListNewsBean list) { SQLiteDatabase db helper.getWritableDatabase(); db.beginTransaction(); // 开启事务批量插入更快 try { for (NewsBean bean : list) { ContentValues values new ContentValues(); values.put(id, bean.getId()); values.put(title, bean.getTitle()); values.put(content, bean.getContent()); values.put(image_url, bean.getImageUrl()); values.put(publish_time, bean.getPublishTime()); // 存在则替换不存在则插入 db.insertWithOnConflict(news, null, values, SQLiteDatabase.CONFLICT_REPLACE); } db.setTransactionSuccessful(); } finally { db.endTransaction(); } }用事务包裹批量插入是血泪经验不然几十条数据一条一条插界面会明显卡顿。CONFLICT_REPLACE保证同一条新闻不会重复存省去了先查再判断的麻烦。如果你要加收藏功能可以再建一张收藏表用新闻 ID 做关联改动量不大。3. 跑起来再说环境配置、依赖安装与首次运行排查3.1 开发环境与依赖版本这份源码基于 Android Studio 开发Gradle 插件版本和编译 SDK 在build.gradle里都有标注。我建议直接用 Android Studio 的“Open an existing project”导入不要用 Eclipse 或者命令行硬编不然光配环境就能耗掉半天。导入之后先看app/build.gradle里的compileSdkVersion和targetSdkVersion如果和你本地安装的 SDK 对不上改成本地已有的版本就行。依赖部分主要是这几个implementation com.squareup.okhttp3:okhttp:4.9.3 implementation com.google.code.gson:gson:2.8.9 implementation com.github.bumptech.glide:glide:4.12.0 implementation androidx.recyclerview:recyclerview:1.2.1 implementation androidx.swiperefreshlayout:swiperefreshlayout:1.1.0这些版本号在源码里可能略有不同但大版本对得上就能跑。如果 Gradle 同步的时候报“Failed to resolve”大概率是网络问题换个镜像源或者多试几次就行。3.2 模拟器与真机运行步骤导入项目后第一步是点“Sync Now”等 Gradle 同步完成。同步成功后连上模拟器或者真机点绿色三角形运行。首次运行可能会弹权限请求主要是网络权限和存储权限都允许就行。如果你用的是真机记得打开开发者选项里的“USB 调试”。模拟器的话建议选 API 30 以上的镜像不然有些控件可能显示异常。运行起来之后首页应该能看到一个新闻列表下拉能刷新点进去能看详情。如果列表是空的先检查网络请求有没有返回数据再看适配器有没有绑上。3.3 接口地址与数据源配置源码里的接口地址集中在ApiConstants这个类里一般长这样public class ApiConstants { public static final String BASE_URL https://你的接口域名/; public static final String NEWS_LIST BASE_URL news/list; public static final String NEWS_DETAIL BASE_URL news/detail; }你要做的就是把BASE_URL换成自己的接口地址。如果没有现成接口可以先用 Mock 数据把界面跑通再慢慢接真实数据。我一般会先在HttpUtil里加一个开关控制走本地 Mock 还是走网络这样调试起来方便很多。提示接口返回的 JSON 字段名要和bean里的属性名对得上不然 Gson 解析出来全是 null。字段名不一致的话用SerializedName注解映射一下就行。4. 避坑与常见问题排查那些第一次跑容易翻车的地方4.1 列表图片不显示或者错位现象新闻列表里的图片要么是白的要么滑几下之后图片串位了。原因Glide 加载图片是异步的如果 RecyclerView 复用 Item 的时候没有取消之前的加载请求就会出现图片错位。解决在onBindViewHolder里用Glide.with(context).load(url).into(holder.imageView)就行Glide 内部会处理复用问题。如果还是错位检查一下ImageView的scaleType是不是设成了fitXY改成centerCrop会好很多。4.2 网络请求在主线程报 NetworkOnMainThreadException现象一运行就崩日志里写着NetworkOnMainThreadException。原因Android 不允许在主线程做网络请求而源码里可能有些地方直接在主线程调了HttpUtil.get。解决OkHttp 的enqueue本身就是异步的不会阻塞主线程。但如果你在onResponse里直接更新 UI那没问题如果是在onSuccess回调里又开了子线程去更新 UI那就会崩。记住一条回调里更新 UI 是安全的因为 OkHttp 的回调默认在子线程但 Android 的 UI 操作必须在主线程所以要用runOnUiThread包一下。4.3 数据库版本升级导致崩溃现象改了表结构之后一运行就报no such column或者table already exists。原因SQLite 的onCreate只在数据库第一次创建时调用改了表结构但没改版本号旧表不会自动更新。解决在SQLiteOpenHelper的构造函数里把版本号加一然后在onUpgrade里写drop table再重建或者写alter table加字段。开发阶段直接删掉 App 重装也行但上线前一定要写好升级逻辑。4.4 Gradle 同步慢或者依赖下载失败现象Gradle 同步卡在“Downloading”半天不动最后报超时。原因默认的 Maven 仓库在国外网络不稳定。解决在项目根目录的build.gradle里把google()和mavenCentral()换成国内镜像源或者用阿里云的 Maven 仓库。改完之后点“Sync Now”重新同步一般就能解决。4.5 详情页传参为空导致白屏现象点列表项进详情页页面是白的没有内容。原因Intent 传参的时候 key 写错了或者详情页拿参数的时候没判空。解决在详情页的onCreate里先检查getIntent().getStringExtra(news_id)是不是 null是 null 就直接finish()返回。传参的时候用常量做 key别手写字符串避免拼错。5. 二次开发能改什么从换皮肤到接自己的接口5.1 替换数据源与接口适配这份源码最大的改造点就是换接口。你只需要改ApiConstants里的BASE_URL然后把bean里的字段和你的接口对齐。如果字段名不一样用SerializedName注解映射。比如你的接口返回的是article_title而实体类里叫title就写成SerializedName(article_title) private String title;这样 Gson 解析的时候会自动把article_title的值赋给title。改完实体类之后列表和详情的展示逻辑基本不用动因为 Adapter 里绑定的还是getTitle()这些方法。5.2 增加分类 Tab 与频道切换源码目前只有一个新闻列表如果你想加分类 Tab可以用TabLayoutViewPager2的组合。每个 Tab 对应一个FragmentFragment 里复用现有的列表逻辑只是请求参数里多传一个category字段。改动量不大但能让 App 看起来完整很多。// 在 Fragment 里根据分类请求不同数据 String url ApiConstants.NEWS_LIST ?category category page page;category的值可以从 Tab 的标题映射过来比如“科技”对应tech“体育”对应sports。这样切换 Tab 的时候每个 Fragment 各自加载自己的数据互不干扰。5.3 加入收藏与离线阅读收藏功能需要新建一张收藏表字段至少包括news_id和user_id如果有多用户的话。点击收藏按钮的时候往表里插一条记录取消收藏就删掉。离线阅读则是把新闻详情的内容存到本地没网的时候直接从数据库读。// 收藏操作 public void addFavorite(String newsId) { SQLiteDatabase db helper.getWritableDatabase(); ContentValues values new ContentValues(); values.put(news_id, newsId); db.insert(favorite, null, values); } // 判断是否已收藏 public boolean isFavorite(String newsId) { SQLiteDatabase db helper.getReadableDatabase(); Cursor cursor db.query(favorite, null, news_id?, new String[]{newsId}, null, null, null); boolean exists cursor.getCount() 0; cursor.close(); return exists; }这两个方法写起来简单但要注意在详情页的onResume里刷新收藏状态不然从收藏列表点进去再返回按钮状态可能不对。5.4 一个我踩过的坑别急着换架构我第一次改这个项目的时候觉得 MVC 太老土上来就想换成 MVVM结果改了两天界面跑不起来了数据也不刷新了。后来老老实实退回去先把接口换掉、把分类加上等整个流程跑顺了再一个模块一个模块地往 MVVM 迁。从那以后我每次拿到别人的源码都强制自己先跑通、再小改、最后才大动。希望这个习惯也能帮到你。本文还有配套的精品资源点击获取