Overview
In this Android tutorial I'm going to show you an approach which helps to update the app's widget realy easy using Kotlin Flow, Room and Dagger-Hilt.
Introduction
First of all, we have to talked in a few words about what does Koltin Flow, Room and Dagger-Hilt mean.
Kotlin Flow
Kotlin Flow is developed by JetBrains, the owner of the Kotlin language and it is a new stream processing API. It implements the Reactive Stream specification, and its goal is ot provide a standard for asynchronous stream processing. Kotlin Flow is build on top of Kotlin Coroutines.
Using Flow we handle streams of values, transform the data in a complex threaded way with only few lines of code.
Room
The Room persistence library provides an abstraction layer over SQLite to allow for more robust database access while harnessing the full power of SQLite.
The library helps you create a cache of your app's data on a device that's running your app. This cache, which serves as your app's single source of truth, allows users to view a consistent copy of key information within your app, regardless of whether users have an internet connection.
Source: Room Persistence Library
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
The sample app
So, after a short indtroduction, let's start coding. :)
Step 1 - Get the starter project
For this tutorial we are going to use a starter project, which is a ToDo app. This app contains Room and RecyclerView to show the created ToDo items.
The tutorial for the starter project is available on Inspire Coding: Room basics – Introduction
GitHub
If you don't want to do the starter tutorial, then you can get the starter project from GitHub as well: GitHub
Clone or just download the project, and import it in Android Studio.
Step 2 - Add the widget
The next step is to add the "widget" package to the main source set.
If the package is created, then create into it a new widget with the below detailes
- Width: 4 cells
- Height: 1 cell
- No Configuration Screen needed
- Language: Kotlin
Step 3 - Widget's layout
The layout gonna be very simple.
- TextView for the title
- TextView for the due date
- TextView for the description
- ImageView for the priority
< 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>
The shape of the widget
Now create a new Drawable resource file with the name: shape_roundedcorners_white.xml and paste into it the below code.
<?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>
This will be the background of the widget.
Step 4 - Implement Hilt
@HiltAndroidApp
Add the @HiltAndroidApp annotation to the MyApp::class
@RoomDatabaseModule
Then create a Hilt module into a new package called "di". This module will get the name of RoomDatabaseModule.
And the file looks like below.
@InstallIn(ApplicationComponent::class)
@Module
class RoomDatabaseModule
{
@Singleton
@Provides
fun providesDatabase (application: Application) = ToDoRoomDatabase.getDatabase(application)
@Singleton
@Provides
fun providesCurrentWeatherDao (database: ToDoRoomDatabase) = database.toDoDao()
}
@AndroidEntryPoint
Thenafter add the @AndroidEntryPoint annotation to the AppWidget::class.
@Inject DAO
Finally modify the ToDoRepository::class to use a constructor injection in the header of the class for the ToDoDAO.
class ToDoRepository @Inject constructor (private val toDoDao: ToDoDao)
Step 5 – Add Flow to DAO
In this step we are going to implement the needed methods to get the todo item from Room which has the todoId of 1.
For this we are going to add the below method to the ToDoDAO::class.
@Query("SELECT * FROM ToDo WHERE toDoId = 1")
fun getFirstToDoItem() : Flow<ToDo>
Note that the method return Flow which wraps a ToDo type.
Step 6 – Extend the repository
Then add the below method to the ToDoRepository::class.
val getFirstToDoItem : Flow<ToDo> = toDoDao.getFirstToDoItem()
The return value is the same like the same method in the ToDoDAO::class.
Step 7 – Update the AppWidget::class
Thenafter we will add 3 member variables to the AppWidget::class.
private val job = SupervisorJob()
val coroutineScope = CoroutineScope(Dispatchers.IO + job)
@Inject lateinit var toDoRepository: ToDoRepository
Because Flow is a suspend function, we can call it only from CoroutineScope. That's why we need the Job and the CoroutineScope.
To get the item from Room is an IO operation, so we are going to add the Dispatchers.IO to the CoroutineScope.
Next, remove the onUpdate() and the onEnabled() methods. We don't need them.
Thenafter add the onReceive() method to the AppWidget::class.
override fun onReceive(context: Context, intent: Intent?)
{
super.onReceive(context, intent)
coroutineScope.launch {
toDoRepository.getFirstToDoItem.collect { _toDo ->
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
)
}
}
}
}
}
Now you should have an error, because we haven't extended the updateAppWidget() method. So, replace it 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)
}
And one more error. It is there, because we haven't implemented the getPendingIntentActivity() method yet. This method will create for the views a PendingIntent to update the views if the widget's instances.
So, paste the below method at the end of the AppWidget.kt file.
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)
}
So, we are almost done. One more thing left before we can test the app. When we delete an instance of the AppWidget, we have to cancel its Job as well. So add the below line to the onDisabled() method.
job.cancel()
Run the app
Finally its time to run the app.
More Android tutorials
If you would like to do more Android tutorials like this, then visit my website:
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! 🙂
Top comments (1)
Field inject not working
@Inject lateinit var toDoRepository: ToDoRepository