Reading Time: 13 minutes
In this tutorial we are going to build a more complex widget, where you can add and remove 1 from the number using the plus and minus buttons. In this advanced tutorial about the Android widgets we are going to learn how we can handle click events and update the widget without opening the Activity. We gonna learn also how we can add a configuration screen to the Android widgets as well and how we can update the widget from the app.
Basics tutorial
This tutorial assumes you’re that familiar with the basics of Android widgets. If you aren’t then please review our free Android widgets – Basics tutorial.
The sample app
As you can see, our sample application going to have a simple user interface, which contains a TextView with a number and a plus and a minus Buttons. The widget will have the same Views, and at the bottom of the widget a TextView, When we click on it then the MainActivity will open.
This app uses SharedPreferences to store the number and the selected limit for the instances of the Widget. When you click on the plus-minus buttons, then the number gonna be saved all the time.
To have communication between the app and the widget instances we gonna create a service. Using it we can update the widget from the MainActivity.
To update the MainActivity we won’t use any service, instead we gonna override the onCreate() and the onResume() methods to load the stored number using again SharedPreferences.
Let’s start coding. 😎
Step 1 – Create new project
First thing first, we gonna create a whole new project. For this, launch Android Studio. If you see the “Welcome page”, then click on the “Start a new Android Studio project”. If you have an open project, then from the “File” menu select “New”, then “New Project”. After thet, this window will popup.
Here select the “Empty Activity” option. In the next window, we should provide some information about the newly created app.
This tutorial will contain very few code, but still we have to select the programming language. As the other tutorials on this website, this will be written also in Kotlin. So, select from the dropdown list the Kotlin language.
From the next list of the “Minimum SDK” select API 21. In our case API 21 gonna be enough.
If you are done, click on the “Finish” button. The build can take few minutes. Be patient!
When the build has been finished, then you should see the open MainActivity::class and next to this the activity_main.xml files.
Step 2 – Add the Widget
You can create the widget manually, or you can add it using Android Studio. In the advanced tutorial we are going to select the second option again. In this case Android Studio is going to prepare for us the needed files.
So, with the right mouse button click on the main source set (where you can find the MainAcitvity.kt file as well). Then, from the quick menu select New, then the Widget option. From the submenu choose the App Widget option.
Our widget’s width gonna be 3 cells, and the heigth 2 cells. We need space for the 2 buttons. 😎
For this sample app we gonna create a Configuration screen as well, so check in its CheckBox.
Now, click on the Finish button.
Note that Android Studio has created a new Activity as well with the name of “AppWidgetConfigureActivity”. This Activity will be repsonsible for the configuration settings. About this we gonna talk later.
Step 3 – Widget layout
We won’t go into the details of the layout for the Widget, because as we have talked about earlier, it will be very simple. So just copy and paste the below xml lines into the app_widget.xml file, what you can find in the res->layout folder.
<RelativeLayout
android:id="@+id/widget_layout"
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@color/blue"
android:padding="@dimen/widget_margin">
<TextView
android:id="@+id/appwidget_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_centerHorizontal="true"
android:layout_margin="8dp"
android:text="@string/number"
android:textColor="#ffffff"
android:textSize="24sp"
android:textStyle="bold|italic" />
<LinearLayout
android:id="@+id/ll_plusMinus"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_below="@id/appwidget_text"
android:layout_marginTop="6dp">
<Button
android:id="@+id/btn_plus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="26sp"
android:text="+" />
<Button
android:id="@+id/btn_minus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="26sp"
android:text="-" />
</LinearLayout>
<TextView
android:id="@+id/tv_openApp"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/open_app"
android:layout_marginTop="8dp"
android:gravity="center_horizontal"
android:textColor="#ffffff"
android:layout_alignParentEnd="true"
android:layout_alignParentStart="true"
android:layout_below="@id/ll_plusMinus"/>
</RelativeLayout>
app_widget.xml
The missing blue and red colors go into the colors. xml file (res->values).
<color name=“blue”>#3669FF</string>
<color name=“red”>#FF4545</string>
And the missing strings go to the strings.xml file. (res->values)
<string name=“no_saved_number”>No saved number</string>
<string name=“number”>Number</string>
<string name=“open_app”>Open app</string>
Step 4 – Layout of the MainAcitvity
We won’t go into the details of the MainAcitivity‘s layout neither. It will be also very simple. So again, copy and paste the below xml code into the activity_main.xml file, what you can find also in the res->layout folders.
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:context=".MainActivity">
<TextView
android:id="@+id/tv_number"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
tools:text="12345"
android:textSize="36sp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:gravity="center"
android:layout_marginTop="6dp"
app:layout_constraintTop_toBottomOf="@+id/tv_number">
<Button
android:id="@+id/btn_plus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="26sp"
android:text="+" />
<Button
android:id="@+id/btn_minus"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:textSize="26sp"
android:text="-" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>
activity_main.xml
Step 5 –The Enum class for operations
To identify which button (plus or minus) we have clicked, we gonna create an Enum class, which gonna hold these 2 options. We are going to create it in the main source set, where you can find the already created Kotlin files.
So, click on the package name of the main source set with the right mouse button, select New and the Kotlin file/class options. In the popup window name the file as “Operation” and selct the Enum class option. Then press enter.
Extend the code with the below one.
enum class Operation(value: String)
{
ADDITION("ADDITION"),
SUBTRACTION("SUBTRACTION")
}
enum class Operation
Step 6 – SharedPreferences
To store the number and the limit, we gonna use SharedPreferences.
For the SharedPreferences operations we gonna create a new Kotlin file also in the main source set. So, create one with the name of “SharedPreferencesDAO”.
Thenafter paste the below Kotlin code into the new file.
import android.content.Context
class SharedPreferencesDAO (private val context: Context)
{
private val PREFS_NAME = "com.inspirecoding.androidwidgetsadvanced"
private val PREF_NUMBER = "number_logger"
private val PREF_LIMIT = "number_limit_"
// Write the prefix to the SharedPreferences object for this widget
internal fun saveNumberPref(number: Int)
{
val prefs = context.getSharedPreferences(PREFS_NAME, 0).edit()
prefs.putString(PREF_NUMBER, number.toString())
prefs.apply()
}
// Read the prefix from the SharedPreferences object for this widget.
// If there is no preference saved, get the default from a resource
internal fun getNumberPref(): String
{
val prefs = context.getSharedPreferences(PREFS_NAME, 0)
val number = prefs.getString(PREF_NUMBER, null)
return number ?: context.getString(R.string.no_saved_number)
}
internal fun saveLimitPref(value: Int, widgetId: Int)
{
val prefs = context.getSharedPreferences(PREFS_NAME, 0).edit()
prefs.putInt(PREF_LIMIT + widgetId, value)
prefs.apply()
}
internal fun getLimitPref(widgetId: Int): Int
{
val prefs = context.getSharedPreferences(PREFS_NAME, 0)
return prefs.getInt(PREF_LIMIT + widgetId, 0)
}
}
SharedPreferencesDAO
Notes
-
-
- Member variables
Change the value of the PREFS_NAME to the package name of your app.
The rest variables will be the keys for the save and get methods. - Save methods
These methods gonna save the number and the limits for the instances of the widget.
In case of the limit we need the id of the instance, because we can add as many instances from a widget, as many we want. - Getter methods
Using the keys we can just get back the values.
In case of the number, the getNumberPref() returns a String in case if there is no stored value yet. If no number was saved before, then the method returns the “No saved number” text.
- Member variables
-
Step 7 – The widget's counter
getPendingIntentWidget()
We are going to work in the CounterWidget.kt file, so open it. Then paste at the end of the file file the below getPendingIntentWidget() method.
private fun getPendingIntentWidget(context: Context, value: Operation): PendingIntent
{
// Construct an Intent which is pointing this class.
val intent = Intent(context, AppWidget::class.java)
intent.action = value.name
// And this time we are sending a broadcast with getBroadcast
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
getPendingIntentWidget()
To add a click listener to the widget’s views, we need to create a PendingIntent. Because our Widget is running in an application that is different from ours and runs in another Android process, we have to use PendingIntent. This is the way to ask another app to launch an Intent for us.
The other parameter of this method is the Operation::enum. It contains which button we have clicked, because we are going to catch it in the onReceive() method. That’s why we have to save it to the intent’s action.
Member variables
In the AppWidget::class we are going to define 2 Int variables to store the current number and the limit. In this case we don’t have to load it every time from SharePreferences.
So, paste the below companion object to the beginning of the AppWidget::class.
companion object
{
var number = 0
var limit = 0
}
companion object
Why do we need the companion object? Because we have to reach always the same instance of the limit and number values, and we need them outside of the class as well. Let’s have a clear picture in the next method. 😊
The companion object is a singleton, and its members can be accessed directly via the name of the containing class.
Source: Kotlinlang
updateAppWidget()
Next, go to the updateAppWidget() method and modfiy it to the below lines.
internal fun updateAppWidget(context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int)
{
// 1
val views = RemoteViews(context.packageName, R.layout.app_widget)
// 2
views.setOnClickPendingIntent(R.id.btn_plus,
getPendingIntentWidget(context, Operation.ADDITION))
views.setOnClickPendingIntent(R.id.btn_minus,
getPendingIntentWidget(context, Operation.SUBTRACTION))
// 3
val sharedPreferencesDAO = SharedPreferencesDAO(context)
// 4
AppWidget.limit = sharedPreferencesDAO.getLimitPref(appWidgetId)
// 5
val background = if (AppWidget.number < AppWidget.limit) R.color.blue else R.color.red
views.setInt(R.id.widget_layout, "setBackgroundResource", background)
// 6
appWidgetManager.updateAppWidget(appWidgetId, views)
}
updateAppWidget()
-
-
- Construct the RemoteViews object
- Add the click listeners to the buttons
- Get the limit from SharedPreferences
- Assign it to the limit variable
- Based on the limit value, set the background of the widget’s instance
- Instruct the widget manager to update the widget
-
updateNumber()
To update the widgets when we click on the buttons, and to save the number using SharedPreferences, we need to declare 2 more methods.
The first one will check the clicked button and based on it, it will update the number.
Paste the below method to the end of the AppWidget.kt file.
private fun updateNumber(intentAction: String)
{
if(intentAction == Operation.ADDITION.name)
{
AppWidget.number++
}
else if(intentAction == Operation.SUBTRACTION.name)
{
AppWidget.number--
}
}
updateNumber()
saveNumber()
This method helps us to save the current number. Paste it also to the end of the AppWidget.kt file.
private fun saveNumber(context: Context)
{
val numberLoggerPersistence = SharedPreferencesDAO(context)
numberLoggerPersistence.saveNumberPref(AppWidget.number)
}
saveNumber()
onReceive()
The onReceive() method is called for every broadcast and before all callback methods, like onUpdate(), onEnabled(). It dispatch calls to the various other methods on AppWidgetProvider.
It means in our case, that we are going to update and save the number then refresh the instances’ of the widget in this method.
Insert the below method into the AppWidget::class.
override fun onReceive(context: Context, intent: Intent)
{
super.onReceive(context, intent)
// 1
val numberLoggerPersistence = SharedPreferencesDAO(context)
if (numberLoggerPersistence.getNumberPref() != context.getString(R.string.no_saved_number))
{
number = numberLoggerPersistence.getNumberPref().toInt()
}
// 2
intent.action?.let { updateNumber(intentAction = it) }
// 3
saveNumber(context)
// 4
val views = RemoteViews(context.packageName, R.layout.app_widget)
views.setTextViewText(R.id.appwidget_text, "$number")
// 5
val appWidgetManager = AppWidgetManager.getInstance(context)
val man = AppWidgetManager.getInstance(context)
val ids = man.getAppWidgetIds(ComponentName(context, AppWidget::class.java))
for (appWidgetId in ids)
{
updateAppWidget(context, appWidgetManager, appWidgetId)
}
// 6
val appWidget = ComponentName(context, AppWidget::class.java)
appWidgetManager.updateAppWidget(appWidget, views)
}
onReceive()
-
-
- Get the saved number from the store
- Handle the click event and update the number
- Save the new number into the store
- Update the TextView of all instances of the widget
- Need is to update all instances of the widget one-by-one, otherwise the configurations of the instances will be ignored
- Instruct the widget manager to update the widget
-
onDeleted()
Before the first run delete the onDelete() method from the AppWidget::class.
Run the app
Finally, it is time to run the app.
Add the widget to the Home screen. It has the default preview image. Then, click on the plus-minus buttons. The background going to be changed when you are above or below 0.
If you add one more instance to the Home screen, then it is going to be changed also when you tap on the buttons.
Next, we are going to prepare the configuration screen to have different limits for the different instances of the widget.
Step 8 – The AppWidgetConfigureActivity
In this step we are going to implement the configuration screen for the widget. This screen will be shown when we add a new instance of the widget… as you could see during the first run.
So, open up the AppWidgetConfigureActivity.kt file from the main source set.
First of all, delete the unnecessary variables and methods, which are outside of the AppWidgetConfigureActivity::class.
Next, remove the instance of appWidgetText, which is an EditText. We won’t set its value anymore. Thenafter remove all of its calles, which are now in red.
Thenafter add the below instance of the SharedPreferencesDAO::class to the beginning of the Activity.
private val numberLoggerPersistence = SharedPreferencesDAO(this)
Then, we have to save the limit number from the EditText. So paste the below line into the beginning of the onClickListener variable.
val limit = appwidget_text.text.toString().toInt()
numberLoggerPersistence.saveLimitPref(limit, appWidgetId)
Save the limit value
Step 9 – Open the app
So, let’s continue this tutorial with some new features. In this step we are going to implement how we can open the app from the widget.
As you saw, the layout of the widget contains a TextView with the text: “Open app”. When you tap on it, then this should open the MainActivity::class.
We have already added 2 click listeners for the plus-minus buttons. In the same place we gonna add the a click listener for the TextView as well. So, open up the AppWidget::class and navigate to the updateAppWidget() method and add the below line after the click listeners of the plus-minus buttons.
views.setOnClickPendingIntent(
R.id.tv_openApp, getPendingIntentActivity(context)
)
setOnClickPendingIntent
Now you should have the getPendingIntentActivity() in red color. It is because we haven’t created this method yet.
This method will create a PendingIntent for the start of the Activity. So, copy and paste the below method after the AppWidget::class.
private fun getPendingIntentActivity(context: Context): PendingIntent
{
// Construct an Intent which is pointing this class.
val intent = Intent(context, MainActivity::class.java)
// And this time we are sending a broadcast with getBroadcast
return PendingIntent.getActivity(context, 0, intent, 0)
}
getPendingIntentActivity()
Run the app
Run again the app an test that the clicking on the TextView works as expected.
There is now a problem. The widget have to pass the current number ot the MainActivity::class. Don’t worry, we are going to fix this in the next step. 😊
Step 10 – Get and set the number in the activity
So, our task now is to update the user interface of the MainActivity to see there the current number as well and handle there the click on the plus-minus buttons as well.
We won’t take it from the widget. Remember, that we save the number every time, when we tap on the plus-minus buttons?
In the MainActivity::class we are going to get the data from SharedPreferences in the onCreate() and onResume() methods. This gonna solve our problem. 😎
Member variables
First add the below 2 member variables to the beginning of the MainActivity::class.
var number = 0
private val numberLoggerPersistence = NumberLoggerPersistence(this)
Member variables
setNumber()
Thenafter add the below method after the onCreate() method. which will get the number from SharedPreferences and set the TextView of the number.
It will set the number integer only, when there is a saved number.
private fun setNumber()
{
if (numberLoggerPersistence.getNumberPref() != getString(R.string.no_saved_number))
{
number = numberLoggerPersistence.getNumberPref().toInt()
}
tv_number.text = numberLoggerPersistence.getNumberPref()
}
setNumber()
Then call this method from the onCreate() and onResume() methods.
override fun onCreate(savedInstanceState: Bundle?)
{
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
setNumber()
}
override fun onResume()
{
super.onResume()
setNumber()
}
call setNumber()
saveNumber()
The next step is to save the number. So, paste the below method after the onCreate() method.
private fun saveNumber()
{
numberLoggerPersistence.saveNumberPref(number)
}
saveNumber()
updateNumber()
In this method we will handle the clicks on the button and set the text of the TextView. Also in this method we will call the saveNumber() method. This method goes also after the onCreate() method.
private fun updateNumber(operation: Operation)
{
if (operation.name == Operation.ADDITION.name)
{
number++
tv_number.text = number.toString()
}
else if (operation.name == Operation.SUBTRACTION.name)
{
number--
tv_number.text = number.toString()
}
saveNumber()
}
updateNumber()
Click listener
Finally in this step add the below click listeners to the onCreate() method.
btn_plus.setOnClickListener {
updateNumber(Operation.ADDITION)
}
btn_minus.setOnClickListener {
updateNumber(Operation.SUBTRACTION)
}
setOnClickListener ()
Run the app
Run again the app an test the TextView on widget. Tap on it and the app should open and the TextView for the number on the MainActivity will be also updated.
Next test the plus-minus buttons on the MainActivity as well. It should work also.
Now, if you send the app to the background or just close it, then you can check that the instances of the widget aren’t refreshed. There you can see still the previous value. But, if you you tap on the buttons, then it will change the real number and update with the correct data the number’s TextView.
We gonna solve this issue in the next step using service.
Step 10 – Update widget with service
So, the last step of this tutorial is to update the widget’s instances from the app. We can do it with a service.
A Service is an application component that can perform long-running operations in the background. It does not provide a user interface. Once started, a service might continue running for some time, even after the user switches to another application. Additionally, a component can bind to a service to interact with it and even perform interprocess communication (IPC).
Source: developer.android.com
When we click on the buttons on the MainActivity, then it will send a broadcast. The service will catch this broadcast and it will call the updateAppWidget() method in the AppWidget.kt file. This service will be start in the onUpdate() of the AppWidget::class…. So, let’s finish this tutorial. 😎
Create the service
First, we are going to create a service. So, click with the right mouse button on the main source set, then select New and go to Service, then select again Service. In the popup window name it as “WidgetUpdaterService”.
After the creation, the WidgetUpdaterService::class overrides the onBind() method. We won’t use it, so change it to return null. It means, it has to return a nullable type: IBinder?
override fun onBind(intent: Intent): IBinder?
{
return null
}
onBind
Override onStartCommand()
Thenafter in the WidgetUpdaterService::class we have to override the onStartCommand() method.
The system invokes this method by calling startService() when another component (such as an activity) requests that the service be started. When this method executes, the service is started and can run in the background indefinitely.
Source: developer.android.com
In this method first we have to find the ids of the widget’s instances, because we have to update them one-by-one using a for-cycle. So, the onStartCommand() method look like below.
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int
{
val appWidgetManager = AppWidgetManager.getInstance(this)
val allWidgetIds = intent?.getIntArrayExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS)
if(allWidgetIds != null)
{
for (appWidgetId in allWidgetIds)
{
updateAppWidget(this, appWidgetManager, appWidgetId)
}
}
return super.onStartCommand(intent, flags, startId)
}
onStartCommand()
Start the service
The next step is to start the WidgetUpdaterService, what we are going to do in the onUpdate() method of the AppWidget::class.
Currently there we have to same for-cycle, what we have added to the service’s onStartCommand() method. We will replace it with the below lines.
// Start the service
val intent = Intent(context.applicationContext, WidgetUpdaterService::class.java)
intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, appWidgetIds)
context.startService(intent)
Start the service
To start a service first we need an Intent, which will take over the ids of the widget’s instances. Then by calling the startService() method on the context we can start the service.
Send Broadcast
So, the last step is to send a broadcast which will tell for the Android system to update the widget. The broadcast message itself is wrapped in an Intent object whose action string identifies the event that occurred (for example ACTION_APPWIDGET_UPDATE).
So, open up the MainActivity.kt file from the main source set, and paste the below method to the end of the MainActivity::class.
fun refreshTodayLabel()
{
// 1
val man = AppWidgetManager.getInstance(this)
// 2
val ids = man.getAppWidgetIds(ComponentName(this, AppWidget::class.java))
// 3
val updateIntent = Intent(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
// 4
updateIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids)
// 5
sendBroadcast(updateIntent)
}
refreshTodayLabel()
-
-
- Creates an instance for the AppWidgetManager.
- Then, we will get the ids of the widget.
- The next Intent will update the app widget.
- Using the putExtra() method we can take over the ids of the widget’s instances.
- Finally we can send the broadcast.
-
Call this method from the updateNumber() method, after saving the number to SharedPreferences.
Run the app
For the last time, run again the app and test it.
Source Code
The source code is available under the below link, check it out and download it.
Questions
I hope the description was understandable and clear. But, if you have still questions, then leave me comments below! 😉
Have a nice a day! 🙂