In this series we are building a Twitter client for the Android platform using the Twitter4J library. This tutorial will create a SQLite database to store the user's home timeline. We will also map the data from the database to the Views we created last time using an Adapter, so that the user can see their updates.
Also available in this series:
- Creating a Twitter Client for Android: Setup & Overview
- Creating a Twitter Client for Android: Building the Interface
- Creating a Twitter Client for Android: Creating a Timeline Database
- Creating a Twitter Client for Android: Retrieving Updates Using a Service
- Creating a Twitter Client for Android: Tweeting, Retweeting, and Replying
In the first two tutorials we setup the project in Eclipse, registered the app with Twitter, imported the Twitter4J JAR file and built the user interface. We also handled getting users to sign in to their Twitter accounts and authorize the app to access their tweets.
This tutorial will create an SQLite database to store the user's home timeline. We will also map the data from the database to the Views we created last time using an Adapter, so that the user can see their updates.
Step 1: Create the Database
Let's get straight in and create the timeline database using SQLite. Create a new class in your project by selecting it in the Package Explorer, choosing File, New then Class. Enter "NiceDataHelper" as the class name. Open the new class file and extend the declaration as follows:
public class NiceDataHelper extends SQLiteOpenHelper
This is a database helper class for the SQLite system. In this class we will create and manage the database for the home timeline tweets.
Add the following import statements at the top of the class:
import android.content.Context; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteOpenHelper; import android.provider.BaseColumns; import android.util.Log;
These imports are for basic database processing and error logging, plus the application Context for general reference.
The database is going to contain a single table for the tweet updates. This table will include the following columns for each tweet: the Twitter status ID; the tweet text; the screen-name of the tweeter; the time the tweet was sent; the URL of the tweeter's profile image.
Start by creating some constants in the class:
/**db version*/ private static final int DATABASE_VERSION = 1; /**database name*/ private static final String DATABASE_NAME = "home.db"; /**ID column*/ private static final String HOME_COL = BaseColumns._ID; /**tweet text*/ private static final String UPDATE_COL = "update_text"; /**twitter screen name*/ private static final String USER_COL = "user_screen"; /**time tweeted*/ private static final String TIME_COL = "update_time"; /**user profile image*/ private static final String USER_IMG = "user_img";
These constant variables include the database version name, which you should alter if you wish to upgrade the database in future developments of your app. The variables also include the name of the database and names for the columns. The BaseColumns class helps us to assign a unique ID column using the suffix "_id".
Next build the SQLite create table String for the home table:
/**database creation string*/ private static final String DATABASE_CREATE = "CREATE TABLE home (" + HOME_COL + " INTEGER NOT NULL PRIMARY KEY, " + UPDATE_COL + " TEXT, " + USER_COL + " TEXT, " + TIME_COL + " INTEGER, " + USER_IMG + " TEXT);";
Create the constructor method for your database helper class:
/** * Constructor method * @param context */ NiceDataHelper(Context context) { super(context, DATABASE_NAME, null, DATABASE_VERSION); }
The constructor simply calls the superclass method. Next add the "onCreate" method in which we will execute creation of the database table:
/* * onCreate executes database creation string */ @Override public void onCreate(SQLiteDatabase db) { db.execSQL(DATABASE_CREATE); }
Here we are simply executing the database creation SQL String. At the end of the method the table will have been created.
To finish preparing our database, implement the "onUpgrade" method in case you decide to alter the database at some future time:
/* * onUpgrade drops home table and executes creation string */ @Override public void onUpgrade(SQLiteDatabase db, int oldVersion, int newVersion) { db.execSQL("DROP TABLE IF EXISTS home"); db.execSQL("VACUUM"); onCreate(db); }
This allows you to upgrade your database by altering the version number and creation String, or any of the columns.
We will now add a final method to our database helper class. This method is going to return values from the latest retrieved Twitter status updates and will be called from the Service class we create in the next tutorial. Add the method outline as follows:
/** * getValues retrieves the database records * - called from TimelineUpdater in TimelineService * - this is a static method that can be called without an instance of the class * * @param status * @return ContentValues result */ public static ContentValues getValues(Status status) { //prepare ContentValues to return ContentValues homeValues = new ContentValues(); //get the values //return the values return homeValues; }
Inside this method we are going to convert the passed Status object to a set of ContentValues. The Status class is part of the Twitter4J library, modelling a single status update, or tweet. When the application Service retrieves the user's home timeline, it will pass each of the Status updates in it to the "getValues" method. This method will retrieve the information from the tweet that we want to store in the database, i.e. the ID, text, username, time and profile image URL. The method will put each of these in a set of ContentValues which it will then return. The Service will then attempt to write the data in the ContentValues to the database, updating it with the newly retrieved tweets.
We will implement most of this process when we create the Service in the next tutorial. For now all we need to do is provide the body of the "getValues" method. Before the line in which the values are returned, add the following:
try { //get each value from the table homeValues.put(HOME_COL, status.getId()); homeValues.put(UPDATE_COL, status.getText()); homeValues.put(USER_COL, status.getUser().getScreenName()); homeValues.put(TIME_COL, status.getCreatedAt().getTime()); homeValues.put(USER_IMG, status.getUser().getProfileImageURL().toString()); } catch(Exception te) { Log.e("NiceDataHelper", te.getMessage()); }
The process can cause exceptions, so the try and catch blocks are necessary. Notice that the method attempts to retrieve each element of the Status update that we designed the database to store using the Twitter4J methods. You will need another couple of imports added to the class:
import twitter4j.Status; import android.content.ContentValues;
Step 2: Create an Adapter Class
We are going to use an Adapter class to map the tweet data to the user interface Views we created in the last tutorial. The ListView we created in the timeline XML layout file is going to show the new tweet updates as they become available. Each tweet update will be displayed within the update XML layout we also defined. If you look back at "res/layout/update.xml" you will see that there are sections for displaying all of the data items we are storing in the database, i.e. the user profile image and screen-name, the tweet and the time it was tweeted.
The Adapter class is going to map incoming update tweet data to these user interface View items. Rather than having to implement this manually, the "SimpleCursorAdapter" class lets us automate part of the process, mapping data to Views while still allowing us to tailor this mapping process to suit the needs of the app.
Create a new class in your app, naming it "UpdateAdapter". Extend the class declaration as follows:
public class UpdateAdapter extends SimpleCursorAdapter
Import the parent class and Log class as follows:
import android.widget.SimpleCursorAdapter; import android.util.Log;
Add instance variables as follows:
/**twitter developer key*/ public final static String TWIT_KEY = "your key";//alter /**twitter developer secret*/ public final static String TWIT_SECRET = "your secret";//alter /**strings representing database column names to map to views*/ static final String[] from = { "update_text", "user_screen", "update_time", "user_img" }; /**view item IDs for mapping database record values to*/ static final int[] to = { R.id.updateText, R.id.userScreen, R.id.updateTime, R.id.userImg }; private String LOG_TAG = "UpdateAdapter";
Alter the Twitter developer Key and Secret to reflect your own, as you used in the app's main Activity class in the last tutorial. The "from" array represents the name of each database table column being mapped. The "to" array represents the IDs of the Views that the "from" items are going to be mapped to. The IDs were all included in the XML layouts we created in the last tutorial.
Step 3: Map the Data to the User Interface
In your Adapter class, add a constructor method as follows:
/** * constructor sets up adapter, passing 'from' data and 'to' views * @param context * @param c */ public UpdateAdapter(Context context, Cursor c) { super(context, R.layout.update, c, from, to); }
This code calls the superclass constructor, passing the application Context, the View item in which the data is going to be displayed, the Cursor for traversing the data and the "from" and "to" arrays we created as instance variables. You will need the following import statements for this code:
import android.content.Context; import android.database.Cursor;
This performs the central part of the mapping process, i.e. displaying the data within the specified Views. However, we also want to tailor the behavior and appearance of the resulting user interface elements. We will be adapting the mapping procedure to fetch the profile image for each tweet and to format the update time. In the last tutorial of the series we will also extend this code to setup click listeners for the retweet button, the reply button and the Twitter username for each tweet.
Implement the "bindView" method within your Adapter class using the following outline:
/* * Bind the data to the visible views */ @Override public void bindView(View row, Context context, Cursor cursor) { super.bindView(row, context, cursor); }
Here we are simply calling the superclass method. The "bindView" method allows us to specify additional processing for when the data is mapped to the Views. First let's try to download the profile image for the current update tweet:
try { //get profile image URL profileURL = new URL(cursor.getString(cursor.getColumnIndex("user_img"))); //set the image in the view for the current tweet ImageView profPic = (ImageView)row.findViewById(R.id.userImg); profPic.setImageDrawable(Drawable.createFromStream ((InputStream)profileURL.getContent(), "")); } catch(Exception te) { Log.e(LOG_TAG, te.getMessage()); }
We need the try and catch blocks because we are attempting to load an external resource. The method first gets the URL of the profile image as stored in the database column. Then the method gets a reference to the View item to display the image in, using its ID attribute, as we set in the update XML layout file. Finally the code fetches the image as an InputStream, importing it as a Drawable and setting it as the image within the ImageView. You will need the following imports:
import java.io.InputStream; import java.net.URL; import android.graphics.drawable.Drawable; import android.view.View; import android.widget.ImageView;
Now let's amend the display of the update time, as it will be stored in the database as a number. To convert it to a human-readable relative time, add the following code after the catch block:
//get the update time long createdAt = cursor.getLong(cursor.getColumnIndex("update_time")); //get the update time view TextView textCreatedAt = (TextView)row.findViewById(R.id.updateTime); //adjust the way the time is displayed to make it human-readable textCreatedAt.setText(DateUtils.getRelativeTimeSpanString(createdAt)+" ");
First we retrieve the column value as a long, then get a reference to the View item to display it within as part of the update XML. Finally we format it as a String so that the user can see when the tweet was sent relative to the present time. You will need the following additional import statements:
import android.text.format.DateUtils; import android.widget.TextView;
Step 4: Build the Home Timeline
Back in the app's main Activity class "TwitNiceActivity" let's now implement the "setupTimeline" method. In the next tutorial we will add the Service processing to the method, but for now let's handle starting up the database and Adapter. Before you start on the method, add the following instance variables at the top of your class:
/**main view for the home timeline*/ private ListView homeTimeline; /**database helper for update data*/ private NiceDataHelper timelineHelper; /**update database*/ private SQLiteDatabase timelineDB; /**cursor for handling data*/ private Cursor timelineCursor; /**adapter for mapping data*/ private UpdateAdapter timelineAdapter;
We will use these variables when building the timeline. Add the following to specify the default size for profile image display:
ProfileImage.ImageSize imageSize = ProfileImage.NORMAL;
You will need to add the following imports:
import twitter4j.ProfileImage; import android.database.Cursor; import android.database.sqlite.SQLiteDatabase; import android.widget.ListView; import android.widget.LinearLayout;
Now to set the timeline up. If you created the "setupTimeline" method outline for testing last time, you can simply add the method body next. If not, create the method outline now:
/** * setupTimeline displays the user's main home Twitter timeline */ private void setupTimeline() { //method body }
You may remember from last time that this method is called when the user has already authorized the app to use their Twitter account. Within the method we will instantiate the Database Helper class, retrieve a readable database, instantiate the Adapter class and set it to map the data to our Views. Start by setting the timeline content View:
setContentView(R.layout.timeline);
Some of the methods we are going to use can throw exceptions, so next add try and catch blocks:
try { //get the timeline } catch(Exception te) { Log.e(LOG_TAG, "Failed to fetch timeline: " + te.getMessage()); }
We will add the next excerpts of code inside the try block. Here we are going to instantiate the variables we declared at the top of the class. First get a reference to the ListView from the timeline layout, in which the update Views will appear:
//get reference to the list view homeTimeline = (ListView)findViewById(R.id.homeList);
Next create an instance of the Database Helper class and use it to retrieve a readable Database:
//instantiate database helper timelineHelper = new NiceDataHelper(this); //get the database timelineDB = timelineHelper.getReadableDatabase();
The first line here creates an instance of the Database Helper class we defined, while the second actually retrieves the database. Now let's query the database and acquire a Cursor for traversing it, passing this Cursor to the Adapter class:
//query the database, most recent tweets first timelineCursor = timelineDB.query ("home", null, null, null, null, null, "update_time DESC"); //manage the updates using a cursor startManagingCursor(timelineCursor); //instantiate adapter timelineAdapter = new UpdateAdapter(this, timelineCursor);
The database query is simply retrieving everything from the table, ordering it with the most recent update tweets first. We pass the Cursor to the constructor method of the Adapter class we created.
Next
In this tutorial we have built our tweet timeline database, defined an Adapter for displaying the data within our user interface Views and utilized these classes within our app's main Activity, for building the visible timeline.
In the next tutorial we will set up a Service and Broadcast Receiver to continually fetch new updates and display them within the user's home timeline. At the moment when you run the app you will not see any updates, as we have not yet fetched the user's timeline. As part of the Service class we will fetch the timeline using the Twitter4J methods, calling these at set intervals to repeatedly retrieve the most recent tweets from the accounts the user follows. We will then write the results to the database and present them within the timeline display.
Comments