Skip to content

fix: Refactor badge selection from index-based to name-based to fix state persistence issue#1601

Open
samruddhi-Rahegaonkar wants to merge 2 commits intofossasia:developmentfrom
samruddhi-Rahegaonkar:issue1597
Open

fix: Refactor badge selection from index-based to name-based to fix state persistence issue#1601
samruddhi-Rahegaonkar wants to merge 2 commits intofossasia:developmentfrom
samruddhi-Rahegaonkar:issue1597

Conversation

@samruddhi-Rahegaonkar
Copy link
Copy Markdown
Member

@samruddhi-Rahegaonkar samruddhi-Rahegaonkar commented Mar 3, 2026

Fixes #1597

Changes

  • Refactor: Use name-based badge selection instead of index-based selection
  • Replaced index-based selection with Set selectedBadgeNames
  • Updated delete logic to dynamically resolve indices from badge names
  • Prevented selection mismatch when badge list order changes
  • Ensured checkbox state persists correctly after saving settings
  • Improved state reliability and future-proofed selection handling

Screenshots / Recordings

Checklist:

  • No hard coding: I have used resources from constants.dart without hard coding any value.
  • No end of file edits: No modifications done at end of resource files.
  • Code reformatting: I have reformatted code and fixed indentation in every file included in this pull request.
  • Code analyzation: My code passes analyzations run in flutter analyze and tests run in flutter test.

Summary by Sourcery

Refactor badge selection to be name-based instead of index-based to ensure stable selection and persistence when badge lists change.

Bug Fixes:

  • Fix loss or mismatch of badge selections when the badge list order or contents change.
  • Ensure selected badge checkboxes persist correctly after saving and reloading settings.

Enhancements:

  • Persist selected badges by name in shared preferences for more robust state handling.
  • Clean up provider and UI selection logic to operate on badge names while keeping controller deletion in sync.

@sourcery-ai
Copy link
Copy Markdown
Contributor

sourcery-ai bot commented Mar 3, 2026

Reviewer's Guide

Refactors badge selection from index-based to name-based, adds persistence for selected badges by name, and updates UI components to rely on the new selection model to avoid mismatches when badge order changes.

Sequence diagram for deleting selected badges via SettingsScreen

sequenceDiagram
  actor User
  participant SettingsScreenState
  participant BadgeScanProvider
  participant SharedPreferences

  User ->> SettingsScreenState: tap delete selected badges
  SettingsScreenState ->> BadgeScanProvider: removeSelectedDevices()
  alt no badges selected
    BadgeScanProvider-->>SettingsScreenState: return (no-op)
  else badges selected
    BadgeScanProvider->>BadgeScanProvider: _badgeNames.removeWhere(name in _selectedBadgeNames)
    BadgeScanProvider->>BadgeScanProvider: _selectedBadgeNames.clear()
    BadgeScanProvider->>SharedPreferences: _saveToPrefs()
    SharedPreferences-->>BadgeScanProvider: persist badge_scan_mode, badge_names, selected_badge_names
    BadgeScanProvider-->>SettingsScreenState: notifyListeners()
  end
  SettingsScreenState-->>User: UI updates with remaining badges
Loading

Sequence diagram for loading persisted badge selection by name

sequenceDiagram
  participant BadgeScanProvider
  participant SharedPreferences
  participant UIWidget

  BadgeScanProvider->>BadgeScanProvider: constructor()
  BadgeScanProvider->>BadgeScanProvider: _loadFromPrefs()
  BadgeScanProvider->>SharedPreferences: getInstance()
  SharedPreferences-->>BadgeScanProvider: SharedPreferences

  BadgeScanProvider->>SharedPreferences: getInt(badge_scan_mode)
  SharedPreferences-->>BadgeScanProvider: modeIndex
  BadgeScanProvider->>BadgeScanProvider: set _mode from BadgeScanMode.values[modeIndex]

  BadgeScanProvider->>SharedPreferences: getStringList(badge_names)
  SharedPreferences-->>BadgeScanProvider: storedNames
  BadgeScanProvider->>BadgeScanProvider: set _badgeNames from storedNames

  BadgeScanProvider->>SharedPreferences: getStringList(selected_badge_names)
  SharedPreferences-->>BadgeScanProvider: storedSelected
  BadgeScanProvider->>BadgeScanProvider: _selectedBadgeNames = storedSelected filtered by _badgeNames

  BadgeScanProvider->>BadgeScanProvider: _isLoaded = true
  BadgeScanProvider-->>UIWidget: notifyListeners()
  UIWidget->>BadgeScanProvider: isSelected(index)
  BadgeScanProvider-->>UIWidget: _selectedBadgeNames.contains(_badgeNames[index])
Loading

Class diagram for updated BadgeScanProvider and related widgets

classDiagram
  class BadgeScanProvider {
    -BadgeScanMode _mode
    -List~String~ _badgeNames
    -Set~String~ _selectedBadgeNames
    -bool _isLoaded

    +BadgeScanProvider()
    +BadgeScanMode get mode()
    +List~String~ get badgeNames()
    +Set~String~ get selectedBadgeNames()
    +bool get isLoaded()

    -Future~void~ _loadFromPrefs()
    -Future~void~ _saveToPrefs()

    +void setMode(BadgeScanMode mode)
    +void setBadgeNames(List~String~ names)
    +void addBadgeName(String name)
    +void removeBadgeNameAt(int index)
    +void updateBadgeName(int index, String newName)

    +void toggleSelection(int index)
    +bool isSelected(int index)
    +void clearSelection()
    +void selectAll()
    +void removeSelectedDevices()
    +List~String~ getSelectedBadgeNames()
  }

  class BadgeScanSettingsWidgetState {
    -List~TextEditingController~ _controllers
    +void onDeleteSelectedPressed()
  }

  class SettingsScreenState {
    +void onDeleteSelectedPressed()
  }

  class SharedPreferences {
    +static Future~SharedPreferences~ getInstance()
    +int getInt(String key)
    +List~String~ getStringList(String key)
    +Future~bool~ setInt(String key, int value)
    +Future~bool~ setStringList(String key, List~String~ value)
  }

  BadgeScanProvider ..> SharedPreferences : uses
  BadgeScanSettingsWidgetState --> BadgeScanProvider : reads badgeNames
  BadgeScanSettingsWidgetState --> BadgeScanProvider : reads selectedBadgeNames
  SettingsScreenState --> BadgeScanProvider : calls removeSelectedDevices
  SettingsScreenState --> BadgeScanProvider : reads selectedBadgeNames
Loading

File-Level Changes

Change Details Files
Refactor provider to store and persist selected badges by name instead of index, and harden preference loading/saving.
  • Replaced Set _selectedIndices with Set _selectedBadgeNames and updated the corresponding public getter.
  • On load, read selected badge names from SharedPreferences, filtering out names that are no longer in the current badge list.
  • Persist selected badge names alongside mode and badgeNames in _saveToPrefs.
  • When badge names are set/updated/removed, reconcile _selectedBadgeNames to drop or rename selections that no longer exist or whose name changed.
  • Updated selection methods (toggleSelection, isSelected, clearSelection, selectAll, removeSelectedDevices, getSelectedBadgeNames) to operate on badge names rather than indices and to save after modifications.
  • Added basic validation to mode loading to guard against invalid stored enum indices and reorganized provider methods with clearer section comments.
lib/providers/BadgeScanProvider.dart
Update views to use name-based selection and correctly map selected badge names back to controller indices for deletion.
  • Replaced selectedIndices usages with selectedBadgeNames in the badge scan settings widget and settings screen.
  • In badgeScanSettingsWidget, derive selected indices by mapping selected badge names to their current indices in provider.badgeNames, ignoring names that are no longer present, and delete corresponding controllers in descending index order.
  • Keep delete button count labels in both widgets in sync with the new selectedBadgeNames-based model.
lib/view/badgeScanSettingsWidget.dart
lib/view/settings_screen.dart

Assessment against linked issues

Issue Objective Addressed Explanation
#1597 Ensure that selected badge checkboxes in Settings → Badge Scan Mode remain checked after clicking Save Settings (i.e., selection state is persisted and restored correctly).
#1597 Persist the user’s chosen badge names for scanning (the selected configuration) across app restarts and badge list changes without selection mismatch.

Possibly linked issues


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
Contributor

@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 2 issues, and left some high level feedback:

  • Using Set<String> _selectedBadgeNames and converting back to indices with indexOf will behave unexpectedly when there are duplicate badge names (only the first index is found and selection cannot distinguish between duplicates); consider either enforcing unique badge names or using a stable identifier (e.g., UUID) instead of the display name.
  • In updateBadgeName, a name that trims to an empty string still replaces the old name and is propagated into selections; if empty names should be disallowed like in setBadgeNames, add an early return or reuse the same validation/filtering logic there for consistency.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- Using `Set<String> _selectedBadgeNames` and converting back to indices with `indexOf` will behave unexpectedly when there are duplicate badge names (only the first index is found and selection cannot distinguish between duplicates); consider either enforcing unique badge names or using a stable identifier (e.g., UUID) instead of the display name.
- In `updateBadgeName`, a name that trims to an empty string still replaces the old name and is propagated into selections; if empty names should be disallowed like in `setBadgeNames`, add an early return or reuse the same validation/filtering logic there for consistency.

## Individual Comments

### Comment 1
<location path="lib/providers/BadgeScanProvider.dart" line_range="9-18" />
<code_context>
+  Set<String> _selectedBadgeNames = {};
</code_context>
<issue_to_address>
**issue:** Using badge names as selection keys can break with duplicate names.

`setBadgeNames` and `addBadgeName` don’t enforce uniqueness, so duplicate names will make `toggleSelection`, `removeSelectedDevices`, and the UI’s `indexOf` mapping ambiguous or only affect the first match. Either normalize `_badgeNames` to unique values (e.g., via a `Set`/deduping) or constrain the API so badge names are guaranteed unique.
</issue_to_address>

### Comment 2
<location path="lib/providers/BadgeScanProvider.dart" line_range="106-115" />
<code_context>

   void updateBadgeName(int index, String newName) {
     if (index < 0 || index >= _badgeNames.length) return;
-    _badgeNames[index] = newName.trim();
+
+    final oldName = _badgeNames[index];
+    final trimmed = newName.trim();
+
+    _badgeNames[index] = trimmed;
+
+    // Update selection if name changed
+    if (_selectedBadgeNames.contains(oldName)) {
+      _selectedBadgeNames.remove(oldName);
+      _selectedBadgeNames.add(trimmed);
+    }
+
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Updating a badge name can introduce empty or duplicate names, affecting selection integrity.

In `updateBadgeName`, `newName` is trimmed but not validated like in `setBadgeNames`/`addBadgeName`. This permits setting a badge to `""` and to names that already exist. Since selection is keyed by name, this can introduce invalid data and merge distinct selections due to `Set<String>` de-duplication. Consider early-returning on `trimmed.isEmpty` and, if appropriate, applying the same duplicate-handling rules used elsewhere.

Suggested implementation:

```
  void updateBadgeName(int index, String newName) {
    if (index < 0 || index >= _badgeNames.length) return;

    final oldName = _badgeNames[index];
    final trimmed = newName.trim();

    // Disallow empty names to keep badge and selection state valid
    if (trimmed.isEmpty) return;

    // Disallow duplicate names to avoid merging distinct selections via Set<String>
    if (_badgeNames.contains(trimmed) && trimmed != oldName) return;

    _badgeNames[index] = trimmed;

    // Update selection if name changed
    if (_selectedBadgeNames.contains(oldName)) {
      _selectedBadgeNames.remove(oldName);
      _selectedBadgeNames.add(trimmed);
    }

```

I assumed that duplicates should be disallowed and handled via an early return. 
If `setBadgeNames`/`addBadgeName` use a different policy (e.g., auto-renaming duplicates or merging), you should align the duplicate check here with that exact behavior to keep the logic consistent across all badge-name mutations.
</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.

Comment thread lib/providers/BadgeScanProvider.dart
Comment thread lib/providers/BadgeScanProvider.dart
@mariobehling mariobehling requested a review from Copilot April 13, 2026 01:13
@mariobehling
Copy link
Copy Markdown
Member

Thanks for the contribution!

A process note.

We have automatic Copilot PR reviews enabled on this repository. These reviews are only triggered if the contributor has GitHub Copilot enabled and an active license on their own account.

Please enable Copilot in your GitHub settings if you have access. In many regions, free licenses are available through educational institutions or developer programs. Enabling Copilot helps us speed up the auto review process and reduces manual review overhead for the core team.

Copy link
Copy Markdown
Member

@mariobehling mariobehling left a comment

Choose a reason for hiding this comment

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

Please address failing tests.

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Refactors the badge scan settings selection model from index-based to name-based, aiming to fix the issue where selected badge checkboxes become unchecked after saving (state persistence across badge list changes).

Changes:

  • Replaced index-based selection (selectedIndices) with name-based selection (selectedBadgeNames) in UI and provider logic.
  • Added persistence for selected badge names via SharedPreferences.
  • Updated removal logic to resolve controller indices dynamically from selected badge names.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
lib/view/settings_screen.dart Updates delete button visibility/count to use selectedBadgeNames instead of selectedIndices.
lib/view/badgeScanSettingsWidget.dart Updates delete logic to derive controller indices from selected badge names and updates delete count.
lib/providers/BadgeScanProvider.dart Refactors selection state to Set<String>, persists it, and updates selection/removal logic accordingly.
Comments suppressed due to low confidence (1)

lib/view/badgeScanSettingsWidget.dart:133

  • The controller-removal logic converts selected badge names to indices using badgeNames.indexOf(name). This breaks if badge names are not unique (it always returns the first match), and can desync _controllers from provider.badgeNames (disposing/removing the wrong controller while the provider removes different rows). Consider enforcing unique badge names, or mapping names to all indices (or using stable IDs) so controller disposal targets the exact items being removed.
                              final badgeNames = provider.badgeNames;

                              // Convert selected badge names to indices
                              final selectedIndices = provider
                                  .selectedBadgeNames
                                  .map((name) => badgeNames.indexOf(name))
                                  .where((index) => index != -1)
                                  .toList()
                                ..sort((a, b) =>
                                    b.compareTo(a)); // descending order

                              for (final index in selectedIndices) {
                                if (index < _controllers.length) {
                                  _controllers[index].dispose();
                                  _controllers.removeAt(index);
                                }
                              }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 75 to +80
void setBadgeNames(List<String> names) {
_badgeNames = names.where((name) => name.trim().isNotEmpty).toList();
_selectedIndices.clear(); // Clear selections when badge names change

// Remove selections that no longer exist
_selectedBadgeNames.removeWhere((name) => !_badgeNames.contains(name));

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

selectedBadgeNames is stored as a Set<String>, but _badgeNames is still allowed to contain duplicates (no de-duplication in setBadgeNames). With duplicate names, selecting one row will mark all rows with that name as selected (isSelected checks by name), and removeSelectedDevices() will remove all duplicates of a selected name. Consider enforcing unique badge names (reject/merge duplicates on set/add/update) or changing selection to use stable IDs so duplicates can be handled predictably.

Copilot uses AI. Check for mistakes.
Comment on lines +114 to +119
// Update selection if name changed
if (_selectedBadgeNames.contains(oldName)) {
_selectedBadgeNames.remove(oldName);
_selectedBadgeNames.add(trimmed);
}

Copy link

Copilot AI Apr 13, 2026

Choose a reason for hiding this comment

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

updateBadgeName updates _selectedBadgeNames to the trimmed value even when it becomes an empty string during editing (e.g., user clears a selected field before typing a new name). Because _saveToPrefs() is called on every change, this can persist an empty badge name and/or empty selection to SharedPreferences. Consider treating an empty trimmed as a special case (don’t add it to _selectedBadgeNames, and/or defer persistence until an explicit save).

Suggested change
// Update selection if name changed
if (_selectedBadgeNames.contains(oldName)) {
_selectedBadgeNames.remove(oldName);
_selectedBadgeNames.add(trimmed);
}
// Update selection if name changed, but never keep an empty badge name
// in the selected set during transient editing states.
if (_selectedBadgeNames.contains(oldName)) {
_selectedBadgeNames.remove(oldName);
if (trimmed.isNotEmpty) {
_selectedBadgeNames.add(trimmed);
}
}
// Avoid persisting transient empty values while the user is editing.
if (trimmed.isEmpty) {
notifyListeners();
return;
}

Copilot uses AI. Check for mistakes.
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.

Selected Badge Names Get Unchecked After Saving Settings

3 participants