【移动端开发】“明日头条APP”

1 系统概述

1.1研究背景

随着移动互联网的快速发展,智能手机已经成为人们获取信息的主要工具之一。新闻作为信息的重要组成部分,其传播方式和获取途径也发生了显著变化。传统的纸质媒体逐渐被网络媒体所取代,用户能够通过各种应用程序实时获取最新的新闻动态。 在这种背景下,各类新闻客户端应运而生,它们提供了便捷的新闻获取渠道。然而,众多新闻客户端在内容、形式和用户体验上各有差异,如何设计一款功能完善、易于操作且能满足用户需求的新闻客户端APP,成为了一个值得研究的课题。本课程设计旨在通过分析现有新闻客户端的特点,结合网络爬虫技术,实现一款基于Android平台的新闻客户端APP,以解决用户在获取新闻时的信息碎片化问题,提高新闻信息的获取效率。

1.2研究意义

通过使用网络爬虫技术,能够有效地获取和整合来自不同新闻源的信息,将原本分散的新闻内容进行归纳和分类。这一过程不仅提升了对数据处理能力的认识,也为学习和掌握现代计算机科学中的数据挖掘和信息检索等技术提供了机会。 随着社会信息的高速传播,大量用户需要一个集成化的新闻平台以便于获取即时信息。本项目的实现可以帮助用户更方便地浏览新闻、获取知识、拓宽视野,并为开发者创造新的市场机会,提升其在新闻行业的竞争力。 新闻是推动社会进步的重要力量,一个优秀的新闻客户端不仅能够提高公众获取信息的效率,还有助于提升公民的新闻素养和社会责任感。在信息爆炸的时代,为用户提供准确、及时、有价值的新闻信息,有助于构建良好的社会舆论环境,促进社会的和谐发展。

2 系统设计

2.1 关键技术

在这里插入图片描述

2.2 系统设计

2.2.1 系统功能模块

在这里插入图片描述

模块功能说明

在这里插入图片描述

2.2.2 数据库设计

在这里插入图片描述

3 系统实现

3.1 数据模型

3.1.1 NewsURL

管理和构建不同类型新闻的 API 请求 URL

public class NewsURL {
// 用于存储访问新闻 API 所需的 APP KEY
public static String key = "065405525167d8e51990614a378fe7af";  // api - key
public static String info_url = "http://v.juhe.cn/toutiao/index?key="+ key + "&type=";
// 头条
public static String headline_url = info_url + "top";
// 社会
public static String society_url = info_url + "shehui";
}

3.1.2 NewsType

封装与新闻类型相关的信息, 实现了 Serializable 接口以便于在对象序列化时使用, 比如在 Android 中通过 Intent 传递数据。提供了静态方法来获取所有新闻类型的集合,用于后续填充 UI 元素。

public class TypeBean implements Serializable{
    private int id;
    private String title;
    private String url;
    private boolean isShow;

    public static List<TypeBean> getTypeList(){
        List<TypeBean> mDatas = new ArrayList<>();

        TypeBean tb1 = new TypeBean(1, "头条", NewsURL.headline_url, true);
        TypeBean tb2 = new TypeBean(2, "社会", NewsURL.society_url, true);
        mDatas.add(tb1);
        mDatas.add(tb2);
        return mDatas;
    }
}

3.1.3 NewsInfo

封装从新闻 API 返回的新闻信息,使用嵌套类结构来组织数据。根据API获取到的JSON格式数据编写代码。爬虫获得的JSON数据如下:

{
    "reason": "success!",
    "result": {
        "stat": "1",
        "data": [
            {
                "uniquekey": "564cdaad64ae35f817f92c83d7874753",
                "title": "开展“净网2024”专项行动 厦门警方侦破一起广告推广型网络黑灰产犯罪案",
                "date": "2024-09-18 13:57:00",
                "category": "头条",
                "author_name": "人民网,供稿:人民资讯",
                "url": "https://mini.eastday.com/mobile/240918135748337907540.html",
                "thumbnail_pic_s": "",
                "is_content": "1"
            }
        ],
        "page": "1",
        "pageSize": "30"
    },
    "error_code": 0
}

新闻信息封装类:

public class InfoBean {
    private String reason;
    private ResultBean result;
    private int error_code;

    public static class ResultBean {
        private String stat;
        private List<DataBean> data;

        public static class DataBean {
            private String uniquekey;
            private String title;
            private String date;
            private String category;
            private String author_name;
            private String url;
            private String thumbnail_pic_s;
            private String thumbnail_pic_s02;
            private String thumbnail_pic_s03;
         }
    }
}

3.2 数据库操作

3.2.1 DBOpenHelper

DBOpenHelper是用于创建和管理 SQLite 数据库的子类,负责创建和初始化 SQLite 数据库,定义表结构和插入初始数据。在数据库版本升级时进行表结构更新或数据迁移。

// 创建和管理 SQLite 数据库的子类
public class DBOpenHelper extends SQLiteOpenHelper{
    public DBOpenHelper(Context context) {
        // 指定数据库名称为 "info.db" 版本号为 1
        super(context, "info.db", null, 1);
    }

    /*
    create table itype(
             id integer primary key,shujus
             title varchar(10) unique not null,
             url text not null,
             isshow varchar(10) not null
    );
    */
    @Override
    public void onCreate(SQLiteDatabase db) {
        String sql = "create table itype(id integer primary key,title varchar(10) unique not null,url text not null,isshow varchar(10) not null)";
        db.execSQL(sql);

        String insertSql = "insert into itype values(?,?,?,?)";
        db.execSQL(insertSql,new Object[]{1,"头条", NewsURL.headline_url,"true"});
        db.execSQL(insertSql,new Object[]{2,"社会",NewsURL.society_url,"true"});
    }

    // 在数据库版本升级时进行表结构更新或数据迁移
    @Override
    public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) {

    }
}

3.2.2 DBManager

封装对 SQLite 数据库的操作。提供初始化数据库、查询数据和更新数据的方法。

public static SQLiteDatabase database; // 保存数据库的可写实例

    // 创建一个 DBOpenHelper 实例并获取一个可写的数据库对象
    public static void initDB(Context context) {
        DBOpenHelper helper = new DBOpenHelper(context);
        database = helper.getWritableDatabase();
    }

    // 从 SQLite 数据库中查询所有的类型数据并将其转化为一个 TypeBean 对象列表
    public static List<TypeBean> getAllTypeList() {
    }

    // 获取所有要求显示的内容的集合
    public static List<TypeBean> getSelectedTypeList() {
    }

    // 根据输入的 typeList 更新数据库中的isshow字段
    public static void updateTypeList(List<TypeBean> typeList) {
    }
}

3.3 适配器类

3.3.1 AddItem

展示新闻类别的列表,结合类型数据和 UI 元素(文本和图像)。根据 TypeBean 中的 isShow来决定图标的显示状态。如果用户点击某一项,该项的状态将被切换,并相应更新图标。点击事件通过匿名内部类实现,允许动态更新 UI。

在这里插入图片描述

// 添加新闻类别的窗口 xml:文本 + 对号

// 自定义适配器类public class AddItemAdapter extends BaseAdapter {
    // 上下文对象,用于获取资源和服务
    Context context;
    // 数据源,存储 TypeBean 对象的列表
    List<TypeBean> mDatas;
    // 创建视图并绑定数据
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // 使用 LayoutInflater 加载自定义布局文件 item_add_lv
        convertView = LayoutInflater.from(context).inflate(R.layout.item_add_lv, null);
        // 初始化convertview中的控件
        TextView nameTv = convertView.findViewById(R.id.item_add_tv); // 获取文本视图
        final ImageView iv = convertView.findViewById(R.id.item_add_iv); // 获取图片视图
        // 获取指定位置的 TypeBean 对象
        final TypeBean typeBean = mDatas.get(position);
        // 设置文本视图的内容为 TypeBean 的标题
        nameTv.setText(typeBean.getTitle());
        // 根据 TypeBean 的 show 状态设置图片视图的资源
        // 为了避免用户全都不选中的情况,不符合新闻的展示习惯,所以前两个项必须展示
        if (position == 0 || position == 1) {
            iv.setVisibility(View.INVISIBLE); // 隐藏前两个项的勾选框
        } else {
            iv.setVisibility(View.VISIBLE); // 显示其他项的勾选框
            // 设置点击事件监听器
            convertView.setOnClickListener(new View.OnClickListener() {
                @Override
                public void onClick(View v) {
                    // 切换选中状态
                    // 更新图片视图的资源
             }
            });
        }
        // 返回当前视图
        return convertView;
    }
}

3.3.2 InfoItem

展示新闻数据的列表项,将 InfoBean.ResultBean.DataBean 类型的数据渲染成特定的布局形式。通过内部类 ViewHolder 缓存视图组件的引用,减少性能损耗。通过 ImageLoader 来异步加载网络图片,并显示在对应的 ImageView 中,考虑图片加载失败和 URI 无效的情况。根据每个 DataBean 实例中的图片地址决定是否显示对应的 ImageView。

在这里插入图片描述

// 处理新闻数据DataBean 将这些数据以自定义的布局形式展示
public class InfoItemAdapter extends BaseAdapter {
    Context context; // 当前应用的上下文
    List<InfoBean.ResultBean.DataBean> mDatas; // 新闻正文内容
    ImageLoader imageLoader; // 异步加载和缓存图片
    DisplayImageOptions options;   // 图片加载配置信息
    public InfoItemAdapter(Context context, List<InfoBean.ResultBean.DataBean> mDatas) {
        this.context = context;
        this.mDatas = mDatas;
        imageLoader = ImageLoader.getInstance();
        options = new DisplayImageOptions.Builder();
    }
    // 创建或重用视图来展示数据
    @Override
    public View getView(int position, View convertView, ViewGroup parent) {
        // 在列表项第一次被创建时找到所有的视图元素并存储在 ViewHolder 对象中
        // 在后续的显示或重用过程中直接使用这些已存储的引用 避免重复的 findViewById 调用
        class ViewHolder {
            TextView titleTv, sourceTv, timeTv; // 新闻标题 新闻来源 发布时间
            ImageView iv1, iv2, iv3; // 新闻图片缩略图
            public ViewHolder(View view) {
       	    }
        }
        ViewHolder holder = null;
        // 1. 没有可复用的视图
        if (convertView == null) {
            // 2. 将自定义的布局文件R.layout.item_newsfrag_lv转换成新的视图
            convertView = LayoutInflater.from(context).inflate(R.layout.item_newsfrag_lv, null);
            // 初始化ViewHolder对象来持有视图的子视图引用
            holder = new ViewHolder(convertView);
            // 将ViewHolder对象作为标签(tag)存储在convertView上 以便后续复用
            convertView.setTag(holder);
            // 3. 有可复用的视图
        } else {
            // 直接从视图的标签中获取ViewHolder对象
            holder = (ViewHolder) convertView.getTag();
        }
        // 获取指定位置的数据源
        // 获取三张图片的地址

        return convertView;
    }
}

3.3.3 NewsInfo

管理ViewPager 的多个片段。FragmentStatePagerAdapter该适配器可以动态加载和释放 Fragments, 在 Fragment 不可见时销毁它们以节省内存。重写 getItemPosition 方法使得对数据集的任何更改都能立即反映到 ViewPager 中。getPageTitle为每个页面提供对应的标题。

在这里插入图片描述

// 扩展了 FragmentStatePagerAdapter 来管理 ViewPager 中的多个片段(Fragments)
// 支持在 ViewPager 中动态加载和释放 Fragment
public class NewsInfoAdapter extends FragmentStatePagerAdapter {
    Context context;
    List<Fragment> fragmentList;
    List<TypeBean> typeBeanList;
    // 指示所有页面都可能改变 Fragment 将在数据集更改时被重新创建
    @Override
    public int getItemPosition(@NonNull Object object) {
        return PagerAdapter.POSITION_NONE;
    }   
    // 返回指定位置的标题
    @Nullable
    @Override
    public CharSequence getPageTitle(int position) {
        TypeBean typeBean = typeBeanList.get(position);
        String title = typeBean.getTitle();
        return title;
    }
}

3.4 Activity模块

3.4.1 MainActivity

显示一个包含多个页面的 ViewPager,用户通过标签条切换不同的页面, 点击添加按钮跳转到一个新的活动来添加新闻类别。初始化界面元素和适配器,获取布局中的视图控件,设置点击事件监听器,初始化片段并设置适配器给 ViewPager,关联 tabStrip 和 ViewPager,使得用户滑动时标签会自动更新。加载用户选择的类型列表,创建对应的 NewsInfoFragment 实例,并将其加入到 fragmentList 中。每个 Fragment 通过 Bundle 接收参数,以便能够根据类型显示不同的信息。当用户点击添加按钮时,启动 AddItemActivity。当 Activity 从停止状态重新启动时,清空之前的数据,重新加载最新的类型信息,更新适配器和标签条。

在这里插入图片描述

public class MainActivity extends AppCompatActivity implements View.OnClickListener {
    ViewPager mainViewPager; //显示多个页面的视图
    PagerSlidingTabStrip tabStrip;//用户界面中的标签条,用于导航
    ImageView addIv;//用于添加项目的图标
    List<Fragment> fragmentList;  // 存储所有要在 ViewPager 中显示的 Fragment
    List<TypeBean> selectTypeList;  // 存储 用户选择的类型列表
    private NewsInfoAdapter adapter;// 设置 ViewPager 内容的适配器
    public void init() {
        addIv.setOnClickListener(this);  //添加点击事件的监听
        fragmentList = new ArrayList<>();
        selectTypeList = new ArrayList<>();
        // 初始化页面
        initPager();
        // 创建适配器对象
        // getSupportFragmentManager():获取支持库中的 Fragment 管理器 允许在 Fragment 中进行操作 比如添加、替换或移除
        adapter = new NewsInfoAdapter(getSupportFragmentManager(), this, fragmentList, selectTypeList);
        // 设置适配器
        // 使得ViewPager知道如何 获取和显示与之相关联的Fragment
        // 适配器负责创建和管理Fragment 向ViewPager提供它们
        mainViewPager.setAdapter(adapter);
        // 关联TabStrip和ViewPager
        // 当用户滑动ViewPager 选中的标签也会发生变化 使得用户可以通过点击标签条快速切换到相应的页面
        // tabStrip根据ViewPager中的Fragment数量动态生成对应的标签
        tabStrip.setViewPager(mainViewPager);
    }
    // 初始化页面的函数
    private void initPager() {
        // 1. 获取用户选择的类型列表
        List<TypeBean> typeList = DBManager.getSelectedTypeList();
        selectTypeList.addAll(typeList);
        for (int i = 0; i < selectTypeList.size(); i++) {
            // 2.1 为每个类型创建一个 NewsInfoFragment 实例
            // 2.2 将类型数据通过 Bundle 传递给 NewsInfoFragment实例 并添加到 fragmentList 中
            Bundle bundle = new Bundle();
            bundle.putSerializable("type", typeBean);
            infoFragment.setArguments(bundle);
            fragmentList.add(infoFragment);
        }
    }
    // 设置点击+号按钮后的操作 -- 跳转到添加类别新页面
    @Override
    public void onClick(View v) {
        switch (v.getId()) {
            case R.id.main_iv_add:
                // 创建一个 Intent 从当前 MainActivity 跳转到 AddItemActivity(用于添加新闻类别)
                Intent intent = new Intent(MainActivity.this, AddItemActivity.class);
                startActivity(intent);
                break;
        }
    }
}

3.4.2 DescActivity

在一个 WebView 中加载和显示网页内容。初始化活动,设置布局 activity_desc。获取 WebView 组件。从 Intent 中提取 URL 字符串。设置 WebView 的各种属性,如启用 JavaScript、缓存方式等。加载指定的 URL。重写 shouldOverrideUrlLoading 方法,以确保所有链接都在同一个 WebView 内打开,而不是跳转到外部浏览器。当用户按下后退键时,如果 WebView 有历史记录,则调用 goBack() 返回上一个页面。如果没有历史记录,调用父类的方法处理其他按键。

在这里插入图片描述

// 在 WebView 中显示网页内容
public class DescActivity extends AppCompatActivity {
    WebView descWeb;
    String url;
    // 设置活动的布局 并获取意图数据中的 URL 用于加载网页
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        descWeb = findViewById(R.id.desc_webview);
        url = getIntent().getStringExtra("url");
        // 创建WebView设置类 对于属性进行设置: 启用 JavaScript、允许文件访问、设置缓存模式等
        WebSettings webSettings = descWeb.getSettings();
        webSettings.setJavaScriptEnabled(true);  //设置页面支持js交互
        webSettings.setUseWideViewPort(true);  //将图片调整到适合webview的大小
        // 设置要加载的网址
        descWeb.loadUrl(url);
        // 默认通过手机浏览器打开网址 为了能够直接通过webview打开网址 设置以下操作
        // 重写 shouldOverrideUrlLoading 方法以确保所有链接都在同一个 WebView 中打开 而不是使用外部浏览器
        descWeb.setWebViewClient(new WebViewClient() {
            @Override
            public boolean shouldOverrideUrlLoading(WebView view, WebResourceRequest request) {
                // 使用webview要加载的url
                view.loadUrl(url);
                return true;
            }
        });
    }
}

3.4.3 AddItemActivity

使用 ListView 展示从数据库中获取的项目类型。通过点击返回按钮,用户可以关闭当前活动并返回到之前的界面。在活动暂停时,更新数据库中的项目类型列表。

在这里插入图片描述

public class AddItemActivity extends AppCompatActivity implements View.OnClickListener {
    ImageView backIv;
    ListView addLv;
    // 数据源
    List<TypeBean> mDatas;
    private AddItemAdapter adapter;
    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_add_item);
        // 查找控件
        backIv = findViewById(R.id.add_iv_back);
        addLv = findViewById(R.id.add_lv);
        backIv.setOnClickListener(this);
        mDatas = DBManager.getAllTypeList();
        // 创建适配器对象
        adapter = new AddItemAdapter(this, mDatas);
        // 设置适配器
        addLv.setAdapter(adapter);
    }
}

3.4.4 配置类UniteApp

UniteApp用于在应用程序启动时进行全局初始化操作。调用DBManager类的initDB方法进行数据库的初始化操作,在整个应用程序中共享数据库对象。创建一个Volley的请求队列httpQueue,调用initImageLoader方法初始化图片加载框架ImageLoader。创建一个ImageLoaderConfiguration对象,并对其进行配置,包括线程优先级、缓存策略等。调用ImageLoader.getInstance().init方法初始化ImageLoader。提供了静态方法getHttpQueue,用于获取全局的请求队列httpQueue。

3.5 程序运行总体思路

在这里插入图片描述

4 系统测试

图4.1 APP图标
在这里插入图片描述

图4.2 主界面

在这里插入图片描述

图4.3 点击新闻
在这里插入图片描述

图4.4 点击添加按钮
在这里插入图片描述

图4.5 选中新增新闻类别
在这里插入图片描述

图4.6 标签条同步更新
在这里插入图片描述

5 设计总结

在这个开发过程中我根据老师上课讲到的网络爬虫,先行创建了一个测试网络爬虫程序,但是一直报错,提示API次数不够,我在网上搜索了很多文章,终于成功申请了API。但是程序仍然无法运行成功,但是直接通过浏览器却可以获得爬虫数据,搜了很多视频,原来是要重新安装该应用才可以。之后我又通过B站视频学习安卓开使用接SQLite数据库,学习在Android Stdio中使用数据库接口进行数据库编程。对我来说最困难的是整个项目的前期思路构建,一方面是对移动开发的知识遗忘很多,一方面是第一次自己编写大项目。我花了一个星期重新把课件看了一遍,大致了解了Activity,Intent,适配器,Service,网络编程的内容。然后我开始构建思路,我先是把主要的三个界面设计出来,主界面,显示新闻的界面,支持添加新闻类别的界面,对于怎么填充界面,我用到了适配器,将从数据库或网络中的数据根据界面格式显示在布局上。在网络编程中,我搜了很多文章看了很多视频,最终选择只用Volley进行网络请求。在设计数据模型时,我根据老师讲的和课件,抽象出三个数据实体,放在Bean文件夹。另一个,让我困扰好几天的是解析网络爬虫获得的JSON数据,我同样是搜文章搜视频,最后通过AI帮忙解决了这个问题,同时我也觉得AI的设计很有道理,把JSON响应抽象出内部类,和HTTP响应一一对应。同时我认为这个项目我做的并不是很好,在主界面显示新闻简要使用RecycleView会更好,因为涉及到数据量大的时候,RecycleView性能和灵活性都更好。以及在数据库操作时,cursor使用完要关闭,但是我关闭后一直报错,不关闭虽然浪费了资源但是程序可以完整成功的运行,搜了很多文章仍没法解决,不过像这种小型程序,cursor占的资源也无伤大雅。在数据库操作时,由于影响记录条数少,并没有使用事务,不过为了提高代码健壮性,之后我应该使用事务保证数据一致性。其次我认为在编写项目时,为一些条件代码加异常,加调试信息,真是太重要了,并且要写一部分代码就调试,我总是写了很多代码再调,而且也没有详细的调试信息,导致开发进度十分缓慢,每次遇到bug就得停滞很久。不过通过这次编写项目,我对logcat这只小猫的使用理解已经更进一步。总的来说,这次项目开发还算可以,我主要复习掌握了布局设计,ListView,ImagheView,Activity,Intent,适配器,网络编程,数据库操作等知识,更重要的,自己把这些知识串起来,运用到了项目中,成功搭建了类似于今日头条的“明日头条”。同时,我也认识到持续学习和改进的重要性,以便更好地应对未来的挑战。在今后的学习中,我将继续积累经验,完善自己的开发技能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿猿收手吧!

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值