androidkotlinandroid-roomkotlin-coroutinesappwidgetprovider

How to interact with db from widget in MVVM pattern


I have an application which fetches the data from api and caches in db. I want to be able to show this data (from db) in the widget and also add a button to update the data, which when pressed will fetch the data and update the db and hence update the widget. The problem is since I'm using mvvm, Im not sure if at all it is possible to make use of livedata and fancy supports of jetpack components.

So I just created a Dao reference in widget update fun and fetched the db. That does fetch the data but it is not updating the textview, i logged it and it showed me the data correctly.

companion object {

        internal fun updateAppWidget(
            context: Context, appWidgetManager: AppWidgetManager,
            appWidgetId: Int
        ) {
            val db = AppDatabase.getInstance(context.applicationContext).weatherDao()
            val views = RemoteViews(context.packageName, R.layout.weather_widget)
            GlobalScope.launch {
                val weather = db.getCurrentWeatherMetricAsync()
                Log.d("TAG_TAG_TAG", "weather: " + weather.temperature);
                withContext(Dispatchers.Main) {
                    views.setTextViewText(R.id.tv_counter, " asd " + weather.temperature)
                }
            }
            appWidgetManager.updateAppWidget(appWidgetId, views)
        }
    }

the dao has a suspended fun for this.

    @Query("select * from current_weather where id = 0")
    suspend fun getCurrentWeatherMetricAsync(): CurrentMetric

Can you please tell me how to interact with the db and the proper means to interact with the repository of application?


Solution

  • Since You're using suspended functions for Room, You can use it in CoroutineScope with Dispatchers.Main (https://medium.com/androiddevelopers/room-coroutines-422b786dc4c5, https://medium.com/androiddevelopers/coroutines-on-android-part-i-getting-the-background-3e0e54d20bb).

    CoroutineScope(Dispatchers.Main.immediate).launch {
         val weather = AppDatabase.getInstance(context.applicationContext).weatherDao().getCurrentWeatherMetricAsync()
         val views = RemoteViews(context.packageName, R.layout.weather_widget)
         views.setTextViewText(R.id.tv_counter, " asd " + weather.temperature)
         appWidgetManager.updateAppWidget(appWidgetId, views)
    }
    

    In case You don't use suspend functions or even Room, You can use Dispatchers.IO to load data async and update RemoteViews in the same thread, without withContext(Dispatchers.Main) because RemoteViews is not 'ours', we just telling launcher that he should set this data on that view. This is by default done in another, no-UI thread - no even in thread created/owned by app.

    This is the only way I found, because since AppWidgetProvider is BroadcastReceiver, You cannot use Architecture Components in it (https://stackoverflow.com/a/47292260/4265500). Just force update widget whenever You update DB (in JobIntentService, Worker (WorkManager), Coroutines, wheatever) and get the latest record in updateAppWidget() (like You're doing right now).

    Here's handy extension function to force widget update:

    fun Context.updateWidget() {
        val widgetUpdateIntent = Intent(this, Widget::class.java).apply {
            action = AppWidgetManager.ACTION_APPWIDGET_UPDATE
            putExtra(
                AppWidgetManager.EXTRA_APPWIDGET_IDS,
                AppWidgetManager.getInstance(this@updateWidget).getAppWidgetIds(
                    ComponentName(
                        this@updateWidget,
                        Widget::class.java
                    )
                )
            )
        }
        sendBroadcast(widgetUpdateIntent)
    }