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:
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.mai
n (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:
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:
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:
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 doInBackgroun
d 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:
Post a Comment