Android widgets – Update using Kotlin Flow, Room and Dagger-Hilt

inspire_coding

Inspire Coding

Posted on September 5, 2020

Android widgets – Update using Kotlin Flow, Room and Dagger-Hilt

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

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>
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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()
}
Enter fullscreen mode Exit fullscreen mode

@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)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
                    )
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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)
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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:
Inspire Coding

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! 🙂

💖 💪 🙅 🚩
inspire_coding
Inspire Coding

Posted on September 5, 2020

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related