This is a refactored version of a previous rss application example which had 1 major issue and gathered some discutions around that tutorial.
Starting with Android 3.0 and higher when trying to perform a network operation from the UI thread, the NetworkOnMainThreadException is thrown. The previous example did not address this issue. Why? Well… the tutorial didn’t have the purpose to present a stable application from all points of view, its main intention was to show how to use the XmlPullParser to parse an XML file.
The new tutorial fixes that issue, and along with that brings some improvements that will make this example easily adaptable to your specific needs.
What is new in version 2:
a) The RSS content is downloaded and parsed in an IntentService, thus it does not block anymore the UI thread.
b) The application now uses Fragments, thus being able to handle properly configuration changes like screen orientation, while performing the background work.
c) The application uses a custom adapter instead of built in ArrayAdapter. This will allow us to work with custom objects, rather than using string arrays like in previous example.
d) The rss parser was modified and now the code is much simpler. With minor adjustments it can be easily adapted to parse a different rss feed.
Requirements didn’t change, we still need to parse the PCWorld’s rss feed (http://www.pcworld.com/index.rss) and display the headlines in a ListView. When clicking on a list item, the built in web browser opens and user is redirected to the corresponding article.
Lets begin first with modification of AndroidManifest file.
1. Add the internet permission:
<uses-permission android:name="android.permission.INTERNET" />
2. As the application uses a Service, it should be specified in the AndroidManifest too:
<application ..> ... <service android:name=".RssService" /> </application>
The implementation of RssService will be shown later.
3. Add support library to project. Assuming you are using Eclipse: right click on project name -> Android Tools -> Add Support Library.
4. Here’s how MainActivity.java looks like:
public class MainActivity extends FragmentActivity { @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setContentView(R.layout.main); if (savedInstanceState == null) { addRssFragment(); } } private void addRssFragment() { FragmentManager manager = getSupportFragmentManager(); FragmentTransaction transaction = manager.beginTransaction(); RssFragment fragment = new RssFragment(); transaction.add(R.id.fragment_container, fragment); transaction.commit(); } @Override protected void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); outState.putBoolean("fragment_added", true); } }
The MainActivity simply adds the RssFragment to the activity. First we check the savedInstanceState to see if it’s null, if so, it means we are entering the activity for the first time and the fragment can be added, otherwise we are returning from a configuration change, so we don’t need to add the fragment once again.
5. main.xml
<?xml version="1.0" encoding="utf-8"?> <FrameLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="fill_parent" android:id="@+id/fragment_container" android:layout_height="fill_parent" />
The main.xml contains a FrameLayout which will serve as the host for the fragment.
6. And here’s how RssFragment.java looks like:
public class RssFragment extends Fragment implements OnItemClickListener { private ProgressBar progressBar; private ListView listView; private View view; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); setRetainInstance(true); } @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if (view == null) { view = inflater.inflate(R.layout.fragment_layout, container, false); progressBar = (ProgressBar) view.findViewById(R.id.progressBar); listView = (ListView) view.findViewById(R.id.listView); listView.setOnItemClickListener(this); startService(); } else { // If we are returning from a configuration change: // "view" is still attached to the previous view hierarchy // so we need to remove it and re-attach it to the current one ViewGroup parent = (ViewGroup) view.getParent(); parent.removeView(view); } return view; } private void startService() { Intent intent = new Intent(getActivity(), RssService.class); intent.putExtra(RssService.RECEIVER, resultReceiver); getActivity().startService(intent); } /** * Once the {@link RssService} finishes its task, the result is sent to this ResultReceiver. */ private final ResultReceiver resultReceiver = new ResultReceiver(new Handler()) { @SuppressWarnings("unchecked") @Override protected void onReceiveResult(int resultCode, Bundle resultData) { List<RssItem> items = (List<RssItem>) resultData.getSerializable(RssService.ITEMS); if (items != null) { RssAdapter adapter = new RssAdapter(getActivity(), items); listView.setAdapter(adapter); } else { Toast.makeText(getActivity(), "An error occured while downloading the rss feed.", Toast.LENGTH_LONG).show(); } progressBar.setVisibility(View.GONE); listView.setVisibility(View.VISIBLE); }; }; @Override public void onItemClick(AdapterView<?> parent, View view, int position, long id) { RssAdapter adapter = (RssAdapter) parent.getAdapter(); RssItem item = (RssItem) adapter.getItem(position); Uri uri = Uri.parse(item.getLink()); Intent intent = new Intent(Intent.ACTION_VIEW, uri); startActivity(intent); } }
We are using the setRetainInstance() method to automatically save the fragment’s state across screen configuration changes. There’s however one thing that we should take in account. The onCreateView() method will be called each time on screen orientation. If the XML layout will be inflated again, you will loose the state of the views.
The solution is to keep the root of view hierarchy as a field in the fragment, so that is saved after configuration change. However, this view is still attached to the old hierarchy, so you need to remove it and re-attach it to the current hierarchy:
@Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { if (view == null) { view = inflater.inflate(R.layout.fragment_layout, container, false); //..... } else { // If we are returning from a configuration change: // "view" is still attached to the previous view hierarchy // so we need to remove it and re-attach it to the current one ViewGroup parent = (ViewGroup) view.getParent(); parent.removeView(view); } return view; }
To get the result from the service, we are using the ResultReceiver. This class allows us to receive a callback result from the service once the task is finished. The only thing we need to do, is to override the onReceiveResult().
Notice how the resultReceiver is passed to the RssService, before starting it:
// .... intent.putExtra(RssService.RECEIVER, resultReceiver); getActivity().startService(intent);
Now the service will use the resultReceiver to notify the fragment that the service has finished its task and pass the data to it.
7. And this is the layout of the fragment: fragment_layout.xml
<?xml version="1.0" encoding="utf-8"?> <RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="match_parent" android:layout_height="match_parent" android:orientation="vertical" > <ListView android:visibility="gone" android:id="@+id/listView" android:layout_width="fill_parent" android:layout_height="fill_parent" > </ListView> <ProgressBar android:id="@+id/progressBar" style="?android:attr/progressBarStyleLarge" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_centerInParent="true" /> </RelativeLayout>
The layout contains the ListView which will hold the items, and the ProgressBar which is displayed while the rss feed is downloaded.
8. Every item from the list is represented by an object of type RssItem.
public class RssItem { private final String title; private final String link; public RssItem(String title, String link) { this.title = title; this.link = link; } public String getTitle() { return title; } public String getLink() { return link; } }
Thus, the title and the link of an rss item is encapsulated in a single object, so we don’t have to store all the titles, and all the links in separate arrays like we did in previous version.
9. And here’s the RssAdapter that works with the rss items:
public class RssAdapter extends BaseAdapter { private final List<RssItem> items; private final Context context; public RssAdapter(Context context, List<RssItem> items) { this.items = items; this.context = context; } @Override public int getCount() { return items.size(); } @Override public Object getItem(int position) { return items.get(position); } @Override public long getItemId(int id) { return id; } @Override public View getView(int position, View convertView, ViewGroup parent) { ViewHolder holder; if (convertView == null) { convertView = View.inflate(context, R.layout.rss_item, null); holder = new ViewHolder(); holder.itemTitle = (TextView) convertView.findViewById(R.id.itemTitle); convertView.setTag(holder); } else { holder = (ViewHolder) convertView.getTag(); } holder.itemTitle.setText(items.get(position).getTitle()); return convertView; } static class ViewHolder { TextView itemTitle; } }
We extend BaseAdapter and provide implementations for the inherited methods.
10. rss_item.xml: the layout of an item from the list.
<?xml version="1.0" encoding="utf-8"?> <TextView xmlns:android="http://schemas.android.com/apk/res/android" android:id="@+id/itemTitle" android:textSize="18dp" android:layout_width="match_parent" android:layout_height="wrap_content" />
11. Next is the implementation of RssService.java
public class RssService extends IntentService { private static final String RSS_LINK = "http://www.pcworld.com/index.rss"; public static final String ITEMS = "items"; public static final String RECEIVER = "receiver"; public RssService() { super("RssService"); } @Override protected void onHandleIntent(Intent intent) { Log.d(Constants.TAG, "Service started"); List<RssItem> rssItems = null; try { PcWorldRssParser parser = new PcWorldRssParser(); rssItems = parser.parse(getInputStream(RSS_LINK)); } catch (XmlPullParserException e) { Log.w(e.getMessage(), e); } catch (IOException e) { Log.w(e.getMessage(), e); } Bundle bundle = new Bundle(); bundle.putSerializable(ITEMS, (Serializable) rssItems); ResultReceiver receiver = intent.getParcelableExtra(RECEIVER); receiver.send(0, bundle); } public InputStream getInputStream(String link) { try { URL url = new URL(link); return url.openConnection().getInputStream(); } catch (IOException e) { Log.w(Constants.TAG, "Exception while retrieving the input stream", e); return null; } } }
The service’s job is to parse the rss feed and send the list of items to the fragment.
12. The actual xml parser, PcWorldRssParser.java
public class PcWorldRssParser { // We don't use namespaces private final String ns = null; public List<RssItem> parse(InputStream inputStream) throws XmlPullParserException, IOException { try { XmlPullParser parser = Xml.newPullParser(); parser.setFeature(XmlPullParser.FEATURE_PROCESS_NAMESPACES, false); parser.setInput(inputStream, null); parser.nextTag(); return readFeed(parser); } finally { inputStream.close(); } } private List<RssItem> readFeed(XmlPullParser parser) throws XmlPullParserException, IOException { parser.require(XmlPullParser.START_TAG, null, "rss"); String title = null; String link = null; List<RssItem> items = new ArrayList<RssItem>(); while (parser.next() != XmlPullParser.END_DOCUMENT) { if (parser.getEventType() != XmlPullParser.START_TAG) { continue; } String name = parser.getName(); if (name.equals("title")) { title = readTitle(parser); } else if (name.equals("link")) { link = readLink(parser); } if (title != null && link != null) { RssItem item = new RssItem(title, link); items.add(item); title = null; link = null; } } return items; } private String readLink(XmlPullParser parser) throws XmlPullParserException, IOException { parser.require(XmlPullParser.START_TAG, ns, "link"); String link = readText(parser); parser.require(XmlPullParser.END_TAG, ns, "link"); return link; } private String readTitle(XmlPullParser parser) throws XmlPullParserException, IOException { parser.require(XmlPullParser.START_TAG, ns, "title"); String title = readText(parser); parser.require(XmlPullParser.END_TAG, ns, "title"); return title; } // For the tags title and link, extract their text values. private String readText(XmlPullParser parser) throws IOException, XmlPullParserException { String result = ""; if (parser.next() == XmlPullParser.TEXT) { result = parser.getText(); parser.nextTag(); } return result; } }
The source code of the project is hosted on github. If you want to download the whole project follow this link: https://github.com/vgrec/SimpleRssReader
