Populate AppWidget ListView with remote data(data from web)



This tutorial is follow up of previous tutorial AppWidget With ListView. In this tutorial,we will just populate the AppWidget ListView with data downloaded from web. For this purpose we are using

  1. Json Data
  2. AQuery


AQuery is an awesome library to work out with remote data be it simple json files,xml files or images,bitmaps to name a few. Go to AQuery to know more about this library. Now let us create a appwidget configuration activity. This appwidget configuration Activty is the Activity which gets launched when appwidget is placed on homescreen i.e. when appwidget is selected from widgets of phone and drag and dropped to homescreen. For any appwidget configuration activity to work it must be declared on

  1. AndroidManifest.xml file with intent action android.appwidget.action.APPWIDGET_CONFIGURE
  2. <!-- Configuration activity which gets 
         launched  on widget being placed on
         homescreen for first time  -->
    <activity android:name=".ConfigActivity" >
    <!-- This intent is required to be recognized
         this activity as appwidget configuration activity -->
       <intent-filter>
           <action android:name=
               "android.appwidget.action.APPWIDGET_CONFIGURE" />
       </intent-filter>
    </activity>
    
  3. And be mentioned on appwidget-provider xml file @res/xml/widgetinfo.xml on attribute android:configure
    <?xml version="1.0" encoding="utf-8"?>
    <appwidget-provider xmlns:android="http://schemas.android.com/apk/res/android"
      android:configure="com.wordpress.laaptu.ConfigActivity"
      android:initialLayout="@layout/widget_layout"
      android:minHeight="110dip"
      android:minWidth="250dip"
      android:previewImage="@drawable/widget_preview"
      android:resizeMode="vertical|horizontal"  
      android:updatePeriodMillis="1800000" />
    


Now let us fill up our ConfigActivity. This Activity is launched once AppWidget is placed and it contains simply a Button. Upon click of ConfigActivity's Button, AppWidget is launched along with a Service is started to fetch our json data.

public class ConfigActivity extends Activity implements OnClickListener {

	private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;

	@Override
	protected void onCreate(Bundle savedInstanceState) {
		super.onCreate(savedInstanceState);
		setContentView(R.layout.configactivity);

		assignAppWidgetId();
		findViewById(R.id.widgetStartButton).setOnClickListener(this);
	}

	/**
	 * Widget configuration activity,always receives appwidget Id appWidget Id =
	 * unique id that identifies your widget analogy : same as setting view id
	 * via @+id/viewname on layout but appwidget id is assigned by the system
	 * itself
	 */
	private void assignAppWidgetId() {
		Bundle extras = getIntent().getExtras();
		if (extras != null)
			appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID,
					AppWidgetManager.INVALID_APPWIDGET_ID);
	}

	@Override
	public void onClick(View v) {
		if (v.getId() == R.id.widgetStartButton)
			startWidget();
	}

	/**
	 * This method right now displays the widget and starts a Service to fetch
	 * remote data from Server
	 */
	private void startWidget() {

		// this intent is essential to show the widget
		// if this intent is not included,you can't show
		// widget on homescreen
		Intent intent = new Intent();
		intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
		setResult(Activity.RESULT_OK, intent);

		// start your service
		// to fetch data from web
		Intent serviceIntent = new Intent(this, RemoteFetchService.class);
		serviceIntent
				.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
		startService(serviceIntent);

		// finish this activity
		this.finish();

	}

}


Read out the comments on ConfigActivity to know about its working. Now let’s create our Service whose main task is to

  • Fetch data from web
  • Populate AppWidget ListView with fetched data

An AppWidgetProvider is basically a BroadCastReciever,so whenever one needs to communicate to AppWidgetProvider,one must send broadcast from activity,service with necessary action string. And this is the pattern on which AppWidgetProvider works.Though this is not compulsory,try to create RemoteViews on AppWidgetProvider and call update from within AppWidgetProvider.
Let’s go through this one by one. First let’s create our remote data fetch service and name it as

public class RemoteFetchService extends Service {

	private int appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID;
	private AQuery aquery;
	private String remoteJsonUrl = "https://laaptu.files.wordpress.com/2013/07/widgetlist.key";

	public static ArrayList<ListItem> listItemList;

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

	/**
	 * Retrieve appwidget id from intent it is needed to update widget later
	 * initialize our AQuery class
	 */
	@Override
	public int onStartCommand(Intent intent, int flags, int startId) {
		if (intent.hasExtra(AppWidgetManager.EXTRA_APPWIDGET_ID))
			appWidgetId = intent.getIntExtra(
					AppWidgetManager.EXTRA_APPWIDGET_ID,
					AppWidgetManager.INVALID_APPWIDGET_ID);
		aquery = new AQuery(getBaseContext());
		fetchDataFromWeb();
		return super.onStartCommand(intent, flags, startId);
	}

	/**
	 * method which fetches data(json) from web aquery takes params
	 * remoteJsonUrl = from where data to be fetched String.class = return
	 * format of data once fetched i.e. in which format the fetched data be
	 * returned AjaxCallback = class to notify with data once it is fetched
	 */
	private void fetchDataFromWeb() {
		aquery.ajax(remoteJsonUrl, String.class, new AjaxCallback<String>() {
			@Override
			public void callback(String url, String result, AjaxStatus status) {
				processResult(result);
				super.callback(url, result, status);
			}
		});
	}

	/**
	 * Json parsing of result and populating ArrayList<ListItem> as per json
	 * data retrieved from the string
	 */
	private void processResult(String result) {
		listItemList = new ArrayList<ListItem>();
		try {
			JSONArray jsonArray = new JSONArray(result);
			int length = jsonArray.length();
			for (int i = 0; i < length; i++) {
				JSONObject jsonObject = jsonArray.getJSONObject(i);
				ListItem listItem = new ListItem();
				listItem.heading = jsonObject.getString("heading");
				listItem.content = jsonObject.getString("content");
				listItem.imageUrl = jsonObject.getString("imageUrl");
				listItemList.add(listItem);
			}

		} catch (JSONException e) {
			e.printStackTrace();
		}
		populateWidget();
	}

	/**
	 * Method which sends broadcast to WidgetProvider
	 * so that widget is notified to do necessary action
	 * and here action == WidgetProvider.DATA_FETCHED
	 */
	private void populateWidget() {

		Intent widgetUpdateIntent = new Intent();
		widgetUpdateIntent.setAction(WidgetProvider.DATA_FETCHED);
		widgetUpdateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
				appWidgetId);
		sendBroadcast(widgetUpdateIntent);

		this.stopSelf();
	}
}


Look upon method populateWidget(),where I have created a broadcast with action WidgetProvider.DATA_FETCHED which is a simple string

public static final String DATA_FETCHED="com.wordpress.laaptu.DATA_FETCHED";

For this broadcast to be received by our AppWidgetProvider named WidgetProvider.java,this action string must be included on intent-filter action at AndroidManifest.xml

 <receiver android:name=".WidgetProvider" >
    <intent-filter>
       <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
       <!-- To receive broadcast with this string name -->
       <action android:name="com.wordpress.laaptu.DATA_FETCHED" />
     </intent-filter>
        <meta-data
           android:name="android.appwidget.provider"
            android:resource="@xml/widgetinfo" />
     </receiver>

Now let us modify our WidgetProvider.java to reflect the changes. Previously ,all the updates of widget were handled on onUpdate() method.But from now on onUpdate() will just start RemoteFetchService i.e. on every 30 min interval,to fetch new data RemoteFetchService will be called to fetch data from web and notify WidgetProvider with broadcast. Just like any BroadCastReceiver, WidgetProvider onReceive() method will receive the sent broadcast. And we just find out the broadcast action and carry out necessary updates through AppWidgetManger. Check out our modified WidgetProvider.java to know more

public class WidgetProvider extends AppWidgetProvider {

	// String to be sent on Broadcast as soon as Data is Fetched
	// should be included on WidgetProvider manifest intent action
	// to be recognized by this WidgetProvider to receive broadcast
	public static final String DATA_FETCHED = "com.wordpress.laaptu.DATA_FETCHED";

	/**
	 * this method is called every 30 mins as specified on widgetinfo.xml this
	 * method is also called on every phone reboot from this method nothing is
	 * updated right now but instead RetmoteFetchService class is called this
	 * service will fetch data,and send broadcast to WidgetProvider this
	 * broadcast will be received by WidgetProvider onReceive which in turn
	 * updates the widget
	 */
	@Override
	public void onUpdate(Context context, AppWidgetManager appWidgetManager,
			int[] appWidgetIds) {
		final int N = appWidgetIds.length;
		for (int i = 0; i < N; i++) {
			Intent serviceIntent = new Intent(context, RemoteFetchService.class);
			serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
					appWidgetIds[i]);
			context.startService(serviceIntent);
		}
		super.onUpdate(context, appWidgetManager, appWidgetIds);
	}

	private RemoteViews updateWidgetListView(Context context, int appWidgetId) {

		// which layout to show on widget
		RemoteViews remoteViews = new RemoteViews(context.getPackageName(),
				R.layout.widget_layout);

		// RemoteViews Service needed to provide adapter for ListView
		Intent svcIntent = new Intent(context, WidgetService.class);
		// passing app widget id to that RemoteViews Service
		svcIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
		// setting a unique Uri to the intent
		// don't know its purpose to me right now
		svcIntent.setData(Uri.parse(svcIntent.toUri(Intent.URI_INTENT_SCHEME)));
		// setting adapter to listview of the widget
		remoteViews.setRemoteAdapter(appWidgetId, R.id.listViewWidget,
				svcIntent);
		// setting an empty view in case of no data
		remoteViews.setEmptyView(R.id.listViewWidget, R.id.empty_view);
		return remoteViews;
	}

	/**
	 * It receives the broadcast as per the action set on intent filters on
	 * Manifest.xml once data is fetched from RemotePostService,it sends
	 * broadcast and WidgetProvider notifies to change the data the data change
	 * right now happens on ListProvider as it takes RemoteFetchService
	 * listItemList as data
	 */
	@Override
	public void onReceive(Context context, Intent intent) {
		super.onReceive(context, intent);
		if (intent.getAction().equals(DATA_FETCHED)) {
			int appWidgetId = intent.getIntExtra(
					AppWidgetManager.EXTRA_APPWIDGET_ID,
					AppWidgetManager.INVALID_APPWIDGET_ID);
			AppWidgetManager appWidgetManager = AppWidgetManager
					.getInstance(context);
			RemoteViews remoteViews = updateWidgetListView(context, appWidgetId);
			appWidgetManager.updateAppWidget(appWidgetId, remoteViews);
		}

	}


Finally the updated data is now referenced from ListProvider.java class,which just clones the ArrayList of RemoteFetchService class as

private void populateListItem() {
  listItemList = (ArrayList<ListItem>)
                 RemoteFetchService.listItemList
				.clone();
}


In this manner the fetched data from the web is now received ListProvider which in turn is reflected on our AppWidget. This approach of using public static ArrayList has a major flaw. When they are more than 2 AppWidgets and they at the same time fetch and begin to modify public static ArrayList of RemoteFetchService,then the last modified value will be taken by both the AppWidget which is incorrect for one of the widget. So,in order to resolve this,better use Database to store values with given AppWidget Id. And on RemoteViewsService or on RemoteViewsFactory,simply fetch the data from Database with that AppWidget Id.
I have put the entire project My Github. Till next time, enjoy life.

Advertisements

18 thoughts on “Populate AppWidget ListView with remote data(data from web)

  1. Pingback: Android Showing Remote Image on ImageView of App-Widget ListView | Laaptu
  2. Pingback: Android Setting Update Interval On Appwidget With Listview | Laaptu
  3. I have use your code as it is, instead of your web service i.e ” https://laaptu.files.wordpress.com/2013/07/widgetlist.key” I have used my web service. Its retrieving data but widget doesnt get refresh with new data as I have reduced time in widgetinfo.xml to 5000 but still widget is not getting refreshed.

    Is there any need to change this line “public static final String DATA_FETCHED = “com.wordpress.laaptu.DATA_FETCHED”; ”

    Please suggest me solution… its very urgent…

    • Hello Pradip, you have to do the following

      1)On AppWidgetProvider instead of calling
      appWidgetManager.updateAppWidget(appWidgetId, remoteViews); you must call
      final ComponentName cn = new ComponentName(context,
      WidgetProvider.class);
      appWidgetManager.notifyAppWidgetViewDataChanged(
      appWidgetManager.getAppWidgetIds(cn),
      R.id.tweetListView);
      which signifies that your data has been downloaded and now it is time to indicate to RemoteViewsFactory that data has been changed. If you are familiar with ListView Adapter where you call adapter.notifyDataSetChanged() whenver data is changed to ListView

      2) You must change the data i.e. the widget is receiving on RemoteViewsFactory onDataSetChanged. Meaning if you are using ArrayList someList,then update its value at there.

      Once you do those,the update is reflected on the widget.

      • I implemented 1st change you mentioned and tried for second as follow but not working..

        In ListProvider which implements RemoteViewsFactory,
        onDataSetChanged I tried to call “populateListItem();”
        which is called in constructor which sets arraylist initially but this does not work and not even set data to list first time also.

        Please help !!!

    • Time in widgetinfo.xml is irrelevant as Android has a system limitation to only update the widget at minimum of every 30 minutes. Setting the value to 5000 ms will not work. I am in the testing phase of my own application, and have resorted to using an alarm service, which makes the data update every 20 seconds for testing purposes.

      I am also getting an issue with non refresh of the data within the widget. I’m using my own async task to fetch a json object and parse it myself. The alarm is successfully triggering the parsing, but it is not dispalying the results back tot he widget. Any advice?

      • Edit…I was able to get it working by following Laaptu’s advice regarding not calling appWidgetManager.updateAppWidget(appWidgetId, remoteViews);. I instead made the call to instanitate a new component namke and to notifyAppWidgetDataChanged method. In my RemoteViewsFactory onDataSetChanged.. I simply inserted this statement if(UpdateService.listItemList !=null ){
        listItemList = (ArrayList) UpdateService.listItemList.clone();
        Log.i(“checking”,listItemList.get(0).heading);
        }
        All appears to be working fine now.

      • further note.. when calling onDatasetChanged, it will not make a call to populateListItem. Whatever procedure you were using to instantiate your data set will need to be done in onDataSetChanged(); ie if(UpdateService.listItemList !=null ){
        listItemList = (ArrayList) UpdateService.listItemList.clone();
        Log.i(“checking”,listItemList.get(0).heading);
        }

  4. Hi,
    Thanks a lot for this tutorial.
    I need to add image button on top of list and onclick of button want to fetch new data from webservice.

    Could you please provide any help.

    • Hello Paddy,
      Are you referring to add refresh button or something like at top,so that when it is clicked webservice is called.

      • Yes, I am able to fetch data initially but to refresh list with new data i want to add a button so that on click of button I can fetch new data with same web service and update data to listview removing previous data.

        My web service is returning new data everytime so i want to call it on button click and update in list..

  5. i still have the same problem of Pradip.
    I modified the onReceive method of the WidgetProvider in this way:

    final ComponentName cn = new ComponentName(context,
    WidgetProvider.class);
    appWidgetManager.notifyAppWidgetViewDataChanged(
    appWidgetManager.getAppWidgetIds(cn),R.id.listViewWidget);

    I also modified the ondataSetChanged of the listProvider:
    public void onDataSetChanged() {
    if(RemoteFetchService.listItemList !=null )
    listItemList = (ArrayList) RemoteFetchService.listItemList
    .clone();
    else
    listItemList = new ArrayList();
    }

    But it’s still not loading after 30 secs…
    Any help?

      • Hello Salvo, did you do the process done by Amit Goel as presented in the comments.
        Meaning , onUpdate method won’t be sufficient to change the data items on list,you have take help of onDataSetChanged of ListProvider. Then only you will see the changes on your list.
        Just try once and look up the comments and tell me.

  6. Thanks!
    I need RemoteView is List Click Event in ListPrivider.java getViewAt.
    I don’t Know setOnClickFillIntent used not working….

    Help me~~~~

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s