I recently developed my first appwidget for Android, and it works really nice. I basically created a collection widget and use a Loader to retrieve the data from a database, and when a user clicks on the list item, that items details will be saved into a bundle in an intent, and the main activity will be launched with the bundled info displayed to the user.
My code works as intended; however, I am concerned with possible edge cases in how widgets actually work, initialize, and update that I have not thought of. Again, I am new to widget development, and the API for widgets took a lot for me to wrap my head around.
Question Objective
While any code review is helpful, I am basing my question on the premise set up here to answer #1 and #2 from the reviewer's point of view:
- Does this code actually do what it is supposed to do under normal conditions?
- Are there edge cases where this code will fail?
This GitHub link links directly to my Hub Flavor that implements the appwidget (Java and res folders, and manifest file).
App Widget Provider
public class ResumeHubWidgetProvider extends AppWidgetProvider {
private static final String LAUNCH_RESUME_ACTION = "io.github.ciscorucinski.personal.intro.hub.LAUNCH_RESUME_ACTION";
private static Intent createIntent(Context context) {
return new Intent(context, ResumeHubWidgetProvider.class);
}
private static void updateAppWidget(Context context, AppWidgetManager appWidgetManager,
int appWidgetId) {
Intent serviceIntent = MyWidgetService.createIntent(context);
serviceIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME)));
// Construct the RemoteViews object
RemoteViews views = new RemoteViews(context.getPackageName(), R.layout.resume_hub_list_widget);
views.setRemoteAdapter(R.id.widget_list, serviceIntent);
Intent widgetProviderIntent = ResumeHubWidgetProvider.createIntent(context);
widgetProviderIntent.setAction(ResumeHubWidgetProvider.LAUNCH_RESUME_ACTION);
widgetProviderIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId);
serviceIntent.setData(Uri.parse(serviceIntent.toUri(Intent.URI_INTENT_SCHEME)));
PendingIntent pendingIntent = PendingIntent.getBroadcast(context, 0, widgetProviderIntent,
PendingIntent.FLAG_UPDATE_CURRENT);
views.setPendingIntentTemplate(R.id.widget_list, pendingIntent);
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views);
}
@Override
public void onUpdate(Context context, AppWidgetManager appWidgetManager, int[] appWidgetIds) {
// There may be multiple similar widgets active, so update all of them
for (int appWidgetId : appWidgetIds) {
updateAppWidget(context, appWidgetManager, appWidgetId);
}
}
@Override
public void onReceive(Context context, Intent intent) {
AppWidgetManager appWidgetManager = AppWidgetManager.getInstance(context);
if (intent.getAction().equals(LAUNCH_RESUME_ACTION)) {
Timber.i("Intent Action is LAUNCH_RESUME_ACTION");
// Open the Resume activity with the user selected resume info
Bundle bundle = intent.getBundleExtra(CREATE_INTENT_BUNDLE);
context.startActivity(ResumeActivity
.createIntentWithFlags(context, bundle,
Intent.FLAG_ACTIVITY_NEW_TASK));
}
int appWidgetIds[] = appWidgetManager.getAppWidgetIds(new ComponentName(context, ResumeHubWidgetProvider.class));
appWidgetManager.notifyAppWidgetViewDataChanged(appWidgetIds, R.id.widget_list);
super.onReceive(context, intent);
}
}
Adapter
class HubWidgetAdapter implements RemoteViewsService.RemoteViewsFactory,
Loader.OnLoadCompleteListener<List<Resume.People>> {
private Context context;
private List<Resume.People> data;
private int appWidgetId;
private PeopleLoader loader;
HubWidgetAdapter(Context context, Intent intent) {
this.context = context;
appWidgetId = intent.getIntExtra(AppWidgetManager.EXTRA_APPWIDGET_ID,
AppWidgetManager.INVALID_APPWIDGET_ID);
}
@Override
public RemoteViews getViewAt(int position) {
RemoteViews view = new RemoteViews(context.getPackageName(),
android.R.layout.simple_list_item_1);
Resume.People person = data.get(position);
view.setTextViewText(android.R.id.text1, person.seeking_position());
view.setTextColor(android.R.id.text1, Color.BLACK);
Bundle bundle = new Bundle();
bundle.putLong(ResumeActivity.ID, person._id());
bundle.putString(ResumeActivity.NAME, person.name());
bundle.putString(ResumeActivity.EMAIL, person.email());
bundle.putString(ResumeActivity.PHONE, person.phone());
bundle.putString(ResumeActivity.GITHUB, person.github());
bundle.putString(ResumeActivity.LINKEDIN, person.linkedin());
bundle.putString(ResumeActivity.SEEKING, person.seeking_position());
Intent intent = new Intent();
intent.putExtra(CREATE_INTENT_BUNDLE, bundle);
Timber.i("Bundled Person%s", intent);
view.setOnClickFillInIntent(android.R.id.text1, intent);
return view;
}
@Override
public void onLoadComplete(Loader<List<Resume.People>> loader, List<Resume.People> data) {
this.data = data;
}
@Override
public void onCreate() {
data = new ArrayList<>();
}
@Override
public void onDestroy() {
if (loader != null) {
loader.unregisterListener(this);
loader.cancelLoad();
loader.stopLoading();
loader = null;
}
data = null;
}
@Override public void onDataSetChanged() {
loader = new PeopleLoader(context);
loader.registerListener(0, this);
loader.startLoading();
}
@Override public int getCount() { return data.size(); }
@Override public RemoteViews getLoadingView() { return null; }
@Override public int getViewTypeCount() { return 1; }
@Override public long getItemId(int position) { return position; }
@Override public boolean hasStableIds() { return true; }
}
Service
public class MyWidgetService extends RemoteViewsService {
static Intent createIntent(Context context) {
return new Intent(context, MyWidgetService.class);
}
@Override
public RemoteViewsFactory onGetViewFactory(Intent intent) {
return new HubWidgetAdapter(this.getApplicationContext(), intent);
}
}
Manifest (Hub Flavor)
The Hub flavor is the only aspect of my code that has appwidgets, so I am only including the Hub Manifest file here that is merged into the main Manifest file.
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<application>
<activity
android:name="io.github.ciscorucinski.personal.intro.ui.ResumeActivity"
android:label="@string/app_name"
android:theme="@style/AppTheme.NoActionBar"/>
<receiver android:name="io.github.ciscorucinski.personal.intro.hub.ResumeHubWidgetProvider">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/resume_hub_list_widget_info" />
</receiver>
<service
android:name="io.github.ciscorucinski.personal.intro.hub.MyWidgetService"
android:exported="false"
android:permission="android.permission.BIND_REMOTEVIEWS" />
</application>
</manifest>