摘要 :本文将详细介绍如何基于 Android 原生开发一套轻量级新闻资讯客户端,涵盖用户登录注册、Tab 分类浏览、新闻列表展示、详情页查看等核心功能。项目采用 OkHttp + Gson 进行网络通信与数据解析,使用 ViewPager + Fragment 实现 Tab 分页架构,Glide 负责图片加载,SQLite 完成本地用户存储。文末附完整源码结构与运行截图。
一、项目概述
1.1 项目背景
在移动互联网时代,新闻资讯类 App 是最常见的应用场景之一。本项目旨在实现一个功能完备、结构清晰的 Android 新闻客户端,帮助开发者掌握以下核心技术点:
- OkHttp 网络请求封装(GET / POST / PUT / DELETE / 文件上传)
- ViewPager + TabLayout + Fragment 的经典 Tab 分页模式
- Gson JSON 数据解析与 JavaBean 映射
- Glide 图片加载框架的使用
- SQLite 本地数据库持久化
- SharedPreferences 登录态管理
1.2 技术栈总览
| 技术 | 版本 | 用途 |
|---|---|---|
| OkHttp | 4.4.0 | HTTP 网络请求核心库 |
| okhttp-sse | 4.4.0 | Server-Sent Events 服务端推送(预留) |
| Gson | 2.8.6 | JSON 序列化/反序列化 |
| Glide | 4.11.0 | 图片异步加载与缓存 |
| glide-transformations | 3.3.0 | 图片圆角/模糊等变换效果 |
| PictureSelector | v2.6.0 | 图片选择器(头像上传) |
| RxJava + RxAndroid | 2.x | 响应式编程(依赖引入) |
| Material Components | 1.4.0 | TabLayout 等 Material Design 组件 |
1.3 项目信息
| 属性 | 值 |
|---|---|
| ApplicationId | com.example.news |
| compileSdk | 30 |
| minSdk | 21(Android 5.0) |
| targetSdk | 28 |
| Java 源文件 | 23 个 |
二、系统架构设计
2.1 整体架构
项目采用经典的 MVC(Model-View-Controller) 分层架构:
┌─────────────────────────────────────────────────────┐
│ View(视图层) │
│ OpenActivity / MainActivity / LoginActivity │
│ RegisterActivity / DetailActivity │
│ + BlankFragment1(复用型 Fragment) │
│ + PagerAdapter / NewsAdapter │
├─────────────────────────────────────────────────────┤
│ Controller(控制层) │
│ Okhttp(单例网络封装) │
│ DBUser(本地用户 CRUD) │
├─────────────────────────────────────────────────────┤
│ Model(数据层) │
│ entity: User, Stuff │
│ bean: NewsList, Result, UserResult ... │
│ SPUtils(SharedPreferences 封装) │
└─────────────────────────────────────────────────────┘
2.2 目录结构
com.example.order/
├── MyApplication.java # 自定义 Application
├── activity/
│ ├── OpenActivity.java # 启动页(LAUNCHER)
│ ├── LoginActivity.java # 登录页
│ ├── RegisterActivity.java # 注册页
│ ├── MainActivity.java # 新闻主页(Tab + ViewPager)
│ └── DetailActivity.java # 新闻详情页
├── fragment/
│ └── BlankFragment1.java # 新闻分类 Fragment(4个Tab复用)
├── adapter/
│ ├── PagerAdapter.java # ViewPager Fragment 适配器
│ └── NewsAdapter.java # 新闻列表 ListView 适配器
├── database/
│ ├── DBHelper.java # SQLiteOpenHelper 单例
│ └── DBUser.java # 用户表 CRUD 操作
├── entity/
│ ├── User.java # 用户实体
│ └── Stuff.java # 物品实体
├── bean/
│ ├── NewsList.java # 新闻列表响应体
│ ├── Result.java # 通用结果响应体
│ ├── UserResult.java # 单用户响应体
│ ├── UsersListResult.java # 用户列表响应体
│ └── UploadFileResult.java # 文件上传响应体
└── utils/
├── Okhttp.java # OkHttp 网络封装(单例)
├── JsonUtils.java # Gson 解析工具
├── SPUtils.java # SharedPreferences 持久化
├── GlideEngine.java # PictureSelector 图片引擎
└── UserTypeEnum.java # 用户类型枚举
三、核心模块实现
3.1 OkHttp 网络请求封装(单例模式)
网络层是整个 App 的核心基础设施。我们采用单例模式封装 OkHttp,统一管理所有 HTTP 请求:
java
public class Okhttp {
private String http = "http://192.168.3.6:8080/news/";
public static final String user = "user";
public static final String NEWS = "news";
public static final String fileUpload = "file/upload";
private static Okhttp netokhttp;
public final OkHttpClient client;
private Okhttp() {
client = initOkHttpClient();
}
// 单例获取
public static Okhttp getInstance() {
if (netokhttp == null) {
netokhttp = new Okhttp();
}
return netokhttp;
}
// 初始化 OkHttpClient(配置超时)
private OkHttpClient initOkHttpClient() {
return new OkHttpClient.Builder()
.readTimeout(2000, TimeUnit.MILLISECONDS) // 读取超时 2s
.connectTimeout(3000, TimeUnit.MILLISECONDS) // 连接超时 3s
.build();
}
}
设计要点:
- 整个应用只维护一个
OkHttpClient实例,避免资源浪费 - 连接超时 3 秒、读取超时 2 秒,适配移动端弱网环境
- 所有请求均采用异步回调 (
enqueue),不阻塞主线程
封装的 RESTful 方法
java
// POST - 新增数据
public void postJson(String url, String json, Callback callback) {
RequestBody body = RequestBody.create(
MediaType.parse("application/json; charset=utf-8"), json);
Request request = new Request.Builder()
.url(http + url).post(body).build();
getInstance().initOkHttpClient().newCall(request).enqueue(callback);
}
// GET - 获取列表
public void getJson(String url, Callback callback) {
Request request = new Request.Builder()
.url(http + url).get().build();
getInstance().initOkHttpClient().newCall(request).enqueue(callback);
}
// PUT - 更新数据
public void putJson(String url, String json, Callback callback) { ... }
// DELETE - 删除数据
public void deleteJson(String url, String id, Callback callback) { ... }
文件上传(Multipart)
java
public void uploadFile(String path, Callback callback) {
File file = new File(path);
RequestBody body = new MultipartBody.Builder()
.setType(MultipartBody.FORM)
.addFormDataPart("file", file.getName(),
MultipartBody.create(MediaType.parse("multipart/form-data"), file))
.build();
Request request = new Request.Builder()
.url(http + fileUpload).post(body).build();
getInstance().initOkHttpClient().newCall(request).enqueue(callback);
}
新闻数据缓存与分类过滤
这是本项目的关键设计------一次请求,多处使用:
java
private List<NewsList.DataBean> data = new ArrayList<>();
// 一次性拉取全部新闻并缓存到内存
public void initList(stuffListInterface listInterface) {
Okhttp.getInstance().getJson(Okhttp.NEWS, new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
String res = response.body().string();
NewsList result = (NewsList) JsonUtils.fromJson(res, NewsList.class);
if (result != null && result.getCode() == 0) {
data.clear();
data.addAll(result.getData()); // 缓存到内存
}
}
});
}
// 按新闻分类过滤(本地新闻/娱乐新闻/体育新闻/科技新闻)
public ArrayList<NewsList.DataBean> getNews(String type) {
ArrayList<NewsList.DataBean> list = new ArrayList<>();
for (NewsList.DataBean bean : data) {
if (bean.getType().equalsIgnoreCase(type)) {
list.add(bean);
}
}
return list;
}
优势 :MainActivity 启动时调用一次 initList(),4 个 Tab 页面各自通过 getNews(type) 从缓存中按分类取数据,避免重复网络请求。
3.2 ViewPager + TabLayout 分页架构
MainActivity 是 App 的主界面,采用 TabLayout + ViewPager + Fragment 的经典组合:

java
public class MainActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
TabLayout tabLayout = findViewById(R.id.tab_layout);
// 添加 4 个分类 Tab
tabLayout.addTab(tabLayout.newTab().setText("本地新闻"));
tabLayout.addTab(tabLayout.newTab().setText("娱乐新闻"));
tabLayout.addTab(tabLayout.newTab().setText("体育新闻"));
tabLayout.addTab(tabLayout.newTab().setText("科技新闻"));
tabLayout.setTabGravity(TabLayout.GRAVITY_FILL);
ViewPager viewPager = findViewById(R.id.view_pager);
PagerAdapter adapter = new PagerAdapter(
getSupportFragmentManager(), tabLayout.getTabCount());
viewPager.setAdapter(adapter);
// Tab 与 ViewPager 联动
viewPager.addOnPageChangeListener(
new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
tabLayout.setOnTabSelectedListener(new TabLayout.OnTabSelectedListener() {
@Override
public void onTabSelected(TabLayout.Tab tab) {
viewPager.setCurrentItem(tab.getPosition());
}
// ...
});
}
}
PagerAdapter ------ Fragment 适配器
每个 Tab 对应一个 BlankFragment1 实例,通过构造参数区分新闻类型:
java
public class PagerAdapter extends FragmentStatePagerAdapter {
int mNumOfTabs;
public PagerAdapter(FragmentManager fm, int NumOfTabs) {
super(fm);
this.mNumOfTabs = NumOfTabs;
}
@Override
public Fragment getItem(int position) {
switch (position) {
case 0: return BlankFragment1.newInstance("本地新闻");
case 1: return BlankFragment1.newInstance("娱乐新闻");
case 2: return BlankFragment1.newInstance("体育新闻");
case 3: return BlankFragment1.newInstance("科技新闻");
default: return null;
}
}
@Override
public int getCount() { return mNumOfTabs; }
}
3.3 新闻列表 Fragment
BlankFragment1 是所有 Tab 共用的 Fragment 模板,通过 newInstance(type) 工厂方法传入分类参数:

java
public class BlankFragment1 extends Fragment {
String type = "";
private ArrayList<NewsList.DataBean> data = new ArrayList<>();
public static BlankFragment1 newInstance(String type) {
BlankFragment1 fragment = new BlankFragment1();
Bundle args = new Bundle();
args.putString("type", type);
fragment.setArguments(args);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
type = getArguments().getString("type"); // 获取分类类型
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
View v = inflater.inflate(R.layout.fragment_blank__cart, container, false);
ListView listview = v.findViewById(R.id.list);
// 从 OkHttp 缓存中按分类获取数据
data.addAll(Okhttp.getInstance().getNews(type));
NewsAdapter adapter = new NewsAdapter(getContext(),
R.layout.layout_news_item, data);
listview.setAdapter(adapter);
// 点击跳转详情页
listview.setOnItemClickListener((adapterView, view, i, l) -> {
Intent intent = new Intent(getActivity(), DetailActivity.class);
intent.putExtra("id", data.get(i));
getActivity().startActivity(intent);
});
return v;
}
}
3.4 新闻列表适配器(NewsAdapter)
使用 ArrayAdapter + 自定义布局渲染新闻条目,集成 Glide 加载缩略图:
java
public class NewsAdapter extends ArrayAdapter<NewsList.DataBean> {
private Context context;
public NewsAdapter(@NonNull Context context, int resource,
@NonNull ArrayList<NewsList.DataBean> objects) {
super(context, resource, objects);
this.context = context;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
NewsList.DataBean s = getItem(position);
View view = LayoutInflater.from(getContext())
.inflate(R.layout.layout_news_item, parent, false);
TextView title = view.findViewById(R.id.title);
TextView content = view.findViewById(R.id.content);
ImageView pic = view.findViewById(R.id.pic);
title.setText(s.getTitle());
content.setText(s.getContent());
// Glide 异步加载图片
if (!TextUtils.isEmpty(s.getPath())) {
Glide.with(context).load(s.getPath()).into(pic);
}
return view;
}
}
3.5 新闻详情页
点击列表项后跳转到 DetailActivity,展示完整新闻内容:
java
public class DetailActivity extends AppCompatActivity {
TextView title, time, content;
ImageView image;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_detail);
title = findViewById(R.id.title);
time = findViewById(R.id.time);
content = findViewById(R.id.content);
image = findViewById(R.id.image);
Intent intent = getIntent();
NewsList.DataBean item = (NewsList.DataBean)
intent.getSerializableExtra("id");
if (item != null) {
title.setText(item.getTitle());
time.setText(item.getTime());
content.setText(item.getContent());
// Glide 加载大图
if (!TextUtils.isEmpty(item.getPath())) {
Glide.with(this).load(item.getPath()).into(image);
}
}
}
public void back(View view) { finish(); }
}
3.6 用户登录模块
登录页支持记住密码 和自动登录 功能,通过 SharedPreferences 持久化:

java
public class LoginActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_login);
EditText etAccount = findViewById(R.id.et_account);
EditText etPassword = findViewById(R.id.et_password);
// 记住密码逻辑
CheckBox checkBoxPsw = findViewById(R.id.checkBoxPsw);
boolean last = (Boolean) SPUtils.get(this, SPUtils.REMEMBER_PASSWORD, false);
checkBoxPsw.setChecked(last);
if (last) {
etAccount.setText((CharSequence) SPUtils.get(this, SPUtils.LAST_NAME, ""));
etPassword.setText((CharSequence) SPUtils.get(this, SPUtils.LAST_PSW, ""));
}
// 登录按钮事件
findViewById(R.id.btn_login).setOnClickListener(v -> {
String account = etAccount.getText().toString();
String password = etPassword.getText().toString();
// 调用接口验证账号密码
Okhttp.getInstance().byAccount(account, new Callback() {
@Override
public void onResponse(Call call, Response response) throws IOException {
String res = response.body().string();
UserResult result = (UserResult) JsonUtils.fromJson(res, UserResult.class);
if (result != null && result.getCode() == 0
&& result.getData().getPassword().equalsIgnoreCase(password)) {
// 验证成功 → 跳转主页
Intent intent = new Intent(LoginActivity.this, MainActivity.class);
startActivity(intent);
finish();
}
}
});
});
// 预加载新闻数据
Okhttp.getInstance().initList(null);
}
}
注册页(RegisterActivity)提供用户名 + 密码 + 确认密码的表单注册流程,数据提交至服务端并存入本地 SQLite。
四、数据模型设计
4.1 新闻数据 Bean(NewsList)
服务端返回的 JSON 结构直接映射为 JavaBean:
json
{
"code": 0,
"message": "成功",
"data": [{
"id": "1",
"title": "新闻标题",
"time": "2024-10-19",
"content": "新闻正文内容...",
"type": "本地新闻",
"path": "https://example.com/image.jpg"
}]
}
对应的 Java 类:
java
public class NewsList implements Serializable {
private int code;
private String message;
private List<DataBean> data;
public static class DataBean implements Serializable {
private String id; // 新闻ID
private String title; // 标题
private String time; // 发布时间
private String content; // 正文内容
private String type; // 分类(本地/娱乐/体育/科技)
private String path; // 配图 URL
// getter & setter ...
}
// getter & setter ...
}
4.2 本地数据库设计
使用 SQLite 存储用户信息,数据库名 newsApp.db:
用户表 user:
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 用户 ID |
| name | TEXT | NOT NULL | 用户名 |
| password | TEXT | NOT NULL | 密码 |
| user_type | TEXT | NOT NULL | 用户类型 |
新闻表 news:
| 字段 | 类型 | 约束 | 说明 |
|---|---|---|---|
| id | INTEGER | PRIMARY KEY AUTOINCREMENT | 新闻 ID |
| title | TEXT | NOT NULL | 标题 |
| time | TEXT | NOT NULL | 发布时间 |
| content | TEXT | NOT NULL | 内容 |
| type | TEXT | NOT NULL | 分类 |
| path | TEXT | NOT NULL | 图片路径 |
预置管理员账号:admin / 123。
五、关键业务流程
5.1 应用启动流程
OpenActivity(启动页,usesCleartextTraffic=true)
↓
检查 SharedPreferences 登录态
↓ 未登录 ↓ 已登录
LoginActivity → MainActivity(直接进入)
5.2 新闻加载流程
MainActivity.onCreate()
↓
PagerAdapter 创建 4 个 BlankFragment1(各带不同 type 参数)
↓
LoginActivity 中已调用 Okhttp.initList() 预加载全部新闻到内存缓存
↓
每个 BlankFragment1.onCreateView()
↓
Okhttp.getInstance().getNews(type) → 按分类过滤缓存数据
↓
NewsAdapter 渲染 ListView
↓
点击条目 → Intent 携带 DataBean → DetailActivity 展示详情
5.3 登录流程
输入账号密码 → 前端非空校验
↓
Okhttp.byAccount(account) → GET 请求服务端
↓
Gson 解析 UserResult → 校验密码
↓
成功 → DBUser 缓存用户 → SPUtils 保存登录态 → 跳转 MainActivity
失败 → Toast 提示错误原因
六、权限与清单配置
AndroidManifest.xml 关键配置
xml
<!-- 权限声明 -->
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.CAMERA" />
<!-- 允许明文 HTTP(开发阶段) -->
<application
android:usesCleartextTraffic="true"
android:name=".MyApplication">
<!-- OpenActivity 为启动页 -->
<activity android:name=".activity.OpenActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
</application>
注意 :
usesCleartextTraffic="true"允许 HTTP 明文传输,生产环境应改为 HTTPS。