Skip to content

refactor: extract shared tap-action helper and skip no-op alarm updates#26

Merged
wassupluke merged 1 commit into
mainfrom
refactor/widget-simplify
Apr 3, 2026
Merged

refactor: extract shared tap-action helper and skip no-op alarm updates#26
wassupluke merged 1 commit into
mainfrom
refactor/widget-simplify

Conversation

@wassupluke
Copy link
Copy Markdown
Owner

@wassupluke wassupluke commented Apr 3, 2026

Summary

  • Extracts the duplicate getLaunchIntentForPackage + fallback-to-MainActivity logic from both WeatherWidget and AlarmWidget into a single resolveTapAction() helper in WidgetTapAction.kt. Each widget still reads from its own DataStore key (WIDGET_TAP_PACKAGE / ALARM_WIDGET_TAP_PACKAGE), so per-widget tap targets are independent and unchanged.
  • Adds a change-detection guard in AlarmWidgetReceiver.updateAlarmText() so the DataStore write and updateAll() call are skipped when the alarm text hasn't changed, avoiding redundant recompositions on repeated broadcasts.

Test plan

  • Add a weather widget and alarm widget — verify each can independently set a different tap target app
  • Trigger an alarm broadcast with no alarm change — verify widget does not recompose (check logs for no DataStore write)
  • Trigger an alarm broadcast with a real alarm change — verify widget updates correctly
  • Build passes: ./gradlew :app:compileDebugKotlin

Summary by Sourcery

Refactor widget tap handling into a shared helper and avoid unnecessary alarm widget updates when the alarm text is unchanged.

Enhancements:

  • Extract a shared tap-action resolver used by both the weather and alarm widgets while preserving their independent configuration.
  • Skip DataStore writes and widget updates for alarm broadcasts when the alarm text has not changed to reduce redundant recompositions.

- Extract duplicate getLaunchIntentForPackage + fallback logic from both
  widgets into resolveTapAction() in WidgetTapAction.kt; each widget
  still reads its own DataStore key so per-widget targets are unchanged
- Guard AlarmWidgetReceiver DataStore write + updateAll() behind a
  change-detection check to avoid redundant writes on repeated broadcasts
@sourcery-ai
Copy link
Copy Markdown

sourcery-ai Bot commented Apr 3, 2026

Reviewer's Guide

Refactors duplicate widget tap-target resolution into a shared helper and adds a change-detection guard to avoid redundant alarm widget updates when the alarm text is unchanged.

Sequence diagram for alarm broadcast handling with change-detection guard

sequenceDiagram
actor System
participant AlarmManager
participant AlarmWidgetReceiver
participant DataStore
participant AlarmWidget

System ->> AlarmManager: trigger alarm broadcast
AlarmManager ->> AlarmWidgetReceiver: onReceive(intent)
activate AlarmWidgetReceiver

AlarmWidgetReceiver ->> AlarmWidgetReceiver: compute alarmText from intent or default
AlarmWidgetReceiver ->> DataStore: data.first()
DataStore -->> AlarmWidgetReceiver: currentAlarmText

AlarmWidgetReceiver ->> AlarmWidgetReceiver: compare alarmText != currentAlarmText
alt text changed
  AlarmWidgetReceiver ->> DataStore: edit set ALARM_TEXT = alarmText
  AlarmWidgetReceiver ->> AlarmWidget: updateAll(context)
else text unchanged
  AlarmWidgetReceiver ->> AlarmWidgetReceiver: skip DataStore write and updateAll
end

deactivate AlarmWidgetReceiver
Loading

Sequence diagram for shared widget tap action resolution

sequenceDiagram
actor User
participant Widget as Widget
participant WidgetTapAction as resolveTapAction
participant PackageManager
participant MainActivity

Widget ->> WidgetTapAction: resolveTapAction(context, tapPackage)
alt tapPackage not null or empty
  WidgetTapAction ->> PackageManager: getLaunchIntentForPackage(tapPackage)
  PackageManager -->> WidgetTapAction: launchIntent
  alt launchIntent has component
    WidgetTapAction -->> Widget: Action to start component
  else no component
    WidgetTapAction -->> Widget: Action to start MainActivity
  end
else no tapPackage
  WidgetTapAction -->> Widget: Action to start MainActivity
end

User ->> Widget: tap widget
Widget ->> Widget: execute tapAction
Loading

Class diagram for shared widget tap action and alarm update guard

classDiagram
class AlarmWidget {
  +provideGlance(context)
}

class WeatherWidget {
  +provideGlance(context)
}

class AlarmWidgetReceiver {
  +onReceive(context, intent)
  +updateAlarmText(context, intent)
}

class WidgetTapActionKt {
  +resolveTapAction(context, tapPackage)
}

class WeatherDataStore {
  <<object>>
  +ALARM_WIDGET_TAP_PACKAGE
  +WIDGET_TAP_PACKAGE
  +ALARM_TEXT
}

class DataStore {
  +data
  +edit(transform)
}

AlarmWidget --> WeatherDataStore : reads
WeatherWidget --> WeatherDataStore : reads
AlarmWidget --> WidgetTapActionKt : uses
WeatherWidget --> WidgetTapActionKt : uses
AlarmWidgetReceiver --> WeatherDataStore : reads_writes
AlarmWidgetReceiver --> DataStore : uses
AlarmWidgetReceiver --> AlarmWidget : updateAll
Loading

File-Level Changes

Change Details Files
Share tap-action resolution logic between Weather and Alarm widgets via a new helper.
  • Replace inline tapIntent resolution in AlarmWidget with a call to a shared resolveTapAction helper that takes context and the widget’s stored tap package.
  • Replace inline tapIntent resolution in WeatherWidget with the same shared resolveTapAction helper, preserving use of their respective DataStore keys.
  • Introduce WidgetTapAction.kt defining resolveTapAction, which attempts to launch the stored package’s main activity and falls back to MainActivity when missing or invalid.
  • Remove now-unneeded direct imports of actionStartActivity and MainActivity from the widget files, centralizing that dependency in the helper.
app/src/main/java/com/wassupluke/widgets/widget/AlarmWidget.kt
app/src/main/java/com/wassupluke/widgets/widget/WeatherWidget.kt
app/src/main/java/com/wassupluke/widgets/widget/WidgetTapAction.kt
Avoid no-op alarm widget recompositions by skipping DataStore writes and updates when the alarm text has not changed.
  • Read the current ALARM_TEXT preference via dataStore.data.first() before attempting to update it.
  • Compare the newly computed alarmText with the stored value and only call dataStore.edit and AlarmWidget().updateAll(context) when they differ.
  • Import kotlinx.coroutines.flow.first to support reading the first value from the DataStore flow.
app/src/main/java/com/wassupluke/widgets/widget/AlarmWidgetReceiver.kt

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link
Copy Markdown

@sourcery-ai sourcery-ai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 1 issue, and left some high level feedback:

  • In AlarmWidgetReceiver.updateAlarmText, you’re doing a separate dataStore.data.first() read before edit; consider collapsing this to a single edit call that checks the existing value inside the transaction and only triggers updateAll when it actually changes to avoid an extra I/O pass.
  • You might make resolveTapAction a bit more flexible by parameterizing the fallback activity (e.g., via a KClass<out Activity> defaulting to MainActivity), which keeps the helper reusable if other widgets later need a different default.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- In `AlarmWidgetReceiver.updateAlarmText`, you’re doing a separate `dataStore.data.first()` read before `edit`; consider collapsing this to a single `edit` call that checks the existing value inside the transaction and only triggers `updateAll` when it actually changes to avoid an extra I/O pass.
- You might make `resolveTapAction` a bit more flexible by parameterizing the fallback activity (e.g., via a `KClass<out Activity>` defaulting to `MainActivity`), which keeps the helper reusable if other widgets later need a different default.

## Individual Comments

### Comment 1
<location path="app/src/main/java/com/wassupluke/widgets/widget/AlarmWidgetReceiver.kt" line_range="54" />
<code_context>
                 }
-                context.dataStore.edit { it[WeatherDataStore.ALARM_TEXT] = alarmText }
-                AlarmWidget().updateAll(context)
+                val current = context.dataStore.data.first()[WeatherDataStore.ALARM_TEXT]
+                if (alarmText != current) {
+                    context.dataStore.edit { it[WeatherDataStore.ALARM_TEXT] = alarmText }
+                    AlarmWidget().updateAll(context)
+                }
             } finally {
</code_context>
<issue_to_address>
**suggestion:** Consider folding the read-then-write into a single `edit` transaction to avoid extra I/O and eliminate subtle race windows.

The current pattern does a `dataStore.data.first()` followed by a conditional `edit`, which adds a second transaction and a race window where another writer could change the value between your read and write. You can keep the "only update when changed" optimization and make the comparison/write atomic by doing the read and conditional write inside a single `edit` and tracking whether it changed:

```kotlin
var changed = false
context.dataStore.edit { prefs ->
    val current = prefs[WeatherDataStore.ALARM_TEXT]
    if (alarmText != current) {
        prefs[WeatherDataStore.ALARM_TEXT] = alarmText
        changed = true
    }
}
if (changed) {
    AlarmWidget().updateAll(context)
}
```

Suggested implementation:

```
                }
                var alarmTextChanged = false
                context.dataStore.edit { prefs ->
                    val current = prefs[WeatherDataStore.ALARM_TEXT]
                    if (alarmText != current) {
                        prefs[WeatherDataStore.ALARM_TEXT] = alarmText
                        alarmTextChanged = true
                    }
                }
                if (alarmTextChanged) {
                    AlarmWidget().updateAll(context)
                }
            } finally {

```

If `kotlinx.coroutines.flow.first` is no longer used elsewhere in `AlarmWidgetReceiver.kt` after this change, you should remove the unused import:
- `import kotlinx.coroutines.flow.first`
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

}
context.dataStore.edit { it[WeatherDataStore.ALARM_TEXT] = alarmText }
AlarmWidget().updateAll(context)
val current = context.dataStore.data.first()[WeatherDataStore.ALARM_TEXT]
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Consider folding the read-then-write into a single edit transaction to avoid extra I/O and eliminate subtle race windows.

The current pattern does a dataStore.data.first() followed by a conditional edit, which adds a second transaction and a race window where another writer could change the value between your read and write. You can keep the "only update when changed" optimization and make the comparison/write atomic by doing the read and conditional write inside a single edit and tracking whether it changed:

var changed = false
context.dataStore.edit { prefs ->
    val current = prefs[WeatherDataStore.ALARM_TEXT]
    if (alarmText != current) {
        prefs[WeatherDataStore.ALARM_TEXT] = alarmText
        changed = true
    }
}
if (changed) {
    AlarmWidget().updateAll(context)
}

Suggested implementation:

                }
                var alarmTextChanged = false
                context.dataStore.edit { prefs ->
                    val current = prefs[WeatherDataStore.ALARM_TEXT]
                    if (alarmText != current) {
                        prefs[WeatherDataStore.ALARM_TEXT] = alarmText
                        alarmTextChanged = true
                    }
                }
                if (alarmTextChanged) {
                    AlarmWidget().updateAll(context)
                }
            } finally {

If kotlinx.coroutines.flow.first is no longer used elsewhere in AlarmWidgetReceiver.kt after this change, you should remove the unused import:

  • import kotlinx.coroutines.flow.first

@wassupluke wassupluke merged commit 6ebbd4c into main Apr 3, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant