Thursday, December 17, 2009

NewsDroid RSS Reader

Welcome to our first tutorial! This tutorial assumes that you are familiar with the official tutorial supplied by Google. If you have not run through that I would highly recommend it!

The syntax highlighting in these tutorials will create hyperlinks to the documentation out of general Java and Android specific classes. So when you see a class like "Activity" in the code you can click on it and be taken to the documentation for that class. Also, the line numbers next to the code correspond to the line numbers in the java files.

In this tutorial we will build a simple RSS reader called "NewsDroid" so that you can get all the headlines without having to hit all your favorite sites in that little browser. The application will open with a list of all of the RSS feeds that you have added, and when a user clicks on a feed then it will display a list of the headlines for that feed. When a user then selects a headline they will be taken directly to that story in the web browser.

This application consists of 7 classes:

  • Simple storage classes:
    • Article - Simple class to hold the values of an article, ie the the title and url
    • Feed - Simple class to hold the values of a feed, ie the title of the feed and url to the XML file
  • Backend Classes:
    • NewsDroidDB - Handles all the SQL Lite database work
    • RSSHandler - Parses RSS feed and saves what we need from them
  • Frontend Classes:
    • ArticlesList - ListActivity that displays the Articles
    • FeedsList - ListActivity that displays the Feeds
    • URLEditor - Activity that allows the user to enter the URL for the feed

You may want to download the complete source for reference as you view this tutorial.

On page 2 we discuss the database backend components.
On page 3 we discuss the RSS Parsing using a SAX object.
On page 4 we put it all together with a couple ListActivities and a URL Entry Box

NewsDroid Backend

Our first task will be to create the NewsDroidDB class. This class will be responsible for creating the database and inserting or deleting records. Before we create the NewsDroidDB we need to design the layout of the database:

For each RSS Feed we need the the title of the feed and the URL for the xml file so that we can fetch the articles. In addition we will want a unique id for each feed so that we can have an articles table that relates to the feeds table. So to create our table "feeds" we will run a create statement as follows:

create table feeds (feed_id integer primary key autoincrement, title text not null, url text not null);

And for the articles we need to store the feed_id so that we know what feed the article is for. We also need the title of the article and the url to the article:

create table articles (article_id integer primary key autoincrement, feed_id int not null, title text not null, url text not null);

Now that we know the layout of our tables we can start working on our NewsDroidDB class. Here are the list of things this class will need to accomplish:

  • Create Tables - We will need to be able to create tables for the feeds and articles if they don't exist, this will be handled in the constructor.
  • Insert Data - Will need to be able to insert a feed or an article.
  • Delete Data - If we can insert we need to be able to delete right?
  • Get Data - We need to be able to pull the data to display in our ListActivities

We'll tackle these tasks in order. Here is the NewsDroidDB class declaration and the constructor:

  1. public class NewsDroidDB {
  2. private static final String CREATE_TABLE_FEEDS = "create table feeds (feed_id integer primary key autoincrement, "
  3. + "title text not null, url text not null);";
  4. private static final String CREATE_TABLE_ARTICLES = "create table articles (article_id integer primary key autoincrement, "
  5. + "feed_id int not null, title text not null, url text not null);";
  6. private static final String FEEDS_TABLE = "feeds";
  7. private static final String ARTICLES_TABLE = "articles";
  8. private static final String DATABASE_NAME = "newdroid";
  9. private static final int DATABASE_VERSION = 1;
  10. private SQLiteDatabase db;
  11. public NewsDroidDB(Context ctx) {
  12. try {
  13. db = ctx.openDatabase(DATABASE_NAME, null);
  14. } catch (FileNotFoundException e) {
  15. try {
  16. db = ctx.createDatabase(DATABASE_NAME, DATABASE_VERSION, 0,
  17. null);
  18. db.execSQL(CREATE_TABLE_FEEDS);
  19. db.execSQL(CREATE_TABLE_ARTICLES);
  20. } catch (FileNotFoundException e1) {
  21. db = null;
  22. }
  23. }
  24. }

Here we declare the class and some of the variables that we will be using for database access, including the create statements, table names, database name, and database version. The constructor takes the context as the argument so that it can initialize the SQLiteDatabase object "db".

On line 33 it will attempt to connect to the database, if the database does not exists it will throw the FileNotFoundException and we will create the database and the tables in the database on lines 36-39.

Now we need functions for inserting feeds and deleting feeds:

  1. public boolean insertFeed(String title, URL url) {
  2. ContentValues values = new ContentValues();
  3. values.put("title", title);
  4. values.put("url", url.toString());
  5. return (db.insert(FEEDS_TABLE, null, values) > 0);
  6. }
  7. public boolean deleteFeed(Long feedId) {
  8. return (db.delete(FEEDS_TABLE, "feed_id=" + feedId.toString(), null) > 0);
  9. }

Both of these functions are boolean and return true if the task was sucessful. As you can see using a ContentValues object and a SQLiteDatabase object it is very easy to do these basic operations.

Here are 2 very similar functions for dealing with articles:

  1. public boolean insertArticle(Long feedId, String title, URL url) {
  2. ContentValues values = new ContentValues();
  3. values.put("feed_id", feedId);
  4. values.put("title", title);
  5. values.put("url", url.toString());
  6. return (db.insert(ARTICLES_TABLE, null, values) > 0);
  7. }
  8. public boolean deleteAricles(Long feedId) {
  9. return (db.delete(ARTICLES_TABLE, "feed_id=" + feedId.toString(), null) > 0);
  10. }

Now we must create functions to return the data, and to help us do this we will create 2 classes mentioned in the introduction Articles and Feeds. These just objects that store all of our values and will be very similar to the layout of the tables. Here are their declarations:

  1. class Article extends Object {
  2. public long articleId;
  3. public long feedId;
  4. public String title;
  5. public URL url;
  6. }
  7. class Feed extends Object {
  8. public long feedId;
  9. public String title;
  10. public URL url;
  11. }

Now that we have these storage objects we can create a function that will return a List object for each of these. First we will look at the getFeeds() function:

  1. public List<Feed> getFeeds() {
  2. ArrayList<Feed> feeds = new ArrayList<Feed>();
  3. try {
  4. Cursor c = db.query(FEEDS_TABLE, new String[] { "feed_id", "title",
  5. "url" }, null, null, null, null, null);
  6. int numRows = c.count();
  7. c.first();
  8. for (int i = 0; i < numRows; ++i) {
  9. Feed feed = new Feed();
  10. feed.feedId = c.getLong(0);
  11. feed.title = c.getString(1);
  12. feed.url = new URL(c.getString(2));
  13. feeds.add(feed);
  14. c.next();
  15. }
  16. } catch (SQLException e) {
  17. Log.e("NewsDroid", e.toString());
  18. } catch (MalformedURLException e) {
  19. Log.e("NewsDroid", e.toString());
  20. }
  21. return feeds;
  22. }

This function returns a List of Feed objects. This list will be used to populate the FeedsList ListActivity, as we will see later. The first step is to query the table using the SQLiteDatabase.query function. Because we just want all of the records from the feeds table this is easy. The query will return a Cursor object which we can loop through and create a new Feed object. As we loop through we add each new feed to the ArrayList feeds via the feeds.add(feed) on line 82.

Here is the function to get the list of articles for a specific feed:

  1. public List<Article> getArticles(Long feedId) {
  2. ArrayList<Article> articles = new ArrayList<Article>();
  3. try {
  4. Cursor c = db.query(ARTICLES_TABLE, new String[] { "article_id",
  5. "feed_id", "title", "url" },
  6. "feed_id=" + feedId.toString(), null, null, null, null);
  7. int numRows = c.count();
  8. c.first();
  9. for (int i = 0; i < numRows; ++i) {
  10. Article article = new Article();
  11. article.articleId = c.getLong(0);
  12. article.feedId = c.getLong(1);
  13. article.title = c.getString(2);
  14. article.url = new URL(c.getString(3));
  15. articles.add(article);
  16. c.next();
  17. }
  18. } catch (SQLException e) {
  19. Log.e("NewsDroid", e.toString());
  20. } catch (MalformedURLException e) {
  21. Log.e("NewsDroid", e.toString());
  22. }
  23. return articles;
  24. }

As you can see this function is almost identical to to the getFeeds function except that it takes in an argument "feedId" and uses this argument in the query function on line 96. The 3rd argument in the db.query function is the "WHERE" argument (minus the "WHERE"), and here we specify "feed_id=X", where X is the feedId passed into the function.

This is all we need for the NewDroidDB class. We can now move on to parsing the RSS document and getting the data into the database...

NewsDroid RSS Parsing

Now it is our goal to download an RSS xml file and get what we need out of it. To do this we will use "Simple API for XML", or SAX. This library is included in the Android SDK and is great for parsing XML data. Before we get started figuring out a parser, lets take a look at the format of an RSS feed:

  1. xml version="1.0" encoding="utf-8" ?>
  2. <rss version="2.0" xml:base="http://www.helloandroid.com" xmlns:dc="http://purl.org/dc/elements/1.1/">
  3. <channel>
  4. <title>Hello Android - Android OS news, tutorials, downloadstitle>
  5. <link>http://www.helloandroid.com
  6. <description />
  7. <language>enlanguage>
  8. <item>
  9. <title>Biggest story of the year!title>
  10. <link>http://www.helloandroid.com/node/59
  11. <description>Here is a teaser for the story.description>
  12. <comments>http://www.helloandroid.com/node/59#comments
  13. <pubDate>Sat, 17 Nov 2007 15:07:25 -0600pubDate>
  14. <dc:creator>hobbsdc:creator>
  15. item>
  16. channel>
  17. rss>

Seems pretty simple right? It better, considering it's really simple syndication! So, lets figure out what we want to pull out of this, and when. If the user is adding a new feed, he or she will supply the URL and then we need to go out and figure out the title of this feed before we can add it to the database. That would be found in the "title" element on line 4, in this case "Hello Android - Android OS news, tutorials, downloads". So once a feed is added and the users selects a feed on the feeds list we need to grab all of the headlines for the articles table, and the url direct link to the article. This is the "title" element and "link" element found in each "item" element. In this example it is lines 9 and 10.

So, to do this we will now create a class "RSSHandler" and extend org.xml.sax.helpers.DefaultHandler. For this class I'm just going to show you the complete class and will explain what's happening below:

  1. public class RSSHandler extends DefaultHandler {
  2. // Used to define what elements we are currently in
  3. private boolean inItem = false;
  4. private boolean inTitle = false;
  5. private boolean inLink = false;
  6. // Feed and Article objects to use for temporary storage
  7. private Article currentArticle = new Article();
  8. private Feed currentFeed = new Feed();
  9. // Number of articles added so far
  10. private int articlesAdded = 0;
  11. // Number of articles to download
  12. private static final int ARTICLES_LIMIT = 15;
  13. // The possible values for targetFlag
  14. private static final int TARGET_FEED = 0;
  15. private static final int TARGET_ARTICLES = 1;
  16. // A flag to know if looking for Articles or Feed name
  17. private int targetFlag;
  18. private NewsDroidDB droidDB = null;
  19. public void startElement(String uri, String name, String qName,
  20. Attributes atts) {
  21. if (name.trim().equals("title"))
  22. inTitle = true;
  23. else if (name.trim().equals("item"))
  24. inItem = true;
  25. else if (name.trim().equals("link"))
  26. inLink = true;
  27. }
  28. public void endElement(String uri, String name, String qName)
  29. throws SAXException {
  30. if (name.trim().equals("title"))
  31. inTitle = false;
  32. else if (name.trim().equals("item"))
  33. inItem = false;
  34. else if (name.trim().equals("link"))
  35. inLink = false;
  36. // Check if looking for feed, and if feed is complete
  37. if (targetFlag == TARGET_FEED && currentFeed.url != null
  38. && currentFeed.title != null) {
  39. // We know everything we need to know, so insert feed and exit
  40. droidDB.insertFeed(currentFeed.title, currentFeed.url);
  41. throw new SAXException();
  42. }
  43. // Check if looking for article, and if article is complete
  44. if (targetFlag == TARGET_ARTICLES && currentArticle.url != null
  45. && currentArticle.title != null) {
  46. droidDB.insertArticle(currentFeed.feedId, currentArticle.title,
  47. currentArticle.url);
  48. currentArticle.title = null;
  49. currentArticle.url = null;
  50. // Lets check if we've hit our limit on number of articles
  51. articlesAdded++;
  52. if (articlesAdded >= ARTICLES_LIMIT)
  53. throw new SAXException();
  54. }
  55. }
  56. public void characters(char ch[], int start, int length) {
  57. String chars = (new String(ch).substring(start, start + length));
  58. try {
  59. // If not in item, then title/link refers to feed
  60. if (!inItem) {
  61. if (inTitle)
  62. currentFeed.title = chars;
  63. } else {
  64. if (inLink)
  65. currentArticle.url = new URL(chars);
  66. if (inTitle)
  67. currentArticle.title = chars;
  68. }
  69. } catch (MalformedURLException e) {
  70. Log.e("NewsDroid", e.toString());
  71. }
  72. }
  73. public void createFeed(Context ctx, URL url) {
  74. try {
  75. targetFlag = TARGET_FEED;
  76. droidDB = new NewsDroidDB(ctx);
  77. currentFeed.url = url;
  78. SAXParserFactory spf = SAXParserFactory.newInstance();
  79. SAXParser sp = spf.newSAXParser();
  80. XMLReader xr = sp.getXMLReader();
  81. xr.setContentHandler(this);
  82. xr.parse(new InputSource(url.openStream()));
  83. } catch (IOException e) {
  84. Log.e("NewsDroid", e.toString());
  85. } catch (SAXException e) {
  86. Log.e("NewsDroid", e.toString());
  87. } catch (ParserConfigurationException e) {
  88. Log.e("NewsDroid", e.toString());
  89. }
  90. }
  91. public void updateArticles(Context ctx, Feed feed) {
  92. try {
  93. targetFlag = TARGET_ARTICLES;
  94. droidDB = new NewsDroidDB(ctx);
  95. currentFeed = feed;
  96. SAXParserFactory spf = SAXParserFactory.newInstance();
  97. SAXParser sp = spf.newSAXParser();
  98. XMLReader xr = sp.getXMLReader();
  99. xr.setContentHandler(this);
  100. xr.parse(new InputSource(currentFeed.url.openStream()));
  101. } catch (IOException e) {
  102. Log.e("NewsDroid", e.toString());
  103. } catch (SAXException e) {
  104. Log.e("NewsDroid", e.toString());
  105. } catch (ParserConfigurationException e) {
  106. Log.e("NewsDroid", e.toString());
  107. }
  108. }
  109. }

So what is going on here you ask?? Lets look the body of the createFeed() function:

  1. targetFlag = TARGET_FEED;
  2. droidDB = new NewsDroidDB(ctx);
  3. currentFeed.url = url;
  4. SAXParserFactory spf = SAXParserFactory.newInstance();
  5. SAXParser sp = spf.newSAXParser();
  6. XMLReader xr = sp.getXMLReader();
  7. xr.setContentHandler(this);
  8. xr.parse(new InputSource(url.openStream()));

First we set the targetFlag so that the rest of the functions know what our target is from the XML file, we are looking specifically for a feed. Next we initialize our droidDB object so that once we get everything we need from the XML we can stick it in the database. After that we set what we know in our currentFeed object (the url the user input). We will update the currentFeed object or currentArticle object as we find out more from the XML file. For the feed object we just need to find its title. For the articles we need the title and the url from the XML.

Now things get SAX specific. We create a SAXParserFactory so we can create a SAXParser so we can create a XMLReader! Anyways, lines 120 and 121 are where we kick this off. We set this class as our ContentHandler and we pass in the xml stream to the XMLReader using the supplied url. At this point the parsing begins.

Now we have 3 functions working to parse this document, it starts at the beginning of the document and calls each of these 3 functions as it moves through the document:

  • startElement - This function is called when the parser reaches an element
  • endElement - This function is called when the parser reaches the end of an element
  • characters - This is called when we are between the start and end

To illustrate this using a simple xml tag: chars

All 3 funtions would be called to parse this string. First startElement() would be called passing in "ele" for the second argument. Then characters() would be called passing in an array of characters "chars". Then endElement() would be called passing in "ele" for the second argument.

So what we can do to parse this XML file is to setup 3 boolean variables starting at line 23. These variables are inItem, inTitle, and inLink. Now, when startElement() is called we can set these values to true if we are entering into one of the 3 elements "item", "title" or "link". We also set these to false when leaving these elements in endElement(). So now when characters() is called we know where we are at and can update the values in currentArticle and currentFeed, you can see this happening at line 96.

Eventually all the fields of currentArticle or currentFeed will be set and it will be time to insert it into the database. We can check this in endElement(), and that can be seen on line 66 and line 75. After we insert a feed we can throw a SAXException to stop parsing, or after we collect enough articles we can stop parsing.

Now we can parse the XML files, handle the database stuff so lets show it to the user now! We'll build the UI on the next page...

User Interface

The user interface for this application is made up of 3 different Activities. The first activity the user comes to, FeedsList, is a simple list activity that displays all of the feeds the user has added, and gives the option to add more feeds or delete the selected feed. The layout of these Activities are very similar to that in Google's Notepad tutorial, so the complete code will not be included in this page. Instead we will highlight a couple of important points.

An important function to look at is the onListItemClick() function for this class:

  1. @Override
  2. protected void onListItemClick(ListView l, View v, int position, long id) {
  3. super.onListItemClick(l, v, position, id);
  4. Intent i = new Intent(this, ArticlesList.class);
  5. i.putExtra("feed_id", feeds.get(position).feedId);
  6. i.putExtra("title", feeds.get(position).title);
  7. i.putExtra("url", feeds.get(position).url.toString());
  8. startSubActivity(i, ACTIVITY_VIEW);
  9. }

This code is executed when the user clicks on a feed. The job of this function is to bring up the ArticleList Activity showing the headlines for the selected feed. The information is passed by adding all of the information to describe a feed to the Intent and then starting the Activity. If we look at the constructor for the ArticlesList Activity we can see how this is used:

  1. @Override
  2. protected void onCreate(Bundle icicle) {
  3. try {
  4. super.onCreate(icicle);
  5. droidDB = new NewsDroidDB(this);
  6. setContentView(R.layout.articles_list);
  7. feed = new Feed();
  8. if (icicle != null) {
  9. feed.feedId = icicle.getLong("feed_id");
  10. feed.title = icicle.getString("title");
  11. feed.url = new URL(icicle.getString("url"));
  12. } else {
  13. Bundle extras = getIntent().getExtras();
  14. feed.feedId = extras.getLong("feed_id");
  15. feed.title = extras.getString("title");
  16. feed.url = new URL(extras.getString("url"));
  17. droidDB.deleteAricles(feed.feedId);
  18. RSSHandler rh = new RSSHandler();
  19. rh.updateArticles(this, feed);
  20. }
  21. fillData();
  22. } catch (MalformedURLException e) {
  23. Log.e("NewsDroid",e.toString());
  24. }
  25. }

When the ArticlesList Activity is created we need to define the values of the private feed object. There are 2 ways that this info could be coming in, either because the Activity was paused or even killed, or because it's being called for the first time via an Intent from FeedsList. When it's called for the first time then we will check the extras from the intent. This is what's happening starting at line 37. If it's being called for the first time we also need to delete all the articles currently in the database and update the table with the new articles from the XML file.

If it is being resumed then we will get the data from the icicle, and we will use the same data that was in there before. To make sure that information is in the icicle we must implement the onFreeze() function as follows:

  1. @Override
  2. protected void onFreeze(Bundle outState) {
  3. super.onFreeze(outState);
  4. outState.putLong("feed_id", feed.feedId);
  5. outState.putString("title", feed.title);
  6. outState.putString("url", feed.url.toString());
  7. }

When a user clicks on a headline we need to launch the browser, it is very simple to create an intent to launch a browser, and we will do this in OnListItemClick():

  1. @Override
  2. protected void onListItemClick(ListView l, View v, int position, long id) {
  3. super.onListItemClick(l, v, position, id);
  4. Intent myIntent = new Intent(Intent.VIEW_ACTION, ContentURI.create(articles.get(position).url.toString()));
  5. startActivity(myIntent);
  6. }

The final Activity is the URLEditor shown above, this is the screen where the user inputs the URL to the RSS feed. This is a simple activity, here is the source:

  1. public class URLEditor extends Activity {
  2. EditText mText;
  3. @Override
  4. public void onCreate(Bundle icicle) {
  5. super.onCreate(icicle);
  6. setContentView(R.layout.url_editor);
  7. // Set up click handlers for the text field and button
  8. mText = (EditText) this.findViewById(R.id.url);
  9. if (icicle != null)
  10. mText.setText(icicle.getString("url"));
  11. Button ok = (Button) findViewById(R.id.ok);
  12. ok.setOnClickListener(new View.OnClickListener() {
  13. public void onClick(View arg0) {
  14. okClicked();
  15. }
  16. });
  17. Button cancel = (Button) findViewById(R.id.cancel);
  18. cancel.setOnClickListener(new View.OnClickListener() {
  19. public void onClick(View arg0) {
  20. finish();
  21. }
  22. });
  23. }
  24. protected void okClicked() {
  25. try {
  26. RSSHandler rh = new RSSHandler();
  27. rh.createFeed(this, new URL(mText.getText().toString()));
  28. finish();
  29. } catch (MalformedURLException e) {
  30. showAlert("Invalid URL", "The URL you have entered is invalid.", "Ok", false);
  31. }
  32. }
  33. @Override
  34. protected void onFreeze(Bundle outState) {
  35. super.onFreeze(outState);
  36. outState.putString("url", mText.getText().toString());
  37. }
  38. }

There are 2 buttons that must be handeld, and we do that with nested functions in the onCreate() function. When the "OK" button is clicked we call the okClicked() function. This function will create a feed by calling RSSHandler.createFeed() passing in the text from the input field. We also must handle the Activity lifecycle, and save whatever is in the text box so that if this activity is interupted the partial URL will remain in the box when it's resumed.

As you can see the UI is very basic, but could easily be extended to look much slicker. Hopefully you have enjoyed our first tutorial, and hopefully we will be able to get some more your way.

No comments:

Post a Comment