Android 新闻资讯客户端实战:从零搭建一个完整的新闻 App

摘要 :本文将详细介绍如何基于 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。


相关推荐
三8443 小时前
PHP 绕过污点追踪构造 Webshell:反序列化引用复用与 Trait 可变函数 RCE
android
石头猫灯3 小时前
WordPress wp2shell漏洞断点逐过程到注入点
android·学习
Android打工仔4 小时前
Kotlin 协程源码解析(五):BaseContinuationImpl.resumeWith() —— Continuation 链是如何被展开的?
android·kotlin
存在morning4 小时前
【PySpark 学习笔记 三】DataFrame API 入门
android·java·数据库
YSoup5 小时前
2026 安卓面试助手APP(安卓八股、题库)
android·面试·职场和发展
xcyxiner5 小时前
flutter 运行到模拟器上
android·前端·flutter
Android-Flutter5 小时前
android Glide 源码流程分析
android·kotlin
Android-Flutter5 小时前
android Glide 使用详解
android·kotlin
恋猫de小郭6 小时前
Dart 3.13 的到底改了什么?为什么很重要?有什么坑?
android·前端·flutter