[TUT] Twitter Feed on an App Widget

Hi guys, this is a Tutorial Request to show how to create an Android widget that shows a Twitter feed when you give it a search term. The first line of the widget shows the search term, the rest of the widget is used to show the latest tweet in the form “username : tweet”.

Simple widget that shows a twitter feed

The request stated they are familiar with creating app widgets, so the focus of this article will be the twitter integration and parsing to be delivered to the widget. The application includes an Activity that is used for you to change the search term.

As usual all classes will not be posted here, so please see the eclipse source or github for the full source.

What we’re going to do:

  1. Create an Activity so we can input a search term
  2. Create a widget with an update period of 30 minutes
  3. Create a service to populate and update our widget
  4. Create a task to talk to Twitter, requesting tweets about our search term
  5. Shows the latest of these tweets on our widget

Ok Here .. we .. go

Creating the Activity, this will be a simple activity with an input edittext and a button to save the input. This input will be the search term used by our widget. If you don’t do this and just add a widget the search term will be blank, so in a production build you might want to set some defaults. When the ‘save’ button is pressed the UI is updated to show the saved search term, it also prompts the user to close the app and go add a widget to the homescreen.

activity_main.xml

<RelativeLayout xmlns:android="https://schemas.android.com/apk/res/android"
    xmlns:tools="https://schemas.android.com/tools"
    android:layout_width="match_parent"
    android:layout_height="match_parent" >

    <TextView
        android:id="@+id/textView_instructions"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:padding="@dimen/padding_medium"
        android:text="@string/twitter_widget_instructions" />

    <TextView
        android:id="@+id/textView_twitter_search_term"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/textView_instructions"
        android:padding="@dimen/padding_medium" />

    <EditText
        android:id="@+id/editText_twitter_search_term"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/textView_twitter_search_term"
        android:inputType="text" />

    <Button
        android:id="@+id/button_set_twitter_search_term"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/editText_twitter_search_term"
        android:onClick="onSetTwitterSearchTermClick"
        android:text="@string/save" />

    <TextView
        android:id="@+id/textView_further_instructions"
        android:layout_width="fill_parent"
        android:layout_height="wrap_content"
        android:layout_below="@id/button_set_twitter_search_term"
        android:padding="@dimen/padding_medium"
        android:text="@string/widget_add_instructions"
        android:visibility="invisible" />

</RelativeLayout>

MainActivity.java

package com.blundell.tut.twitterfeedwidget.ui;

import com.blundell.tut.twitterfeedwidget.FromXML;
import com.blundell.tut.twitterfeedwidget.R;
import com.blundell.tut.twitterfeedwidget.persistance.PreferenceConstants;
import com.blundell.tut.twitterfeedwidget.persistance.WidgetPreferences;
import com.blundell.tut.twitterfeedwidget.receiver.TwitterWidgetProvider;

import android.app.Activity;
import android.appwidget.AppWidgetManager;
import android.content.ComponentName;
import android.content.SharedPreferences;
import android.os.Bundle;
import android.view.View;
import android.widget.EditText;
import android.widget.TextView;

/**
 * A config style activity, here you can set the search term that the widget will show the first tweet for. This is saved into SharedPreferences
 * and used by the widget to show the tweets.
 * you can also reopen this activity to change the search term.
 * If you check the twitter_appwidget_info.xml file the current update time for this widget is every 30 minutes.
 *
 * @author paul.blundell
 *
 */
public class MainActivity extends Activity {

	// The preferences to remember the search term
    private WidgetPreferences widgetPrefs;
	private TextView currentTwitterSearchTermTextView;
	private EditText newTwitterSearchTermEditText;
	private TextView addWidgetInstructionsTextView;

	@Override
    public void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);

        initWidgetPreferences();
        initViews();
        populateViews();
    }

	/**
	 * This is the onClick to change the search term, we don't do any validation on the input
	 * This will save the search term, update the activity UI and also update any currently added widgets
	 * @param button
	 */
	@FromXML
	public void onSetTwitterSearchTermClick(View button){
		String newSearchTerm = getInputTwitterSearchTerm();
		widgetPrefs.saveTwitterSearchTerm(newSearchTerm);
		currentTwitterSearchTermTextView.setText(newSearchTerm);
		addWidgetInstructionsTextView.setVisibility(View.VISIBLE);
		updateAllWidgets();
	}

	private void initWidgetPreferences() {
		SharedPreferences sharedPreferences = getSharedPreferences(PreferenceConstants.WIDGET_PREFS, MODE_PRIVATE);
        widgetPrefs = new WidgetPreferences(sharedPreferences);
	}

	private void initViews() {
		currentTwitterSearchTermTextView = (TextView) findViewById(R.id.textView_twitter_search_term);
		newTwitterSearchTermEditText = (EditText) findViewById(R.id.editText_twitter_search_term);
		addWidgetInstructionsTextView = (TextView) findViewById(R.id.textView_further_instructions);
	}

	private void populateViews() {
		currentTwitterSearchTermTextView.setText(widgetPrefs.getTwitterSearchTerm());
	}

	private String getInputTwitterSearchTerm() {
		return newTwitterSearchTermEditText.getText().toString();
	}

	private void updateAllWidgets(){
	    AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(getApplicationContext());
	    int[] appWidgetIds = appWidgetManager.getAppWidgetIds(new ComponentName(this, TwitterWidgetProvider.class));
	    if (appWidgetIds.length > 0) {
	        new TwitterWidgetProvider().onUpdate(this, appWidgetManager, appWidgetIds);
	    }
	}
}

Next off we will create the widget with an update period of 30 minutes. To create a widget you need an AppWidgetProvider, Provider XML properties and a widget layout file. Our layout file has two textviews to display the search term being used and the tweet received:

appwidget_twitter.xml

<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="https://schemas.android.com/apk/res/android"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:padding="@dimen/widget_margin" >

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        android:background="@drawable/widget_background"
        android:orientation="vertical"
        android:padding="10dip" >

        <TextView
            android:id="@+id/textView_widget_search_term"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />

        <TextView
            android:id="@+id/textView_widget_tweet"
            android:layout_width="fill_parent"
            android:layout_height="wrap_content" />
    </LinearLayout>

</FrameLayout>

We also define properties for our widget, stating how often we want the widget to be updated, giving it a width and height but also allowing it to be resizeable (>4.0).

twitter_appwidget_info.xml

<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider xmlns:android="https://schemas.android.com/apk/res/android"
    android:initialLayout="@layout/appwidget_twitter"
    android:minHeight="80dip"
    android:minResizeHeight="80dip"
    android:minResizeWidth="80dip"
    android:minWidth="80dip"
    android:resizeMode="horizontal|vertical"
    android:updatePeriodMillis="300000" />

Then we need the receiver, this is where the broadcast from the system is caught to say ‘update our widget’ how often this is called depends on the updatePeriodMillis in our properties file (so 30 minutes).

TwitterWidgetProvider.java

package com.blundell.tut.twitterfeedwidget.receiver;

import com.blundell.tut.twitterfeedwidget.service.UpdateService;

import android.appwidget.AppWidgetManager;
import android.appwidget.AppWidgetProvider;
import android.content.Context;
import android.content.Intent;

/**
 * Starts our service where we will update the widgets
 *
 * @author paul.blundell
 *
 */
public class TwitterWidgetProvider extends AppWidgetProvider {

	@Override
	public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
		Intent intent = new Intent(context, UpdateService.class);
        intent.putExtra(UpdateService.EXTRA_WIDGET_IDS, appWidgetIds);
        context.startService(intent);
	}

}

Since we will be doing a lot of work when our widget updates (i.e. talking to twitter and parsing a response) we start a service to complete this work. The service extends IntentService this allows us do networking calls (because networking calls can’t be done on the UI thread), IntentService will manage and run threads for us, which is nice!

UpdateService.java

package com.blundell.tut.twitterfeedwidget.service;

import com.blundell.tut.twitterfeedwidget.R;
import com.blundell.tut.twitterfeedwidget.persistance.PreferenceConstants;
import com.blundell.tut.twitterfeedwidget.persistance.WidgetPreferences;
import com.blundell.tut.twitterfeedwidget.service.task.RetrieveTweetTask;
import com.blundell.tut.twitterfeedwidget.util.Log;

import android.app.IntentService;
import android.appwidget.AppWidgetManager;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.widget.RemoteViews;

/**
 * This is our service to update the widget, here we will get latest tweet for our search term and display it on the widget
 * Using IntentService allows us to do network calls as it spawns its own threads
 * @author paul.blundell
 *
 */
public class UpdateService extends IntentService {

	public UpdateService() {
		super("UpdateService");
	}

	public UpdateService(String name) {
		super(name);
	}

	public static final String EXTRA_WIDGET_IDS = "com.blundell.tut.provider.UpdateService.EXTRA_WIDGET_IDS";

	@Override
	protected void onHandleIntent(Intent intent) {
		Log.d("Widget onHandleIntent Update Service started");
		if(intent != null){
            updateAllWidgets(intent);
        }
	}

	private void updateAllWidgets(Intent intent) {
		int[] appWidgetIds = getAppWidgetIdentifiers(intent);
		for (int appWidgetId : appWidgetIds) {
			refreshWidget(appWidgetId);
		}
	}

	private static int[] getAppWidgetIdentifiers(Intent intent) {
		return intent.getIntArrayExtra(EXTRA_WIDGET_IDS);
	}

	private void refreshWidget(int appWidgetId) {
		RemoteViews view = buildViewUpdate(this, appWidgetId);

		updateWidget(appWidgetId, view);
	}

	private static RemoteViews buildViewUpdate(Context context, int appWidgetId) {
		String searchTerm = getTwitterSearchTerm(context);
		String tweet = getLatestTweet(searchTerm);

		RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.appwidget_twitter);
		views.setTextViewText(R.id.textView_widget_search_term, searchTerm);
		views.setTextViewText(R.id.textView_widget_tweet, tweet);

		return views;
	}

	private static String getTwitterSearchTerm(Context context) {
		WidgetPreferences prefs = getWidgetPreferences(context);
		return prefs.getTwitterSearchTerm();
	}

	private static WidgetPreferences getWidgetPreferences(Context context) {
		return new WidgetPreferences(context.getSharedPreferences(PreferenceConstants.WIDGET_PREFS, MODE_PRIVATE));
	}

	/**
	 * This is the task that will talk to twitter and do the parsing of the response
	 * @param searchTerm
	 * @return the latest tweet for the given search term
	 */
	private static String getLatestTweet(String searchTerm) {
		return new RetrieveTweetTask().retrieveFor(searchTerm);
	}

	private void updateWidget(int appWidgetId, RemoteViews views) {
		AppWidgetManager manager = AppWidgetManager.getInstance(this);
		manager.updateAppWidget(appWidgetId, views);
		Log.d("widget updated:"+ appWidgetId);
	}

	@Override
	public IBinder onBind(Intent intent) {
		return null;
	}
}

The IntentService will talk to our SharedPreferences to retrieve the search term that was entered into the activity. It then starts a task that goes off to the internet to talk to the Twitter API. The Twitter API then returns us some JSON that we need to parse.

RetrieveTweetTask.java

package com.blundell.tut.twitterfeedwidget.service.task;

import java.io.IOException;
import java.io.InputStream;
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;

import org.apache.http.HttpResponse;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.DefaultHttpClient;
import org.json.JSONException;

import com.blundell.tut.twitterfeedwidget.parser.TwitterJsonSearchParser;
import com.blundell.tut.twitterfeedwidget.util.Log;

/**
 * Connects to the internet using Twitters RESTful API to retrieve a list of tweets
 *
 * Does networking ensure it is not ran on the UI thread
 * @author paul.blundell
 *
 */
public class RetrieveTweetTask {

	// The API to use see: https://dev.twitter.com/docs/using-search
	private static final String TWITTER_SEARCH_API_URL = "https://search.twitter.com/search.json?q=";
	private final HttpClient client;

	public RetrieveTweetTask() {
		client = new DefaultHttpClient();
	}

	/**
	 * Given a search term will return the latest tweet for that term
	 * @param searchTerm the search term to use
	 * @return a tweet or blank if an error occurs
	 */
	public String retrieveFor(String searchTerm) {
		String tweet = "";

		try {
			// Connect to twitter
			HttpGet getTweet = new HttpGet(TWITTER_SEARCH_API_URL+ URLEncoder.encode(searchTerm, "UTF-8"));
			HttpResponse response = client.execute(getTweet);
			InputStream inputStream = response.getEntity().getContent();
			// Parse the returned JSON
			TwitterJsonSearchParser jsonSearchParser = new TwitterJsonSearchParser(inputStream);
			tweet = jsonSearchParser.getLatestTweet();

		} catch (ClientProtocolException e) {
			Log.e("ClientProtocolException", e);
		} catch (UnsupportedEncodingException e){
			Log.e("UnsupportedEncodingException", e);
		} catch (IOException e) {
			Log.e("IOException", e);
		} catch (JSONException e) {
			Log.e("JSONException", e);
		}

		return tweet;
	}
}

For the retrieval and parsing this tutorial uses really simple error catching and you might want to do something a bit more involved, maybe tell the user there search term is lame or that they have no internet connection etc.

In this example the JSON parsing is also very simple, we just look for the text of the tweet and who sent it, getting these strings and passing them back to our widget. One thing to look out for is, this example always returns the 0’th indexed tweet, therefore if they search for something complex and there are no tweets returned, then everything will blow up and it’ll just fail silently showing a blank widget.

TwitterJsonSearchParser.java

package com.blundell.tut.twitterfeedwidget.parser;

import java.io.InputStream;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import com.blundell.tut.twitterfeedwidget.util.Log;
import com.blundell.tut.twitterfeedwidget.util.StreamConverter;

/**
 * Parses the JSON returned from the Twitter API
 * See here for the format: https://dev.twitter.com/docs/api/1/get/search
 * @author paul.blundell
 *
 */
public class TwitterJsonSearchParser {

	private final JSONObject jsonObject;

	public TwitterJsonSearchParser(InputStream inputStream) throws JSONException {
		jsonObject = StreamConverter.convertStreamToJsonObject(inputStream);
	}

	public String getLatestTweet() {
		String tweet = "Error";
		try {
			tweet = parseJsonForLatestTweet();
		} catch (JSONException e) {
			Log.e("Failed to get latest tweet", e);
		}
		return tweet;
	}

	private String parseJsonForLatestTweet() throws JSONException {
		JSONArray jsonTweets = getTweetResults();
		JSONObject jsonFirstTweet = getLatestTweet(jsonTweets);

		String fromUser = getUserOfTweet(jsonFirstTweet);
		String tweetText = getTweet(jsonFirstTweet);

		return createTweet(fromUser, tweetText);
	}

	private JSONArray getTweetResults() throws JSONException {
		return jsonObject.getJSONArray("results");
	}

	private static JSONObject getLatestTweet(JSONArray json) throws JSONException {
		return json.getJSONObject(0);
	}

	private static String getUserOfTweet(JSONObject json) throws JSONException {
		return json.getString("from_user");
	}

	private static String getTweet(JSONObject json) throws JSONException {
		return json.getString("text");
	}

	private static String createTweet(String user, String text) {
		return user + ": " + text;
	}
}

Thats it! You can now see tweets from your given search term, also updating this search term through the activity we have created.

Hope you find this useful please say thank you!

Here is the TwitterFeedWidgetTut Eclipse Source for download.

Here is a mirror on GitHub