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。


相关推荐
素师良码16 小时前
第5篇:显示驱动必备调试工具浅析
android
维克兜率天16 小时前
【维克】配对交易的季节性:哪些品种适合长拿?
android·开发语言·笔记·python·算法·kotlin·量化
墨天梦16 小时前
B06_XML控件布局与ViewBinding
android·kotlin
晚风叙码17 小时前
MySQL 表的操作:从建表到删表,一篇讲清楚
android·mysql·oracle
事圆则缓18 小时前
Android 性能优化常见工具与使用场景:从症状到证据的排查指南
android·性能优化
avi911118 小时前
Unity 非后台,非自动构建,但统计报表 BuildReport ,构建自动化系统C#
android·webserver·ios打包·admin·c#后台·unity打包系统·unity自动构建
新鲜势力呀18 小时前
PHP 定时任务系统实战:从 Cron 混乱执行到任务调度中心 + Redis队列 + 失败重试
android·java·redis
YF021119 小时前
Android 16系统 如何修改 eth0 IP ?
android
其实防守也摸鱼19 小时前
渗透测试学习计划(全栈综合 · 零基础进阶)
android·数据库·学习·安全·oracle·自动化·学习方法
释厄62320 小时前
BSD 简单真理循环论——简单真理 × 复杂循环=大一统
android·开发语言·kotlin