Tuesday, February 22, 2011

Loading Twitter Data into Android with Lists

Loading Twitter Data into Android with Lists: "

In my previous article, we created your first Android app featuring a basic Hello World function. Well, it seems the real Hello World of the mobile world is a Twitter feed.

This tutorial will guide you through creating a simple Android app to display a list of tweets coming from the JSON based search API provided by Twitter. We will work through:

  • displaying a list of items
  • customizing the look of each list item
  • accessing remote services and parsing data
  • creating responsive user interfaces

For instructions on how to set up an Android development environment, take a look at the previous article, which guides you all the way from installing software and establishing a workspace through to running a skeleton app in the Android Emulator.

Displaying Lists

To get started on our new project, create a new Android project in Eclipse (File -> New -> Other -> Android and select Android Project), entering an appropriate project name, application name, package name, and a name for the single launch activity.

You can see the options I’ve used below:

Android Project Set Up

To check that you have created the project correctly, run the skeleton app in the emulator by highlighting the project name in Eclipse and selecting Run -> Run As -> Android Application.

Currently the Activity you have created will be extending android.app.Activity, and in your onCreate method you will be setting the ContentView of the activity to R.layout.main (linking to the layout as specified in /res/layout/main.xml). The Android SDK provides a convenient way to quickly display a list of data using a superclass called android.app.ListActivity. This Activity already provides a ContentView, configured with a ListView, ready to use and populate with data.

Now change the superclass of TwitterFeedActivity to extend ListActivity, removing the setting of the ContentView from the onCreate method.

The ListView now needs to be given data to display, along with a means to map that data into rows. ListAdaptors provide this mechanism and are set on the underlying ListView of the ListActivity using setListAdaptor.

Create some sample data containing two Strings, and an Android SDK provided adaptor (ArrayAdaptor) that knows how to handle arrays of arbitrary data into ListViews (The Android SDK also comes with several other ListAdaptors, such as Cursor Adaptors, which can assist when connecting local data storage to a ListView). You also need to provide the adaptor with a layout it can use to render the elements onto each row. In the example below we are using the Android SDK provided layout, simple_list_item_1, which is a single text label–perfect for laying our single strings:

public class TwitterFeedActivity extends ListActivity {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
String[] elements = {"Line 1", "Line 2"};
setListAdapter(new ArrayAdapter<String>(this, android.R.layout.simple_list_item_1, elements));
}
}

Adjust your code as above and confirm the following results in the emulator:

Android Emulator

Customizing List Items

The basic Twitter client needs to display at least two fields per row: the author of the tweet, and the content of the tweet. In order to achieve this you will have to move beyond the inbuilt layout and ArrayAdaptor and implement your own instead.

Start by creating a class called Tweet that can be used to hold both the author and the content as Strings. Then create and populate a Tweet object with some test data to be displayed in the custom list item:

package com.sitepoint.mytwitter;
public class Tweet {
String author;
String content;
}

Create a layout XML file in /res/layout/list_item.xml to define two TextViews to display the content and author on separate rows. In order to display them one above the other, use a LinearLayout, configured to render each element within it vertically (android:orientation='vertical').

<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="fill_parent"
android:layout_height="?android:attr/listPreferredItemHeight"
android:padding="6dip">
<TextView android:id="@+id/toptext" android:layout_width="fill_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical" android:singleLine="true" />
<TextView android:layout_width="fill_parent"
android:layout_height="wrap_content"  android:id="@+id/bottomtext"
android:singleLine="true" />
</LinearLayout>

Once the  XML file has been created, the Android Eclipse plugin will automatically add it as a reference into the generated R file. This R file is kept under the /gen folder of your project and acts as a bridge between your XML elements and your Java code. It allows your Java code to reference XML elements and files created under the /res folders. The file you have just created can now be referenced as R.layout.list_item in the Java code, as you will do next in the custom list adaptor.

Create a private class (inside the Activity) called TweetListAdaptor which subclasses ArrayAdaptor. This class should be used to store an ArrayList of the Tweets being displayed, as well as providing a way to map the Tweet objects to the TextViews you created in the layout above.

This mapping overrides the getView method in ListAdaptor, and should return a View object populated with the contents of the data at the requested position:

public View getView(int position, View convertView, ViewGroup parent) { 

Furthermore, it should–if possible–reuse any View objects being passed into the method through the convertView parameter. If a new View object has to be created for every element in a list, large lists would stutter when they scroll. Caching Views allows the minimum amount of View objects to be created to populate a large list of data.

The complete implementation of the custom TweetListAdaptor is below. Note the “if statement” checking whether the passed convertView is able to be reused. If the View is null, the ListView has run out of Views to display and requires a new View to be created for this row.

This is achieved using the LayoutService, which is able to inflate (or load) the layout as you specified earlier in XML. You will see here how to reference the layout file using the generated R class through the R.layout.list_item reference.

Once a View is created (or reused), the particular Tweet is extracted from the ArrayList at the position required to be rendered by the ListView. You are then able to obtain references to the two TextView elements using the ids assigned to them in the XML (for example, android:id='@+id/toptext'). Once you have the references, you can set them with the appropriate content and author from the Tweet object.

private class TweetListAdaptor extends ArrayAdapter<Tweet> {
private ArrayList<Tweet> tweets;
public TweetListAdaptor(Context context,
int textViewResourceId,
ArrayList<Tweet> items) {
super(context, textViewResourceId, items);
this.tweets = items;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
LayoutInflater vi = (LayoutInflater) getSystemService                         (Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate(R.layout.list_item, null);
}
Tweet o = tweets.get(position);
TextView tt = (TextView) v.findViewById(R.id.toptext);
TextView bt = (TextView) v.findViewById(R.id.bottomtext);
tt.setText(o.content);
bt.setText(o.author);
return v;
}
}

Now the onCreate method can be adjusted to use the custom list adaptor with the created test data as shown below:

public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
Tweet tweet = new Tweet();
tweet.author = "dbradby";
tweet.content = "Android in space";
ArrayList<Tweet> items = new ArrayList<Tweet>();
items.add(tweet);
TweetListAdaptor adaptor = new TweetListAdaptor(this,R.layout.list_item, items);
setListAdapter(adaptor);
}

which will display the following, when run in the emulator:

Android Emulator Twitter Feed

Accessing remote services and parsing data

The Android SDK contains packages aimed at simplifying access to HTTP-based APIs. The Apache HTTP classes have been included and can be found under the org.apache.http package. You’ll be using these classes, along with the org.json classes to parse the data coming back from a call out the to Twitter search API.

Before any remote services can be accessed from an Android app, a permission has to be declared. This will alert the user (of your app) of what the app might be doing. Permissions could be to monitor incoming SMS, read the address book or, in your case, to simply make a request on the internet. These permissions are displayed to a user in the Android Market place before an app is installed, giving the user the choice of whether they want to give this app the declared permissions.

The permission you need to use is INTERNET and should be inserted outside of the application tag in the AndroidManifest.xml file.

</application>
<uses-permission android:name="android.permission.INTERNET" />
</manifest>

With the permission in place, we can create a private method in the Activity that makes a request, parses the result, and returns an ArrayList of Tweet objects. The code listed below makes the request and looks for the resulting JSON array, which is iterated to extract each tweet’s text and from_user elements.

private ArrayList<Tweet> loadTweets(){
ArrayList<Tweet> tweets = new ArrayList<Tweet>();
try {
HttpClient hc = new DefaultHttpClient();
HttpGet get = new HttpGet("http://search.twitter.com/search.json?q=android");
HttpResponse rp = hc.execute(get);
if(rp.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
{
String result = EntityUtils.toString(rp.getEntity());
JSONObject root = new JSONObject(result);
JSONArray sessions = root.getJSONArray("results");
for (int i = 0; i < sessions.length(); i++) {
JSONObject session = sessions.getJSONObject(i);
Tweet tweet = new Tweet();
tweet.content = session.getString("text");
tweet.author = session.getString("from_user");
tweets.add(tweet);
}
}
} catch (Exception e) {
Log.e("TwitterFeedActivity", "Error loading JSON", e);
}
return tweets;
}

Now replace the dummy data you previously used with a call to the loadTweets method when constructing the custom list adaptor in the onCreate method.

TweetListAdaptor adaptor = new TweetListAdaptor(this,R.layout.list_item, loadTweets());

Run this in the emulator and you should now see real data coming back from the API request being displayed in the list view as below:

Android Emulator MyTwitter

Creating responsive user interfaces

The code in its current state has the potential to cause an Application Not Responding (ANR) dialog to appear, prompting the user to quit your app. This can occur due to the long-running work of making a remote request for data being performed within methods such as onCreate.

Long-running tasks should never be performed on the main application thread (which drives the user interface event loop). They should instead be spawned off into child threads to perform the work.

While Java’s Thread class can be used for this task, there is a complication in that once the long-running task is complete, it generally wants to change the user interface to report the results (that is, display a list of tweets loaded from a request).

User interface elements can only have their state altered from the main thread, as the Android UI toolkit is not thread-safe, therefore the background thread needs to message back to the main thread in order to manipulate the UI.

Thankfully, the Android SDK provides a convenient class AsyncTask, which provides an easy mechanism for asynchronous tasks to interact safely with the UI thread. This is achieved by subclassing AsyncTask and overriding the doInBackground method to perform the long-running task, then overriding onPostExecute to perform any manipulations on the UI.

When the AsyncTask is created (it has to be created on the UI thread) and executed, the doInBackground method is invoked on a background thread. On completion, the onPostExecute method is invoked back on the main UI thread.

To use this in your app, you will need to implement a private class within the Activity (like the custom adaptor class) called MyTask, which subclasses AsyncTask. You can override the doInBackground method with the contents of your previous loadTweets method.

Instead of the ArrayList being returned, you maintain an instance variable in the Activity so that the data can be shared across the private classes. Then in the onPostExecute you can set the List Adaptor with the data, as was done previously in onCreate. The onCreate method now simply creates the MyTask object and calls the execute method.

In order to demonstrate the UI responding while the background request is being performed, I have added a ProgressDialog to the AsyncTask as well. The complete listing of the Activity is below:

package com.sitepoint.mytwitter;
import java.util.ArrayList;
import org.apache.http.HttpResponse;
import org.apache.http.HttpStatus;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.json.JSONArray;
import org.json.JSONObject;
import android.app.ListActivity;
import android.app.ProgressDialog;
import android.content.Context;
import android.os.AsyncTask;
import android.os.Bundle;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
public class TwitterFeedActivity extends ListActivity {
private ArrayList<Tweet> tweets = new ArrayList<Tweet>();
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
new MyTask().execute();
}
private class MyTask extends AsyncTask<Void, Void, Void> {
private ProgressDialog progressDialog;
protected void onPreExecute() {
progressDialog = ProgressDialog.show(TwitterFeedActivity.this,
"", "Loading. Please wait...", true);
}
@Override
protected Void doInBackground(Void... arg0) {
try {
HttpClient hc = new DefaultHttpClient();
HttpGet get = new HttpGet("http://search.twitter.com/search.json?q=android");
HttpResponse rp = hc.execute(get);
if(rp.getStatusLine().getStatusCode() == HttpStatus.SC_OK)
{
String result = EntityUtils.toString(rp.getEntity());
JSONObject root = new JSONObject(result);
JSONArray sessions = root.getJSONArray("results");
for (int i = 0; i < sessions.length(); i++) {
JSONObject session = sessions.getJSONObject(i);
Tweet tweet = new Tweet();
tweet.content = session.getString("text");
tweet.author = session.getString("from_user");
tweets.add(tweet);
}
}
} catch (Exception e) {
Log.e("TwitterFeedActivity", "Error loading JSON", e);
}
return null;
}
@Override
protected void onPostExecute(Void result) {
progressDialog.dismiss();
setListAdapter(new TweetListAdaptor(
TwitterFeedActivity.this, R.layout.list_item, tweets));
}
}
private class TweetListAdaptor extends ArrayAdapter<Tweet> {
private ArrayList<Tweet> tweets;
public TweetListAdaptor(Context context,
int textViewResourceId,
ArrayList<Tweet> items) {
super(context, textViewResourceId, items);
this.tweets = items;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = convertView;
if (v == null) {
LayoutInflater vi = (LayoutInflater) getSystemService(Context.LAYOUT_INFLATER_SERVICE);
v = vi.inflate(R.layout.list_item, null);
}
Tweet o = tweets.get(position);
TextView tt = (TextView) v.findViewById(R.id.toptext);
TextView bt = (TextView) v.findViewById(R.id.bottomtext);
tt.setText(o.content);
bt.setText(o.author);
return v;
}
}
}

You should now have a complete app requesting the Twitter data in the background.

The AsyncTask has quite a few more features to explore. You can be sure we will cover those in future articles. In the meantime you should read the excellent Android document covering these classes at http://developer.android.com.






"

No comments: