Android Tutorial Multiple instances of the same widget

General Information

Normally it is not possible to create several widget instances of the same homescreen widget which all display different data.

I found a solution for this and with the help of a small code module with some special subs it works really great and is not too complicated to use.

What's missing from plain B4A are the following things:

- A possibility to start a configuration Activity for your widget to make individual settings
- A possibility to update only a single widget instance and not all of them
- A possibility to get a list of widgets created by the widget service
- A way to decide which widget fired a click event

WARNING: This tutorial is for the advanced B4A developer. You need to have basic knowledge of Intents and XML modification.
But you don't need to understand the magic stuff done in the provided code module subs. If you want to understand this you will need some knowledge about how the RemoteViews object works and how it is implemented.

Setting up a configuration activity

To make a widget individual from others you will need a possibility to change some settings for a single widget instance. For this you can create a widget configuration activity. This activity has to handle some extra information in the starting intent and has to return a special result to the widget.

To start we need a Project with a widget service completely set up. You have to compile the project at least once to create the xml files we have to change.

Just create a normal B4A activity called "WidgetConfig" for the configuration activity. We have to add a special intent filter in the manifest editor for it:

B4X:
AddActivityText (WidgetConfig , <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_CONFIGURE"/>
    </intent-filter> )

Then we have to tell our widget that it has to call a configuration activity on creation of a new widget. This has to be done in the widget provider info xml found under Objects\res\xml\<widgetservicename>_info.xml.
Add the following property to the xml (replace <your.package.name> with you apps package name. This adds our activity as a configuration activity.

B4X:
android:configure="<your.package.name>.widgetconfig"

Be sure to make the file read only after this or use the new B4A2.50 CustomBuildActions!

Note that the following code is not complete (all UI stuff is missing). See the example for the complete code!

Now we can take a look at the activity code. The activity is called with the AppWidgetId in the extra data. We have to save this in a global variable so we have access to the id everywhere.

B4X:
Sub Activity_Create(FirstTime As Boolean )
        StartingIntent = Activity.GetStartingIntent
       
        If StartingIntent.HasExtra("appWidgetId" ) Then
               gAppWidgetId = StartingIntent.GetExtra("appWidgetId")
        Else
               gAppWidgetId = -1
        End If
End Sub

We need to save the settings for the widget instances somewhere. I decided to use StateManager code module for this. In the example there are two settings for each widget. A "cityname" and a "timezone". The WidgetId is added to the setting name so we can see what setting is for which widget instance. So for widget "42" the setting for the city name is called "cityname_42".

If the user changed the settings we have to tell the launcher if the user canceled the operation or saved the settings. For this the activity must return a result (0 for cancel, -1 for ok, in the example there are variables for these values).

B4X:
Sub Activity_Resume
   'Initialize the result code with "CANCEL"
   Activity.SetActivityResult(ACTIVITY_RESULT_CANCELED, StartingIntent)
End Sub

Sub Activity_Pause (UserClosed As Boolean)
   'If the activity is paused, close it and cancel the configuration process
   Activity.Finish
End Sub

Sub btSave_Click
   'The user clicked "Save" so set the OK-Result
   Activity.SetActivityResult(ACTIVITY_RESULT_OK, StartingIntent)
   'Save the settings for the widgetId
   StateManager.SetSetting("cityname_" & gAppWidgetId, eLocation.Text)
   StateManager.SetSetting("timezone_" & gAppWidgetId, spTimeZone.GetItem(spTimeZone.SelectedIndex))
   StateManager.SaveSettings

   gAppWidgetId = -1
   Activity.Finish
End Sub

The widget service

The main difference of our widget service to a normal B4A widget service is that we first need the information for which widget the service was called. The information is provided in the starting intent. There are 3 possibilities:

1. The widget service is called with a single widget id. This happens when configuring the widget or deleting a single widget instance from the homescreen. We have to handle/update only this widget.

2. The widget service is called with a list of widgets. If this is the case we have to handle/update all widgets in the list.

3. The widget service is called with no widget ids. This happens when the service is started with "StarServiceAt". In this case we have to find out the widget ids ourself. For this there is a Sub called GetWidgetIds() in the Util code module.

B4X:
Sub Service_Start (StartingIntent As Intent)
   mAppWidgetIdList.Clear
   'Is the service called from a single widget?
   If StartingIntent.HasExtra("appWidgetId") Then
      mAppWidgetIdList.Add(StartingIntent.GetExtra("appWidgetId"))
   End If

   Dim helper() As Int
   'Is there a list of widget ids in the Intent?
   If mAppWidgetIdList.Size = 0 Then
      If StartingIntent.HasExtra("appWidgetIds") Then
         helper = StartingIntent.GetExtra("appWidgetIds")
         mAppWidgetIdList.AddAll(helper)
      End If
   End If

   'Is the service called manually without any widget ids in starting intent?
   If mAppWidgetIdList.Size = 0 Then
      helper = Util.GetWidgetIds("advancedwidget")
      mAppWidgetIdList.AddAll(helper)
   End If

   'If there is no widget on the homescreen we can just exit
   If mAppWidgetIdList.Size = 0 Then
      Log("No AppWidget Ids found!")
      Return
   End If
End Sub

After we have a list of widget ids (or just a single id) we can handle/update every widget instance by its own. There are two more actions we can handle for our widget: "APPWIDGET_UPDATE_OPTIONS" and "APPWIDGET_DELETE".
The first is called when the app returns from the configuration activity. The second one if a single widget instance is removed.

Updating the widget is done by setting up the data for the RemoteViews object and then update a single widget instance with Util.UpdateSingleWidget() in a loop over all WidgetIds in list of widget ids.

B4X:
Sub Widget_RequestUpdate
   Dim LocationText As String
   Dim tzLocal As AHTimeZone
   tzLocal.Initialize

   'Instead of just updating the widget like in normal B4A code we need to loop over all widget ids and
   'set the content of each widget individually.
   'Here the setting information for each widget is read and the content is updated for each widget instance
   'individually.
   For i = 0 To mAppWidgetIdList.Size - 1
      Log("Update Widget: " & mAppWidgetIdList.Get(i))
      Dim tz As AHTimeZone
      tz.Initialize2(StateManager.GetSetting2("timezone_" & mAppWidgetIdList.Get(i), tzLocal.ID))
      LocationText = StateManager.GetSetting2("cityname_" & mAppWidgetIdList.Get(i), "Default")

      Dim dt As AHDateTime
      dt.Initialize
      dt.TimeZone=tz
      dt.Pattern="HH:mm"

      rv.SetText("lLocation", LocationText)
      rv.SetText("lTimeZone", tz.ID)
      rv.SetText("lClock", dt.Format(DateTime.Now))

      'Just update one widget instance with this special sub
      Util.UpdateSingleWidget(rv, mAppWidgetIdList.Get(i))
   Next
End Sub

Handling Events

If we want to use click events of the views in our widget we will have a problem. The normal B4A click event for widget views does not contain a widget id so it is not possible to find out which widget fired the click event.

We have to overwrite the click event and setup our own. This is done with the Util.SetWidgetClickEvent() sub. We have to set up the event for each widgetid:

B4X:
If mAppWidgetIdList.Size > 0 Then
   For i = 0 To mAppWidgetIdList.Size - 1
      Log("AppWidgetId: " & mAppWidgetIdList.Get(i))
      Util.SetWidgetClickEvent("advancedwidget", mAppWidgetIdList.Get(i), rv, "pMain", "pMain")
      '... add more views the same way
   Next
End If

Now the service is called with the correct widget id when a view on the widget is clicked.

Thats all.

Provided is an example of a world clock widget. If you add a widget of this type the configuration activity will pop up. Here you can set a name for a location and a timezone. If you press "Apply changes" then the widget is added and the configured location is shown and a clock with the current time in this timezone. You can add as may widgets with different locations and timezones as you want. If you press one of the widgets, the configuration activity is shown again and you can change the settings of this single widget. So the example shows how to call the configuration activity manually. There are many comments in the example so I hope you will understand it.

The example is written for B4A 2.70 (The "magic" shown in this tutorial works with older B4A versions, too) and needs the Reflection library, AHLocale library and RandomAccessFile library (for the StateManager).

Questions welcome.
 

Attachments

  • WorldClock1_2.zip
    55.3 KB · Views: 1,262
Last edited:

leitor79

Active Member
Licensed User
Longtime User
Hi, again!

I've done 2 layouts, but I'd like each one to have different default widths. I've thought this could be done with some properties in each of the layout files, but I could't find any. Which approach could I take?

Thank you very much!
 
Top