I'm trying to add an AppWidget to my Android app, built with Kotlin, Jetpack Compose, Room and Dagger-Hilt.
The general idea is for the widget to display the quote of the day, by picking it from the quoteRepository
, which returns a Flow<QuoteOfTheDay>
from the Room database. In production, the widget should update once a day, ideally at or around midnight in order to reflect the day change and the new quote.
There are two problems I need help with:
- When first added to the home screen, the widget picks the correct quote. However, when the app is relaunched in Android Studio, the widget is reset to the default XML layout with dummy data. I would prefer if the current quote persists across app updates and relaunches.
- I've done everything in my limited powers to replicate the behaviour described in the official Android widget guide related to layout. Nevertheless, I cannot get the widget to pick the medium-sized layout upon resizing - instead, it just rearranges the small layout.
The widget is using the standard XML layout and this is the provider:
@AndroidEntryPoint
class AppWidget : AppWidgetProvider() {
private val job = SupervisorJob()
private val coroutineScope = CoroutineScope(Dispatchers.Default + job)
@Inject lateinit var repository: quoteRepository
private val today = LocalDate.now()
private val dateFormatter: DateTimeFormatter = DateTimeFormatter.ofPattern("MMMM")
private val supportedSizes = listOf(
SizeFCompat(160.0f, 110.0f),
SizeFCompat(200.0f, 110.0f)
)
override fun onReceive(context: Context, intent: Intent?) {
super.onReceive(context, intent)
var layoutId = 0
coroutineScope.launch {
repository.getDailyquote(today).collect { _quote ->
val appWidgetManager = AppWidgetManager.getInstance(context)
val ids = appWidgetManager.getAppWidgetIds(ComponentName(context, AppWidget::class.java))
for (appWidgetId in ids) {
appWidgetManager.updateAppWidget(appWidgetId, supportedSizes) {
layoutId = when (it) {
supportedSizes[0] -> R.layout.quote_widget_small
else -> R.layout.quote_widget_medium
}
RemoteViews(context.packageName, layoutId)
}
updateWidget(
context, appWidgetManager, appWidgetId, layoutId,
_quote.quote.title,
today.dayOfMonth.toString(),
today.format(dateFormatter)
)
}
}
}
}
override fun onUpdate(
context: Context,
appWidgetManager: AppWidgetManager,
appWidgetIds: IntArray
) {
var layoutId = 0
coroutineScope.launch {
for (id in appWidgetIds) {
appWidgetManager.updateAppWidget(id, supportedSizes) {
layoutId = when (it) {
supportedSizes[0] -> R.layout.quote_widget_small
else -> R.layout.quote_widget_medium
}
RemoteViews(context.packageName, layoutId)
}
repository.getDailyquote(today).collect { a ->
updateWidget(
context,
appWidgetManager,
id,
layoutId,
a.quote.title,
today.dayOfMonth.toString(),
today.format(dateFormatter)
)
}
}
}
scheduleUpdates(context)
}
override fun onDisabled(context: Context) {
job.cancel()
}
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
// reschedule update alarm so it does not include ID of currently removed widget
scheduleUpdates(context)
}
private fun getActiveWidgetIds(context: Context): IntArray {
val appWidgetManager = AppWidgetManager.getInstance(context)
val componentName = ComponentName(context, this::class.java)
// return ID of all active widgets within this AppWidgetProvider
return appWidgetManager.getAppWidgetIds(componentName)
}
private fun scheduleUpdates(context: Context) {
val activeWidgetIds = getActiveWidgetIds(context)
if (activeWidgetIds.isNotEmpty()) {
val nextUpdate = ZonedDateTime.now() + WIDGET_UPDATE_INTERVAL
val pendingIntent = getUpdatePendingIntent(context)
context.alarmManager.set(
AlarmManager.RTC_WAKEUP,
nextUpdate.toInstant().toEpochMilli(), // alarm time in millis since 1970-01-01 UTC
pendingIntent
)
}
}
private fun getUpdatePendingIntent(context: Context): PendingIntent {
val widgetClass = this::class.java
val widgetIds = getActiveWidgetIds(context)
val updateIntent = Intent(context, widgetClass)
.setAction(AppWidgetManager.ACTION_APPWIDGET_UPDATE)
.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIds)
val requestCode = widgetClass.name.hashCode()
val flags = PendingIntent.FLAG_CANCEL_CURRENT or
PendingIntent.FLAG_IMMUTABLE
return PendingIntent.getBroadcast(context, requestCode, updateIntent, flags)
}
private val Context.alarmManager: AlarmManager
get() = getSystemService(Context.ALARM_SERVICE) as AlarmManager
companion object {
private val WIDGET_UPDATE_INTERVAL = Duration.ofMinutes(2)
}
private fun updateWidget(
context: Context, appWidgetManager: AppWidgetManager, appWidgetId: Int, layoutId: Int,
title: String?, day: String?, month: String?
) {
// Construct the RemoteViews object
val views = RemoteViews(context.packageName, layoutId)
if (title != null) {
views.setTextViewText(R.id.quote_text, title)
} else {
views.setTextViewText(R.id.quote_text, "")
}
if (day != null) {
views.setTextViewText(R.id.date_text, day)
} else {
views.setTextViewText(R.id.date_text, "")
}
if (month != null) {
views.setTextViewText(R.id.month_text, month.lowercase())
} else {
views.setTextViewText(R.id.month_text, "")
}
views.setOnClickPendingIntent(R.id.widget_layout,
getPendingIntentActivity(context))
// Instruct the widget manager to update the widget
appWidgetManager.updateAppWidget(appWidgetId, views)
}
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, PendingIntent.FLAG_IMMUTABLE)
}
}