开发一个简单的RSS阅读器

Github链接:RSS
在此项目的分支下有个release可以测试

原因

随着智能手机的普及,人们从智能终端获取新闻资讯的需求逐渐增加,用手机看新闻、报纸、刊物等已经成为大多数人每天必做的事情之一。人们迫切地希望可以有一款能随时提供最新资讯、重大新闻,可以将用户个性化订阅的新闻、报纸、刊物等快速、准确、方便地推送到智能手机用户的新闻资讯阅读软件。

当然其实是因为Android的课设给了一堆题目,又因为即刻的关系,自然而然选了RSS阅读器。

以下其实是课设报告的版本…所以看上去可能不是那么舒服

功能描述

  1. 新闻组的管理

    用户能按照自己的需求来对新闻组进行CRUD等基本操作;

  2. RSS新闻频道的管理

    用户能够按照自己的需求对RSS新闻频道进行增删改查等基本功能的实现;

  3. 新闻频道阅读:

    当用户打开一个RSS新闻频道时,能够准确无误的从网络上加载该频道的新闻列表;

  4. 新闻信息的阅读:

    当用户觉得某一条新闻有趣时,能够打开新闻显示界面进行概要浏览,还能进去具体网页进行更为详细的了解;

  5. 新闻的更新:

    当RSS更新以后,程序能做相应的更新,能与网络上的内容保持一致性。做到即时更新;

  6. 新闻的分享:

    当用户认定某一条新闻比较有趣的时候,可用与别人通过SMS短信进行分享。

开发及运行环境

  • 开发平台:Android
  • 开发语言:Java(1.6以上)
  • 开发工具:Android Studio(3.2以上)
  • 构建工具:Gradle(3.4以上)
  • 数据库系统:SQLite(3.0)

详细功能划分

基于团队人数少,项目结构紧凑等特点,将上面六点主要功能划分为总计个功能点,6类任务划分。(其实是因为一组有六个人)

项目结构图

应用基础

  1. 应用主题框架;
  2. 数据库(表)设计及生成;
  3. RSS源的解析;
  4. 实体类;
  5. 数据库协作类接口及实现;

订阅页及item页

  1. 显示已订阅的RSS源;
  2. 刷新RSS源的数据(主动刷新|定时刷新);
  3. 添加RSS源数据到本地;
  4. 点击某个RSS跳转到item页;
  5. item页显示该RSS下的所有item;
  6. 点击某个item跳转到详情页;

详情页

  1. 根据item的url加载内容;
  2. 直接在浏览器打开该url;
  3. 收藏到本地|从本地取消收藏;
  4. 调用系统api分享此url;

发现页

  1. 显示数据库中所有的RSS源及对应信息;
  2. 根据订阅状态显示不同图标;
  3. 更改订阅状态;
  4. 搜索RSS源;

我的页

  1. 设置应用阅读字体大小;
  2. 进入收藏页并显示收藏的文章信息;
  3. 点击收藏item进入详情页;

长按及分组

  1. 订阅页长按订阅界面项跳出选项(取消订阅,更改分组,删除此项);
  2. 发现页长按跳出更改分组,添加分组选项;
  3. 我的页点击“我的分组”显示RSS源的分组信息。

人员

团队成员共6人,故将上面的6个任务分别作安排。

这样划分可以保证每个人可以在一定程度上单独开发模块,同时可以使用Github来保证项目的一致性(说实话这是我第一次在课设的时候用Github,确实更节省很多不必要的功夫)

个人任务

应用主题框架

应用主题框架采用BottomNavigationBar+ViewPager+Fragment组合,实现结果和微信等主流应用类似,比较美观易用。

MainActivity中仅有1个BottomNavigationView和1个ViwePager组件,在代码中给Bar挂载上bottom menu。

1
2
3
4
5
6
7
8
9
10
11
@Override
public void onPageSelected(int position) {

if (menuItem != null) {
menuItem.setChecked(false);
} else {
bottomNavigationView.getMenu().getItem(0).setChecked(false);
}
menuItem = bottomNavigationView.getMenu().getItem(position);
menuItem.setChecked(true);
}

menu的样式和效果在对应的xml文件中定义。

ViewPager添加3个Fragment。

1
2
3
4
5
6
7
8
private void setupViewPager(ViewPager viewPager) {
ViewPagerAdapter adapter = new ViewPagerAdapter(getSupportFragmentManager());

adapter.addFragment(SubscribeFragment.newInstance());
adapter.addFragment(FindFragment.newInstance("1","2"));
adapter.addFragment(MyFragment.newInstance("1","2"));
viewPager.setAdapter(adapter);
}

效果:

主界面页

实体类

订阅页需要的数据为:已订阅RSS的名字,RSS的消息数;

发现页需要的数据为:所有的RSS的name,url,订阅状态,分组;

item页需要的数据为:某个RSS的name,item的title,description和time(图片不考虑);

详情页需要的数据为:所属RSS的title,item的url和time;

收藏页需要的数据为:已收藏的item的RSS的url,title,description和收藏时间。

结合以上,设计三个实体类:

  • RSSUrl

    1
    2
    3
    4
    5
    int id;
    String url;
    String name;
    String groupName;// 新闻组
    SubscribeStatus status;// 是否订阅
  • RSSItemBean

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 标题
    private String title;
    // 发布者(为自定义name时默认为它)
    private String author;
    // 详情链接
    private String link;
    // 网站或栏目的简要介绍
    private String description;
    // 时间
    private Date pubDate;
  • FavorRSSItem

    1
    2
    3
    4
    5
    6
    // 此item的url
    private String itemUrl;
    private String titleName;
    private String description;
    // 收藏时间
    private String favorTime;

RSS源的解析

RSS是基于XML(可扩展标志语言)的一种形式,并且所有的RSS文件都要遵守万维网联盟(W3C)站点发布的XML 1.0规范。一般来说,RSS文档的最顶层是一个元素作为根元素,元素有一个强制属性version,用于指定当前RSS文档的版本,目前常用的RSS版本是2.0。元素下的子元素是唯一的一个元素,它包含了关于该网站或栏目的信息和内容,在下必备的语句有三个:

:网站或栏目的名称,一般与网站或栏目的页面title一致;
:网站或栏目的URL;
:对网站或栏目的简要描述。

还可以使用一些如(语言)、(版权声明)等可选语句来丰富< channel>内容,具体的新闻提要就要依靠来体现了。一般一条新闻就是一个,< item>下至少要存在一个或<description>,其他语句可以根据需要进行选择。</description>

一般一个RSS文件如下例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<?xml version="1.0" encoding="gb2312"?>
<rss version="2.0">
<channel>
<title>网站或栏目的名称</title>
<link>网站或栏目的URL地址</link>
<description>网站或栏目的简要介绍</description>
<item>
<title>新闻标题</title>
<link>新闻的链接地址</link>
<description>新闻简要介绍</description>
<pubDate>新闻发布时间</pubDate>
<author>新闻作者名称</author>
</item>
<item>
   ......
</item>
</channel>
</rss>

但是各个RSS源的xml文档可能都有自己的特点,为了保证解析的简单稳定,我引入了一个专门用于帮助解析xml的包:Rome

在utils包中,我设计了一个RSSUtil类,通过设置rss的url,并使用rome的一些类来获取title,link,description,item等数据,并提供get方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void parseFromUrl(String url) throws IOException, FeedException {
SyndFeedInput input = new SyndFeedInput();
URLConnection connection = new URL(url).openConnection();

// 防止403
connection.setRequestProperty("User-Agent", "Mozilla/4.0 (compatible; MSIE 5.0; Windows NT; DigExt)");
String contentEncoding = connection.getHeaderField("Content-Encoding");
if (contentEncoding != null && contentEncoding.contains("gzip")) {
// System.out.println("content encoding is gzip");
GZIPInputStream gzipInputStream = new GZIPInputStream(connection.getInputStream());
feed = input.build(new XmlReader(gzipInputStream));
} else {
feed = input.build(new XmlReader(connection.getInputStream()));
}
}
  1. 文章数:rssUtil.getFeedSize()
  2. RSS标题:rssUtil.getTitleName()
  3. RSS描述: rssUtil.getDescription()
  4. 文章项:rssUtil.getRssItemBeans()

为了保证数据在不影响主线程的情况下获取到,调用parseFromUrl()方法在新线程中,并使用CountDownLatch保证该线程已经被执行完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
private void getRSSData(final String rssUrl) {
final CountDownLatch cdl = new CountDownLatch(1);
new Thread(new Runnable() {
@Override
public void run() {
doParse(rssUrl);
cdl.countDown();
}
}).start();
try {
cdl.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}

数据库(表)设计及生成

三个实体类中需要持久化的仅为RSSUrl和FavorRSSItem,因此基于他们的属性设计以下数据库:

RSSUrl表截图

FavorRSSItem表截图

数据库协作类接口及实现

数据库接口在后来的模块合并过程中增加了一些,增加的几个并没有在下面列出(因为过了一段时间忘了加了哪些了,可以参见源码)

DatabaseHelper类

继承SQLiteOpenHelper用于数据库和表的创建工作,并对外提供getWritableDatabase()和getReadableDatabase()方法。

BaseDALImpl类

数据库DAL基类,预设一些方法和成员,并提供preInsertRssUrl()将来自xml中的一些内置的RSS url链接杀入到RSS_URL表中。该方法在线程PreDatabaseThread的run()方法中被调用。

简单说明流程图

1
2
3
4
@Override
public void run() {
new BaseDALImpl(context).preInsertRssUrl();
}

在MainActivity中start该线程。

1
2
preDatabaseThread=new PreDatabaseThread(getApplicationContext());
new Thread(preDatabaseThread).start();

FavorRSSItemDAL类

favor_rss_item表协作类接口,共以下5个接口。

  1. 显示所有收藏
1
2
3
4
5
6
/**
* 查询表favor_rss_item全部内容
* @param favorRSSItems 必须保证传进 Adapter 的数据 List 是同一个 List
* @return rssUrlArrayList
*/
ArrayList<FavorRSSItem> getAllData(ArrayList<FavorRSSItem> favorRSSItems);
  1. 查询收藏
1
2
3
4
5
6
7
/**
* 模糊查询|不实现
* @param rssUrlArrayList 必须保证传进 Adapter 的数据 List 是同一个 List
* @param query 输入的字符串
* @return rssUrlArrayList
*/
ArrayList<FavorRSSItem> getQueryData(ArrayList<FavorRSSItem> rssUrlArrayList, String query);
  1. 查询某项item
1
2
3
4
5
6
/**
* 查询favor_rss_item某一项的内容
* @param id 选中项的id
* @return 一项item
*/
FavorRSSItem getOneData(Integer id);
  1. 收藏
1
2
3
4
5
6
7
8
/**
* 收藏
* @param url 该item的url
* @param titleName 标题
* @param description 描述
* @return 是否添加成功
*/
long insertOneData(String url,String titleName,String description);
  1. 取消收藏
1
2
3
4
5
6
/**
* 取消收藏
* @param id 选中项的id
* @return 一项item
*/
int deleteOneData(Integer id);

RSSUrlDAL类

rss_url表协作类接口,共以下9个接口。

  1. 查询所有
1
2
3
4
5
6
/**
* 查询表rss_url全部内容
* @param rssUrlArrayList 必须保证传进 Adapter 的数据 List 是同一个 List
* @return rssUrlArrayList
*/
ArrayList<RSSUrl> getAllData(ArrayList<RSSUrl> rssUrlArrayList);
  1. 获取所有已订阅内容
1
ArrayList<RSSUrl> getSubscribe(ArrayList<RSSUrl> rssUrls);
  1. 模糊查询
1
2
3
4
5
6
7
/**
* 模糊查询
* @param rssUrlArrayList 必须保证传进 Adapter 的数据 List 是同一个 List
* @param query 输入的字符串
* @return rssUrlArrayList
*/
ArrayList<RSSUrl> getQueryData(ArrayList<RSSUrl> rssUrlArrayList, String query);
  1. 查询rss_url某一项的内容
1
2
3
4
5
6
/**
* 查询rss_url某一项的内容
* @param id 选中项的id
* @return 一项item
*/
RSSUrl getOneData(Integer id);
  1. 添加
1
2
3
4
5
6
7
8
/**
* 添加一个rss源
* @param url rss url
* @param groupName 组名,default=""
* @param status 订阅状态,主动添加时default=SUBSCRIBED
* @return 是否添加成功
*/
long insertOneData(String url, String groupName, SubscribeStatus status);
  1. 添加
1
2
3
4
5
6
/**
* 删除某项
* @param id 选中项的id
* @return 一项item
*/
int deleteOneData(Integer id);
  1. 更改标题名

    1
    2
    3
    4
    5
    6
    7
    /**
    * 更改标题名
    * @param id 选中项的id
    * @param name 新的名字
    * @return 是否成功
    */
    int updateName(Integer id, String name);
  2. 更改组名

    1
    2
    3
    4
    5
    6
    7
    /**
    * 更改组名
    * @param id 选中项的id
    * @param groupName 新的组名
    * @return 是否成功
    */
    int updateGroupName(Integer id, String groupName);
  3. 更改订阅状态

    1
    2
    3
    4
    5
    6
    7
    /**
    * 更改订阅状态
    * @param id 选中项的id
    * @param status 订阅状态
    * @return
    */
    int updateSubscribeStatus(Integer id,SubscribeStatus status);

补充

  1. RSS解析的时间格式和收藏时插入数据库的时间格式均需要重新解析,转化为自己想要的格式;

  2. 因为RSS源的问题,会有数据空(titleName)和带html格式(description)的问题,前者已通过判断设值的方式完成。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    /**
    * 获取标题,title为空时设为unknown
    * @return
    */
    public String getTitleName() {
    titleName=feed.getTitle();
    if(titleName==null||titleName.isEmpty()){
    titleName="unknown";
    }
    return titleName;
    }

    后者未完全实现。

    1
    2
    3
    public String getDescription() {
    return ClearStringUtil.clearDescription(feed.getDescription());
    }
  3. 可能因为RSSItem数据量过大而影响性能,但暂时未使用分批查询模式;

  4. 为了匹配某项以.xml结尾的rss源,增加url匹配;

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    private void doParse(String rssUrl) {
    if(!rssUrl.startsWith("https://")&&!rssUrl.startsWith("http://")){
    System.out.println("add https:// for url");
    rssUrl="https://"+rssUrl;
    }
    if (rssUrl.endsWith(".xml")) {
    ...
    } else {
    ...
    }
    }
  5. 数据库的增删改操作均有返回值,以判断是否执行成功。

总结

  1. 模块式的任务划分确实能提高工作效率,减少工作负担;
  2. 开发供其他人使用的接口时要描述清楚功能和参数;
  3. 开发环境尽量保持相同,否则会浪费大量时间和精力。

土豪将鼓励我继续创作和搬运!