Reading Time: 8 minutes
In this Android widgets tutorial we are going to learn how we can update the Widget with Kotlin Flow, Room and Hilt. As an example we are going to extend a ToDo application (which already uses Room) by Kotlin Flow and Hilt to update the data of the Widget with the first item of our Todo list.
Basics tutorial
This tutorial assumes that youβre familiar with the basics of Android widgets. If you aren’t then please review our free Android widgets – Basics tutorial.
Advanced tutorial
If you think, that the basic tutorial wasn’t enough, then check out our advanced tutorial as well. In this tutorial you will learn
how you can make a
-
-
- configuration screen,
- update the widget
- by itself and
- from the app using Service.
-
You can find the advanced tutorial under the link: Android widgets – Advanced
The sample app
In this tutorial we are going to extend the example app of the Room basics tutorial. This tutorial teach you the basics of Room. How you can implement it and how does it work.
You can find the Room basics tutorial here: Android Room basics
GitHub
If you don’t want to do the tutorial, then you can download the starter app for this tutorial from GitHub using the below link:
Be careful to download the viewmodel branch.
If you download the app, then in Android Studio open it by clicking on the
File -> New -> Import Project
In the popup window find the project’s folder or the build.gradle file.Β
If the app is open, then check out the source files to get familiar with it, if you haven’t done the Room basics tutorial.
Step 1 β Add a dependency
This project already contains the needed dependencies for Kotlin Flow and Room, but not for Dagger Hilt.
Hilt is a dependency injection library for Android that reduces the boilerplate of doing manual dependency injection in your project.
Hilt provides a standard way to use DI in your application by providing containers for every Android class in your project and managing their lifecycles automatically. Hilt is built on top of the popular DI library Dagger to benefit from the compile-time correctness, runtime performance, scalability, and Android Studio support that Dagger provides.
Source: Dependency injection with Hilt
Next, paste the below implementation into the dependencies {} section.
implementation “com.google.dagger:hilt-android:$hilt_version“
kapt “com.google.dagger:hilt-android-compiler:$hilt_version“
kapt “androidx.hilt:hilt-compiler:$hilt_androidx_version“
Then, add the below line to the top of App Build.gradle file.
apply plugin: ‘kotlin-kapt’
Thenafter we have to add one more line to the app, but in this case to the Project Build.gradle file, what you can find also in the Gradle Scripts.
Paste the below implementation into the dependencies {} section.
classpath “com.google.dagger:hilt-android-gradle-plugin:$hilt_version“
The last step is to add the versions for the dependencies. So paste the below line directly to the top of the buildscript{} section.
ext.hilt_androidx_version = “1.0.0-alpha02”
ext.hilt_version = “2.28.3-alpha”
Finally, click on the Sync now button, what you can find in Android Studio at the top left corner.
Step 2 β Add the Widget
Our next task will be to create the widget. You can create it manually, or you can add it using Android Studio. In this tutorial we are going to select the second option. In this case Android Studio is going to prepare for us the needed files.
First we gonna create a new package, which will contain the file for the widget. 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 Package option and name it as “widget”.
Then again click with the right mouse button on the widget package, from the quick menu select New, then the Widget option. From the submenu choose the App Widget option.
Name the widget as “AppWidget”.
Our widget gonna be 4 cells wide, and 1 cell high.Β
For this sample app we don’t need to create a Configuration screen, so leave unchecked the CheckBox.
Now, click on the Finish button.
Step 3 β Widget's 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
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/widget_layout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="@drawable/shape_roundedcorners_white">
<TextView
android:id="@+id/tv_widget_title"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:text="Task 1"
android:textColor="@color/colorPrimary"
android:textSize="14sp" />
<TextView
android:id="@+id/tv_widget_dueDate"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/tv_widget_title"
android:layout_marginStart="8dp"
android:text="@string/duedate"
android:textColor="@color/darkGrey"
android:textSize="8sp" />
<TextView
android:id="@+id/tv_widget_description"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/tv_widget_dueDate"
android:layout_alignParentStart="true"
android:layout_toStartOf="@id/iv_widget_priority"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:text="@string/placeholder_text"
android:textColor="@color/darkGrey"
android:textSize="10sp" />
<ImageView
android:id="@+id/iv_widget_priority"
android:layout_width="30dp"
android:layout_height="30dp"
android:layout_alignParentTop="true"
android:layout_alignParentBottom="true"
android:layout_alignParentEnd="true"
android:layout_marginEnd="8dp"
android:src="@drawable/prio_green" />
</RelativeLayout>
app_widget.xml
The shape of the widget
Our widget will get rounded corners and a bit dirty white background. For this we are going to create a new xml file. For this, click with the right mouse button on the drawable folder, then select the Drawable resource file option. In the popup window name the new file as “shape_roundedcorners_white”.
Then replace the xml code with the below one.
<?xml version="1.0" encoding="utf-8"?>
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<solid
android:color="@color/white"/>
<corners
android:radius="12dp"/>
</shape>
Shape of the widget
Step 4 β Implement Hilt
@HiltAndroidApp
In this step we are going to implement Hilt. So for the first step, open the MyApp::class, which is in the root of the main source set. Then annotate the class by @HiltAndroidApp.
@HiltAndroidApp
class MyApp: Application()
{
...
}
@AndroidEntryPoint
The @AndroidEntryPoint annotation triggers Hilt’s code generation, including a base class for our application that serves as the application-level dependency container.
@RoomDatabaseModule
Next, we are going to create the modul for the ToDoRoomDatabase::class and ToDoDao::class into a new package, called “di”. So create it in the main source set as we have done it in case of the widget package.
A Hilt module is a class that is annotated with @Module. It informs Hilt how to provide instances of certain types. We must annotate Hilt modules with @InstallIn to tell Hilt which Android class each module will be used or installed in.
Source: Dependency injection with Hilt
Then create a new Kotlin file in the di package and name it as “RoomDatabaseModule”.
The below code into this new Kotlin file.
@InstallIn(ApplicationComponent::class)
@Module
class RoomDatabaseModule
{
@Singleton
@Provides
fun providesDatabase (application: Application) = ToDoRoomDatabase.getDatabase(application)
@Singleton
@Provides
fun providesCurrentWeatherDao (database: ToDoRoomDatabase) = database.toDoDao()
}
RoomDatabaseModule
-
-
- @Singleton: Defines the instance as an application wide available singleton instance.
- @Provides: Because we don’t direcly own the Room database, we have to annotate with @Provides. In this way we tell Hilt how to provide instances of this type.
-
@AndroidEntryPoint
The next step of the implementation is to annotate the AppWidget::class as well with
@AndroidEntryPoint.
Using the @AndroidEntryPoint annotation Hilt can provide dependencies to other Android classes. In our case we are going to use the dependency of the ToDoDatabaseΒ inside of the AppWidget::class. We are going to talk about this later.
@Inject DAO
The last step is to extend the constructor of the ToDoRepository::class to let Hilt inject the ToDoDAO::class. So, open it from the repository package, and modify the header of the class ot the below one.
class ToDoRepository @Inject constructor (private val toDoDao: ToDoDao)
@Inject constructor
That’s it, the implementation of Hilt is finished. π
Step 5 β Add Flow to DAO
So, finally, Flow. π What is this exactly? You can ask.
Kotlin Flow is a new stream processing API, which is developed by JetBrains, the owner of the Kotlin langauge. Itβs an implementation of the Reactive Stream specification, an initiative whose goal is to provide a standard for asynchronous stream processing. Jetbrains built Kotlin Flow on top of Kotlin Coroutines.
Kotlin Flow helps us to transform data in a complex multithreaded way with few lines of code.
After a short introduction, we will continue the implementation of this sample app. So, open the ToDoDAO::class from the repository package, and add to it the below function.
@Query("SELECT * FROM ToDo WHERE toDoId = 1")
fun getFirstToDoItem() : Flow<ToDo>
getFirstToDoItem()
Check out this method. Using the @Query annotations of the Room library we can define custom SQL query from the database. In our case we will get the todo task which has the toDoId 1.
The return type will be Flow<ToDo>.
Step 6 β Extend the repository
Next, we are going to add a call to the above getFirstToDoItem() method into the ToDoRepository::class. So, open it. You can find it in the repository package.
val getFirstToDoItem : Flow<ToDo> = toDoDao.getFirstToDoItem()
getFirstToDoItem
As you can see, in the case of the repository it is just a single line of code.
Step 7 β Update the AppWidget::class
Finally it’s time to get the data from Room and update the widget using Kotlin Flow.
Member variables
First, we have to create a Job and a CoroutineScope member variable for the AppWidget::class. So open the AppWidget.kt file from the widget package.
Using Job we can control the lifecycle of the coroutine. Because of this, later on, when we remove the instance of the widget, we have to cancel this Job as well.
All coroutines run inside a CoroutineScope and it takes a CoroutinesContext as a parameter. A CoroutinesContext is a set of elements, to define the threading policy, exception handler, control the lifetime of the coroutine, and so on. We can use plus operator to combine the elements of CoroutinesContext. There are 3 important CoroutinesContext:
-
-
- Dispatchers:Defines which thread runs the coroutine.
- CoroutineExceptionHandler: Handles uncaught exceptions.
- Job
-
Dispatchers
-
-
- Dispatchers.Default: This is the standard builder in case if neither the dipatcher nor the ContinuationInterceptor is specified in their context.
- Dispatchers.IO: This dispatcher is optimized to perform disk or network I/O outside of the main thread.
- Dispatchers.Main: If we would like to run a coroutine on the main Android thread, then use this one. This should be used only for interacting with the UI and performing quick work.
- Dispatchers.Unconfined: A coroutine dispatcher that is not confined to any specific thread.
-
private val job = SupervisorJob()
val coroutineScope = CoroutineScope(Dispatchers.IO + job)
Member variables for Flow
The third member variable will be a field injected instance of the ToDoRepository::class. Using the @Inject Hilt annotation for the variable we can obtain dependency from a component.
@Inject lateinit var toDoRepository: ToDoRepository
Member variables for the ToDoRepository
Launch the Flow
Next, we are going to start using the above created member variables. First, we are going to launch the CoroutineScope, thenafter we can start collecting the ToDo item using Kotlin Flow. We are going to do that inside of the onReceive() method of the AppWidget::class.
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.
override fun onReceive(context: Context, intent: Intent?)
{
super.onReceive(context, intent)
coroutineScope.launch {
toDoRepository.getFirstToDoItem.collect { _toDo ->
}
}
}
onReceive()
Using the collect() terminal operator we can start collecting the ToDo item. It means, it will emit the item in every case when it changes. Since collect() is a suspending function, it can only be called from a coroutine or another suspending function. This is why you wrap the code with CoroutineScope.
Below lines go into the body of the collect() terminal operator.
val appWidgetManager = AppWidgetManager.getInstance(context)
val man = AppWidgetManager.getInstance(context)
val ids = man.getAppWidgetIds(ComponentName(context, AppWidget::class.java))
if (_toDo != null)
{
for (appWidgetId in ids)
{
updateAppWidget(
context, appWidgetManager, appWidgetId,
_toDo.title, _toDo.dueDate, _toDo.description, _toDo.priority
)
}
}
Update all instances of the widget
First we have to get all the ids of the widget’s instances, because it can happen, thet the user has created more instances. In our case all of them should have the same data. So, if the _toDo fetched variable is not null, then we will iterate through the ids and update the widget with the updateAppWidget().
Currently there we have some errors, because we haven’t updated the updateAppWidget() method yet.
Remove onUpdate()
For this app we won’t use the onUpdate() method, so simply just remove it from the AppWidget::class.
updateAppWidget()
This method could be familiar if you have done the Advanced Widgets tutorial. In this method we update the views of the widget using the parameters of the method. We won’t go into the details. So, just replace the whole updateAppWidget() method with the below one.
internal fun updateAppWidget(
context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int,
title: String?, dueDate: String?, description: String?, priority: String?)
{
// Construct the RemoteViews object
val views = RemoteViews(context.packageName, R.layout.app_widget)
if (title != null) {
views.setTextViewText(R.id.tv_widget_title, title)
} else {
views.setTextViewText(R.id.tv_widget_title, "")
}
if (dueDate != null) {
views.setTextViewText(R.id.tv_widget_dueDate, dueDate)
} else {
views.setTextViewText(R.id.tv_widget_dueDate, "")
}
if (description != null) {
views.setTextViewText(R.id.tv_widget_description, description)
} else {
views.setTextViewText(R.id.tv_widget_title, "")
}
if (description != null) {
when (priority)
{
Prioirities.LOW.name -> views.setImageViewResource(R.id.iv_widget_priority, R.drawable.prio_green)
Prioirities.MEDIUM.name -> views.setImageViewResource(R.id.iv_widget_priority, R.drawable.prio_orange)
Prioirities.HIGH.name -> views.setImageViewResource(R.id.iv_widget_priority, R.drawable.prio_red)
}
}
views.setOnClickPendingIntent(R.id.widget_layout,
getPendingIntentActivity(context))
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
onReceive()
getPendingIntentActivity()
Now we have an error, because we haven’t provided the getPendingIntentActivity() method yet. This method will create a PendingIntent.
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()
Cancel the Job
The last thing before we run the app is to cancel the Job, when we remove the widget’s instance. So, add the below line to the onDisabled() method.
job.cancel()
Run the app
Finally, run the app. It should work as you can see in the below short video.
In our example I have extended the app with a simple preview image, what you can find in the GitHub repository.
GitHub
The source code is available on GitHub, check it out and download it using the below link.
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! π