Skip to content

feat: "Deal of the Day" Home Screen Widget using Jetpack Glanceย #15

@MostafaMohamed2002

Description

@MostafaMohamed2002

๐Ÿงฉ Feature Request: Home Screen Widget โ€” "Deal of the Day"

Summary

Add a home screen App Widget built with Jetpack Glance that surfaces today's top free game or best deal directly on the Android launcher โ€” no app open required. This dramatically increases daily engagement and puts GameVault's core value on the user's home screen 24/7.


User Story

As a gamer, I want a home screen widget that shows me today's best free game or top deal at a glance, so I never miss a time-sensitive offer even when I'm not actively using the app.


Motivation

GameVault's core value is time-sensitive discovery โ€” free games and giveaways expire. A widget solves the fundamental problem that users must remember to open the app. Competing deal apps (IsThereAnyDeal, Epic Games) invest heavily in widgets precisely because they are the highest-retention surface on Android.

  • ๐Ÿ“ˆ Increases daily active opens (widget tap โ†’ app deep-link)
  • โฐ Surfaces urgency passively ("Expires in 4 hours" visible on home screen)
  • ๐ŸŽฏ Differentiates GameVault in Play Store listings (widget support shown in screenshots)
  • ๐Ÿ”— Works in tandem with the planned push notification feature (feat: Integrate Firebase Analytics for User Behavior Trackingย #13) for a complete passive-engagement system

Widget Variants

Variant A โ€” Small (2ร—2): Free Game Spotlight

Shows the current top free game with thumbnail, title, and a "Claim" button.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ ๐ŸŽฎ FREE TODAY        โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”               โ”‚
โ”‚ โ”‚imgโ”‚ Game Title    โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”˜ PC โ€ข Steam    โ”‚
โ”‚      [ Claim Now ]  โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Variant B โ€” Medium (4ร—2): Deal of the Day

Shows the best deal with original price, sale price, savings badge, and store name.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ ๐Ÿท๏ธ BEST DEAL TODAY          GameVault โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”  Game Title                 โ”‚
โ”‚ โ”‚ img  โ”‚  ~~$59.99~~  $14.99  -75%   โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  Steam        [ View Deal ] โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Variant C โ€” Medium (4ร—2): Expiring Giveaway

Shows a giveaway with a live countdown timer emphasizing urgency.

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ ๐ŸŽ GIVEAWAY EXPIRING SOON   GameVault โ”‚
โ”‚ โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”  Game Title                 โ”‚
โ”‚ โ”‚ img  โ”‚  Expires in: 05:42:10       โ”‚
โ”‚ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”˜  Epic Games  [ Claim Free ] โ”‚
โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜

Technical Design

1. Dependencies

// app/build.gradle.kts
implementation("androidx.glance:glance-appwidget:1.1.x")
implementation("androidx.glance:glance-material3:1.1.x")

2. Widget Receiver & Provider

// widget/GameVaultWidget.kt
@GlanceAppWidget
class GameVaultWidget : GlanceAppWidget() {

    override suspend fun provideGlance(context: Context, id: GlanceId) {
        val widgetData = GameVaultWidgetRepository.getWidgetData(context)
        provideContent {
            GameVaultWidgetContent(widgetData)
        }
    }
}

// widget/GameVaultWidgetReceiver.kt
class GameVaultWidgetReceiver : GlanceAppWidgetReceiver() {
    override val glanceAppWidget = GameVaultWidget()
}

3. AndroidManifest.xml Registration

<receiver
    android:name=".widget.GameVaultWidgetReceiver"
    android:exported="true"
    android:label="@string/widget_label">
    <intent-filter>
        <action android:name="android.appwidget.action.APPWIDGET_UPDATE" />
    </intent-filter>
    <meta-data
        android:name="android.appwidget.provider"
        android:resource="@xml/game_vault_widget_info" />
</receiver>

4. Widget Info XML

<!-- res/xml/game_vault_widget_info.xml -->
<appwidget-provider
    xmlns:android="http://schemas.android.com/apk/res/android"
    android:minWidth="110dp"
    android:minHeight="110dp"
    android:targetCellWidth="2"
    android:targetCellHeight="2"
    android:maxResizeWidth="250dp"
    android:maxResizeHeight="110dp"
    android:resizeMode="horizontal|vertical"
    android:updatePeriodMillis="0"
    android:previewLayout="@layout/widget_preview"
    android:initialLayout="@layout/widget_loading"
    android:widgetCategory="home_screen"
    android:description="@string/widget_description" />

updatePeriodMillis="0" โ€” updates are driven by WorkManager, not the system timer (avoids battery drain).

5. Data Flow

Widget data is sourced from Room (already integrated) โ€” the widget never makes direct network calls.

WorkManager (periodic, every 6h)
    โ””โ”€โ–ถ Fetches latest free game / best deal from API
    โ””โ”€โ–ถ Writes to Room cache
    โ””โ”€โ–ถ Calls GlanceAppWidgetManager.update() to refresh widget
// worker/WidgetUpdateWorker.kt
class WidgetUpdateWorker(
    context: Context,
    workerParams: WorkerParameters
) : CoroutineWorker(context, workerParams) {

    @Inject lateinit var freeGamesRepository: FreeGamesRepository

    override suspend fun doWork(): Result {
        return try {
            // 1. Fetch & cache via existing repository
            freeGamesRepository.refreshGames()
            // 2. Trigger widget redraw
            GameVaultWidget().updateAll(applicationContext)
            Result.success()
        } catch (e: Exception) {
            Timber.e(e, "Widget update failed")
            Result.retry()
        }
    }
}

6. Deep-Link on Widget Tap

Tapping the widget opens the app directly to the game detail screen:

@Composable
fun GameVaultWidgetContent(data: WidgetData) {
    val claimIntent = actionStartActivity<MainActivity>(
        actionParametersOf(
            ActionParameters.Key<String>("destination") to "detail/${data.gameId}"
        )
    )
    Button(
        text = "Claim Now",
        onClick = claimIntent
    )
}

Handle the deep-link in MainActivity:

// MainActivity.kt
val destination = intent.getStringExtra("destination")
if (destination != null) {
    navController.navigate(destination)
}

Widget Data Repository

A lightweight WidgetDataRepository reads from Room to avoid duplicating network logic:

// data/repository/WidgetDataRepository.kt
class WidgetDataRepository @Inject constructor(
    private val freeGamesDao: FreeGamesDao,
    private val dealsDao: DealsDao
) {
    suspend fun getTopFreeGame(): Game? = freeGamesDao.getAll().firstOrNull()
    suspend fun getBestDeal(): Deal? = dealsDao.getAll().maxByOrNull { it.savings }
    suspend fun getExpiringGiveaway(): Giveaway? =
        dealsDao.getGiveaways().minByOrNull { it.endDate }
}

WorkManager Scheduling

// Scheduled once on app first launch in MyApp.kt or MainActivity
val widgetSyncRequest = PeriodicWorkRequestBuilder<WidgetUpdateWorker>(
    repeatInterval = 6,
    repeatIntervalTimeUnit = TimeUnit.HOURS
)
    .setConstraints(
        Constraints.Builder()
            .setRequiredNetworkType(NetworkType.CONNECTED)
            .build()
    )
    .build()

WorkManager.getInstance(context).enqueueUniquePeriodicWork(
    "widget_sync",
    ExistingPeriodicWorkPolicy.KEEP,
    widgetSyncRequest
)

New Files to Create

app/src/main/java/com/mostafadevo/freegames/
โ”œโ”€โ”€ widget/
โ”‚   โ”œโ”€โ”€ GameVaultWidget.kt           โ† GlanceAppWidget subclass
โ”‚   โ”œโ”€โ”€ GameVaultWidgetReceiver.kt   โ† GlanceAppWidgetReceiver
โ”‚   โ”œโ”€โ”€ GameVaultWidgetContent.kt    โ† Composable widget UI
โ”‚   โ””โ”€โ”€ WidgetData.kt               โ† Simple data holder for widget state
โ”œโ”€โ”€ worker/
โ”‚   โ””โ”€โ”€ WidgetUpdateWorker.kt       โ† CoroutineWorker for background sync
โ””โ”€โ”€ data/repository/
    โ””โ”€โ”€ WidgetDataRepository.kt     โ† Reads cached data from Room for widget

app/src/main/res/xml/
โ””โ”€โ”€ game_vault_widget_info.xml      โ† AppWidgetProviderInfo

app/src/main/res/drawable/
โ””โ”€โ”€ widget_preview.xml              โ† Widget picker preview

Files to Modify

File Change
AndroidManifest.xml Add <receiver> for GameVaultWidgetReceiver
app/build.gradle.kts Add Glance + WorkManager dependencies
di/AppModule.kt (or new WidgetModule.kt) Bind WidgetDataRepository, inject into Worker via HiltWorkerFactory
MyApp.kt Schedule WidgetUpdateWorker on startup
ui/MainActivity.kt Handle deep-link Intent extras from widget tap
gradle/libs.versions.toml Add glance, glance-material3, work-runtime-ktx versions

Hilt + WorkManager Integration

Hilt injection in Workers requires HiltWorkerFactory:

// di/WorkerModule.kt
@InstallIn(SingletonComponent::class)
@Module
object WorkerModule {
    @Provides
    fun provideWorkManagerConfiguration(
        workerFactory: HiltWorkerFactory
    ): Configuration = Configuration.Builder()
        .setWorkerFactory(workerFactory)
        .build()
}

Add to AndroidManifest.xml:

<provider
    android:name="androidx.startup.InitializationProvider"
    android:authorities="${applicationId}.androidx-startup"
    tools:node="remove" />

Definition of Done

  • GameVaultWidget renders correctly on Android 12+ launcher (Pixel & Samsung tested)
  • Widget displays top free game with thumbnail, title, and platform
  • "Claim Now" / "View Deal" tap opens the correct game detail screen via deep-link
  • Widget data refreshes every 6 hours via WorkManager (not updatePeriodMillis)
  • Widget shows a loading skeleton on first install before data is available
  • Widget shows a graceful fallback if Room cache is empty ("Open GameVault to load deals")
  • Widget preview image added for the widget picker (@xml/game_vault_widget_info)
  • All three variants (Small, Medium-Deal, Medium-Giveaway) implemented
  • Countdown timer on Variant C updates every minute
  • Hilt injection works correctly in WidgetUpdateWorker
  • WidgetDataRepository unit tested with in-memory Room
  • Play Store screenshots updated to show the widget

Effort Estimate

High โ€” 2โ€“4 weeks

Glance has its own Composable scope (different from regular Compose), WorkManager + Hilt wiring requires care, and widget UI must be tested across multiple launcher apps and Android versions.

Impact: High

Home screen widgets are the highest-engagement surface on Android. A well-designed widget is a daily reminder of the app's value without requiring any user action.


Related Issues

Labels: enhancement ยท widget ยท glance ยท workmanager ยท high-impact

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions