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,169
Last edited:

corwin42

Expert
Licensed User
Longtime User
Has the issue of the disappearing widgets been fixed?

You are talking about the issue with the disappearing images in ImageViews in a widget?
This is an issue with the internal handling of bitmaps for RemoteViews in Android and has nothing to do with the subject of this tutorial. Please open a new thread for this issue.
 

NeoTechni

Well-Known Member
Licensed User
Longtime User
You are talking about the issue with the disappearing images in ImageViews in a widget?
This is an issue with the internal handling of bitmaps for RemoteViews in Android and has nothing to do with the subject of this tutorial. Please open a new thread for this issue.

I really hate splitting 1 topic up into a hundred threads. Makes it harder for everyone to keep track of it (especially the people who need to keep track of it, ie: you). It should all be in one, organized, easy to find place.

Especially since if/when you solve the issue (you said you were working on a library?) every single person who uses this code will need it. Not just me.
 

corwin42

Expert
Licensed User
Longtime User
This thread is about "multiple instances of a widget with different data".

The problem you have is not related to this subject. So if we merge several problems in one thread it all will get a mess and nobody will ever find the information. So it is better to have one thread for one issue.

For info: I have reported your problem in the past:Problem with widget with many /large images
 
Last edited:

NeoTechni

Well-Known Member
Licensed User
Longtime User
But the issue only happens when using this tutorial.
I didnt have it when i used only one widget type.

So if we merge several problems in one thread it all will get a mess and nobody will ever find the information

I feel no one will find it if you split it up. The solution would be updated into the first post
 

corwin42

Expert
Licensed User
Longtime User
But the issue only happens when using this tutorial.
I didnt have it when i used only one widget type.

Ok then it may look like it is related to the tutorial but trust me, it is not. I had this issue with "normal" widgets, too.

I fixed it for my project with a small library but this fix works because I use static images (drawables) from the resource directory. A general solution with generated images would be more complicated. The trick will be to save the bitmap to a file and give the ImageView of the widget a URI to the file and not the bitmap directly. It is even more complicated because the launcher needs to have access rights to the file you save. I will see if I can create an example.

Edit: And another small problem is that the solution will only work with Android SDK >= 8
 
Last edited:

Laurent95

Active Member
Licensed User
Longtime User
Sorry, it took some time.
Hello,

Thank you for do it, that works fine. And no worries it seems that i was busy also, then forgive me for late reply ;)

I think if I add a nice application Icon it could be released to the market without change. :)

Lolllll indeed, and never i will do it, i respect your work. Even if i try to animate an imageview inside a widget, and take a weather/clock widget (who is really inspired from your work), for the example :)

Best regards.
 

Ionut Indigo

Member
Licensed User
Longtime User
Hi corwin42, I'm trying to compile the application and i get:
B4X:
res\xml\advancedwidget_info.xml:3: error: No resource identifier found for attribute 'minResizeWidth' in package 'android'
res\xml\advancedwidget_info.xml:3: error: No resource identifier found for attribute 'previewImage' in package 'android'
res\xml\advancedwidget_info.xml:3: error: No resource identifier found for attribute 'resizeMode' in package 'android'
res\xml\advancedwidget_info.xml:3: error: No resource identifier found for attribute 'widgetCategory' in package 'android'
Libraries AHLocale, RandomAccesFile and Reflection are checked.
What is there to do?
Thanks!
 

Ionut Indigo

Member
Licensed User
Longtime User
Thanks corwin42, that worked.
Very good tutorial!

(was this app developed for landscape, because the transparent panel is a little bit off, no biggie)
 

Attachments

  • 2013-07-23_19-23-37.jpg
    2013-07-23_19-23-37.jpg
    42.5 KB · Views: 333
Last edited:

Laurent95

Active Member
Licensed User
Longtime User
Hi all
Just for you know how the work of Corwin is good, and of course some other guys who are very good also in coding :)
Take a look at this application :
https://play.google.com/store/apps/details?id=lizaprod.b4a.widget.easyclockweather&hl=en,
that i have does in starting from this tutorial in the first time. At this time, there are soon 17 000 installations since November 1st 2013.

I would thank you Markus for this very good tutorial about widgets and the usage of reflection library from Andrew Graham.
Respect for your work.

Regards.
 

Laurent95

Active Member
Licensed User
Longtime User
Laurent95,
good work!!!
The data, are stored in the database or in the preferences ?
Thank

Hello,
Thank you for the appreciation.
On this widget the data are stored in a file in the cache folder of application.
I don't stored all of the weather data, due to multiple instances of the widget possible.
That's the library who is called for the forecast and the weather data.
For the preferences it's the module called "StateManager v1.15" who manage all.
And Indeed as NeoTechni said, you can store it where you want, the module is clear and easy to understand.

Regards
 

Mrjoey

Active Member
Licensed User
Longtime User
Hello , is it possible to use statelistdrawable buttons and panels with background image? thanks.
 

leitor79

Active Member
Licensed User
Longtime User
Hi!

Thank you very much for sharing this! Very useful!

I'm wondering where the preview|thumbnail for the widget add is taken. I mean (the right one):

2017-09-27 00_12_12.png

I've edited/remove all advancedwidget_layout_X.xml files from the layout and layout-land folders, but I still see the same box (my layouts doesn't even have a time control). Tried cleaning project (ctrl+p), deleting cache & data before uninstalling the app, with no luck.

Thank you very much!
 

corwin42

Expert
Licensed User
Longtime User
Sorry for the late answer.

The preview is a png file found in Objects\res\drawable

It can be configured in the Objects\res\xml\advancedwidget_info.xml
 
Top