Skip to content

feat: indicators panel resize and toolbar#487

Open
behnam-deriv wants to merge 7 commits into
masterfrom
feat-indicators-panel-resize-and-toolbar
Open

feat: indicators panel resize and toolbar#487
behnam-deriv wants to merge 7 commits into
masterfrom
feat-indicators-panel-resize-and-toolbar

Conversation

@behnam-deriv

@behnam-deriv behnam-deriv commented Jul 14, 2026

Copy link
Copy Markdown
Collaborator

Clickup link:
Fixes issue: #

This PR contains the following changes:

  • ✨ New feature (non-breaking change which adds functionality)
  • 🛠️ Bug fix (non-breaking change which fixes an issue)
  • ❌ Breaking change (fix or feature that would cause existing functionality to change)
  • 🧹 Code refactor
  • ✅ Build configuration change
  • 📝 Documentation
  • 🗑️ Chore

Developers Note (Optional)

Pre-launch Checklist (For PR creator)

As a creator of this PR:

  • ✍️ I have included clickup id and package/app_name in the PR title.
  • 👁️ I have gone through the code and removed any temporary changes (commented lines, prints, debug statements etc.).
  • ⚒️ I have fixed any errors/warnings shown by the analyzer/linter.
  • 📝 I have added documentation, comments and logging wherever required.
  • 🧪 I have added necessary tests for these changes.
  • 🔎 I have ensured all existing tests are passing.

Reviewers

Pre-launch Checklist (For Reviewers)

As a reviewer I ensure that:

  • ✴️ This PR follows the standard PR template.
  • 🪩 The information in this PR properly reflects the code changes.
  • 🧪 All the necessary tests for this PR's are passing.

Pre-launch Checklist (For QA)

  • 👌 It passes the acceptance criteria.

Pre-launch Checklist (For Maintainer)

  • [MAINTAINER_NAME] I make sure this PR fulfills its purpose.

Summary by Sourcery

Add persistent, resizable layout for main chart and bottom indicator panels across mobile and web, including unique per-indicator panel identities and themeable divider UI.

Enhancements:

  • Introduce panel size repository and fraction-based resizing logic to manage and persist panel heights for main and indicator charts.
  • Refine mobile and web chart layouts to use shared resizable dividers between panels, respecting minimum panel heights and hidden indicator behavior.
  • Extend indicator configuration models with stable config IDs used to preserve panel sizing across sessions and re-creations.
  • Expose panel size persistence through DerivChart API and wire it into the example app with a helper to quickly add sample indicators.
  • Update chart theming and dimensions to support interactive panel dividers with active-state styling and tuned hit areas.

Tests:

  • Add unit tests for panel fraction synchronization and cascading resize behavior.
  • Add unit tests for panel size repository persistence semantics and load-generation tracking.
  • Add widget tests for the resizable chart divider drag callbacks.

@sourcery-ai

sourcery-ai Bot commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Implements resizable chart panel dividers for main and indicator panels on mobile and web, with persisted panel height fractions and indicator-specific IDs, plus supporting theme, dimensions, example wiring, and tests.

Sequence diagram for resizable panel divider drag and persistence

sequenceDiagram
  actor User
  participant ResizableChartDivider
  participant _ChartState
  participant PanelSizeRepository
  participant SharedPreferences

  User->>ResizableChartDivider: onVerticalDragUpdate
  ResizableChartDivider->>_ChartState: _resizeCascadingPanels(orderedKeys,dividerIndex,deltaFraction,usableHeight)
  _ChartState->>_ChartState: resizeCascadingFractions(_panelFractions,orderedKeys,dividerIndex,deltaFraction,usableHeight)
  _ChartState->>_ChartState: setState()

  User->>ResizableChartDivider: onVerticalDragEnd
  ResizableChartDivider->>_ChartState: _persistPanelFractions()
  _ChartState->>PanelSizeRepository: save(_panelFractions)
  PanelSizeRepository->>SharedPreferences: setString(_storageKey,jsonEncode(fractions))
  PanelSizeRepository->>PanelSizeRepository: notifyListeners()
Loading

File-Level Changes

Change Details Files
Introduce persistent panel size management and cascading resize logic for chart panels.
  • Add PanelSizeRepository to persist main and bottom panel height fractions globally using SharedPreferences.
  • Add syncPanelFractions and resizeCascadingFractions helpers to normalize fractions and implement cascading resize respecting minimum heights.
  • Wire PanelSizeRepository into Chart and DerivChart, tracking per-panel fractions, reacting to async loads, and saving on drag end.
lib/src/deriv_chart/chart/panel_size/panel_size_repository.dart
lib/src/deriv_chart/chart/chart.dart
lib/src/deriv_chart/deriv_chart.dart
lib/deriv_chart.dart
test/deriv_chart/chart/panel_size_repository_test.dart
test/deriv_chart/chart/panel_fractions_test.dart
Add resizable dividers between chart panels with platform-appropriate hit areas and active styling.
  • Create ResizableChartDivider widget with mouse/touch drag handling and active color from ChartTheme.
  • Expose panelDividerActiveColor in ChartTheme and implement it in light/dark themes.
  • Use ResizableChartDivider between main chart and bottom panels on web and between bottom indicator panels on mobile, invoking cascading resize and persisting fractions.
lib/src/deriv_chart/chart/resizable_chart_divider.dart
lib/src/theme/chart_theme.dart
lib/src/theme/chart_default_light_theme.dart
lib/src/theme/chart_default_dark_theme.dart
lib/src/deriv_chart/chart/chart_state_web.dart
lib/src/deriv_chart/chart/chart_state_mobile.dart
test/deriv_chart/chart/resizable_chart_divider_test.dart
Refactor mobile bottom indicator layout to support individual panel resizing and stable sizes when hidden/unhidden.
  • Remove _bottomSectionHeight and _isAllBottomIndicatorsHidden logic from mobile chart state.
  • Compute bottom indicator panel keys and ordered chain including main panel to drive resizing and syncing of fractions.
  • Render each bottom indicator in a SizedBox with computed height, keeping its divider even when hidden and clamping visible height to a minimum title bar height.
  • Adjust BottomChartMobile to remove its own divider and rely on external ResizableChartDivider.
  • Account for divider heights when computing usable panel height on mobile.
lib/src/deriv_chart/chart/chart_state_mobile.dart
lib/src/deriv_chart/chart/bottom_chart_mobile.dart
lib/src/theme/dimens.dart
Refine web bottom indicator layout to use fractional heights instead of flex and integrate resizing.
  • Replace Expanded flex-based layout with explicit SizedBox heights driven by panel fractions and usable height.
  • Track fractions for main and all bottom panels, preserving custom sizes when a panel is expanded fullscreen.
  • Insert ResizableChartDivider between bottom panels when not expanded, wired to cascading resize and persistence.
  • Simplify BottomChart to remove its internal divider column, letting outer layout own dividers.
lib/src/deriv_chart/chart/chart_state_web.dart
lib/src/deriv_chart/chart/bottom_chart.dart
Add stable indicator configuration IDs and propagate them through configs and serialization.
  • Extend IndicatorConfig and all concrete indicator configs to accept and propagate configId.
  • Update copyWith methods to include configId so cloned configs preserve their identity.
  • Update generated *.g.dart JSON (fromJson/toJson) to read/write configId fields.
  • Assign a unique configId when adding new indicators via IndicatorsDialog to avoid size reuse collisions across indicators.
lib/src/add_ons/indicators_ui/indicator_config.dart
lib/src/add_ons/indicators_ui/indicators_dialog.dart
lib/src/add_ons/indicators_ui/*/*_indicator_config.dart
lib/src/add_ons/indicators_ui/*/*_indicator_config.g.dart
Update example app to showcase resizable indicator panels and simplify sample markers.
  • Introduce an AddOnsRepository for indicators in the fullscreen example and pass it into DerivChart and ChartSection.
  • Add an action button to quickly add sample bottom indicators (RSI, MACD, Stochastic) if not already present.
  • Increase marker duration and remove intermediate checkpoint markers from example chart markers.
example/lib/main.dart

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

@github-actions

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.

OpenSSF Scorecard

PackageVersionScoreDetails

Scanned Manifest Files

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Hey - I've found 3 issues, and left some high level feedback:

  • The naming between the top-level syncPanelFractions/resizeCascadingFractions and the _ChartState instance methods _syncPanelFractions/_resizeCascadingPanels is quite close and easy to confuse; consider renaming one side or extracting the shared helpers into a separate utility to make the call-sites and responsibilities clearer.
  • In panel_size_repository_test.dart the test hard-codes the storage key panelHeights instead of using a single source of truth; exposing the key (or a helper) from PanelSizeRepository would avoid these tests silently drifting out of sync if the key changes.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The naming between the top-level `syncPanelFractions`/`resizeCascadingFractions` and the `_ChartState` instance methods `_syncPanelFractions`/`_resizeCascadingPanels` is quite close and easy to confuse; consider renaming one side or extracting the shared helpers into a separate utility to make the call-sites and responsibilities clearer.
- In `panel_size_repository_test.dart` the test hard-codes the storage key `panelHeights` instead of using a single source of truth; exposing the key (or a helper) from `PanelSizeRepository` would avoid these tests silently drifting out of sync if the key changes.

## Individual Comments

### Comment 1
<location path="lib/src/deriv_chart/chart/chart.dart" line_range="485-486" />
<code_context>
+  /// Height available for panel fractions once [dividerCount]
+  /// [ResizableChartDivider]s - which take up real space in the same
+  /// Column as the panels - have been subtracted from [totalHeight].
+  double _usableHeightFor(double totalHeight, int dividerCount) =>
+      (totalHeight - dividerCount * Dimens.chartPanelDividerHitHeight)
+          .clamp(0.0, double.infinity);
+
</code_context>
<issue_to_address>
**issue (bug_risk):** Guard against zero or negative usableHeight to avoid division-by-zero and NaN/Infinity in resize math.

When `totalHeight` is less than `dividerCount * Dimens.chartPanelDividerHitHeight`, `_usableHeightFor` (now clamped) returns `0`. Callers like `_ChartStateMobile` and `_ChartStateWeb` then use `usableHeight` in:

- Drag math: `deltaPixels / usableHeight``Infinity`/`NaN` when `usableHeight == 0`.
- `resizeCascadingFractions`: `minFraction = indicatorTitleBarMinHeight / usableHeight` → also invalid for zero.

This is reachable on very small view heights or many indicators. In addition to clamping, please short‑circuit resizing when `usableHeight <= 0` (e.g., treat drag deltas as no‑ops or skip `_resizeCascadingPanels`) to avoid corrupting `_panelFractions` and keep the layout stable.
</issue_to_address>

### Comment 2
<location path="lib/src/deriv_chart/deriv_chart.dart" line_range="221" />
<code_context>

   late AddOnsRepository<DrawingToolConfig> _drawingToolsRepo;

+  final PanelSizeRepository _panelSizeRepo = PanelSizeRepository();
+
   final DrawingTools _drawingTools = DrawingTools();
</code_context>
<issue_to_address>
**suggestion (bug_risk):** Dispose the internal PanelSizeRepository to avoid leaking a ChangeNotifier.

`_panelSizeRepo` is a `ChangeNotifier` but isn't disposed in `_DerivChartState.dispose`. Since `Chart` only removes its listener, the notifier itself lives for the lifetime of `DerivChartState` and can accumulate listeners when the widget is recreated.

To fix this, dispose `_panelSizeRepo` only when this widget owns it (i.e. when `widget.panelSizeRepo == null`):

```dart
@override
void dispose() {
  if (widget.panelSizeRepo == null) {
    _panelSizeRepo.dispose();
  }
  super.dispose();
}
```

This keeps lifecycle responsibility clear between host-supplied and internally-created repos.

Suggested implementation:

```
  final PanelSizeRepository _panelSizeRepo = PanelSizeRepository();

  final DrawingTools _drawingTools = DrawingTools();

  late final InteractiveLayerBehaviour _interactiveLayerBehaviour;

```

```
  @override
  void dispose() {
    if (widget.panelSizeRepo == null) {
      _panelSizeRepo.dispose();
    }
    super.dispose();
  }

```

I assumed a minimal `dispose` implementation (`super.dispose();`) for the `_DerivChartState` class. If your actual `dispose` method already removes listeners (e.g. from `panelSizeRepo`, `Chart`, or other `ChangeNotifier`s), make sure to keep that existing cleanup logic and only insert the conditional `_panelSizeRepo.dispose();` before `super.dispose();` as shown. The `SEARCH` block for `dispose` should be adjusted to match your current method body exactly.
</issue_to_address>

### Comment 3
<location path="lib/src/deriv_chart/chart/resizable_chart_divider.dart" line_range="61-65" />
<code_context>
+            setState(() => _isDragging = false);
+            widget.onDragEnd?.call();
+          },
+          onVerticalDragCancel: () => setState(() => _isDragging = false),
+          child: child,
+        ),
</code_context>
<issue_to_address>
**suggestion:** Align drag-cancel behavior with drag-end to ensure persistence hooks run consistently.

`onVerticalDragEnd` clears `_isDragging` and triggers `onDragEnd`, which persists the panel fractions, but `onVerticalDragCancel` only clears `_isDragging`. If a drag is cancelled after updates, the UI resets while the persisted fractions remain stale.

To keep cancellation behavior consistent with drag end, have `onVerticalDragCancel` call the same cleanup logic:

```dart
onVerticalDragEnd: (_) {
  setState(() => _isDragging = false);
  widget.onDragEnd?.call();
},
onVerticalDragCancel: () {
  setState(() => _isDragging = false);
  widget.onDragEnd?.call();
},
```

```suggestion
          onVerticalDragEnd: (_) {
            setState(() => _isDragging = false);
            widget.onDragEnd?.call();
          },
          onVerticalDragCancel: () {
            setState(() => _isDragging = false);
            widget.onDragEnd?.call();
          },
```
</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 on lines +485 to +486
double _usableHeightFor(double totalHeight, int dividerCount) =>
(totalHeight - dividerCount * Dimens.chartPanelDividerHitHeight)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

issue (bug_risk): Guard against zero or negative usableHeight to avoid division-by-zero and NaN/Infinity in resize math.

When totalHeight is less than dividerCount * Dimens.chartPanelDividerHitHeight, _usableHeightFor (now clamped) returns 0. Callers like _ChartStateMobile and _ChartStateWeb then use usableHeight in:

  • Drag math: deltaPixels / usableHeightInfinity/NaN when usableHeight == 0.
  • resizeCascadingFractions: minFraction = indicatorTitleBarMinHeight / usableHeight → also invalid for zero.

This is reachable on very small view heights or many indicators. In addition to clamping, please short‑circuit resizing when usableHeight <= 0 (e.g., treat drag deltas as no‑ops or skip _resizeCascadingPanels) to avoid corrupting _panelFractions and keep the layout stable.


late AddOnsRepository<DrawingToolConfig> _drawingToolsRepo;

final PanelSizeRepository _panelSizeRepo = PanelSizeRepository();

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion (bug_risk): Dispose the internal PanelSizeRepository to avoid leaking a ChangeNotifier.

_panelSizeRepo is a ChangeNotifier but isn't disposed in _DerivChartState.dispose. Since Chart only removes its listener, the notifier itself lives for the lifetime of DerivChartState and can accumulate listeners when the widget is recreated.

To fix this, dispose _panelSizeRepo only when this widget owns it (i.e. when widget.panelSizeRepo == null):

@override
void dispose() {
  if (widget.panelSizeRepo == null) {
    _panelSizeRepo.dispose();
  }
  super.dispose();
}

This keeps lifecycle responsibility clear between host-supplied and internally-created repos.

Suggested implementation:

  final PanelSizeRepository _panelSizeRepo = PanelSizeRepository();

  final DrawingTools _drawingTools = DrawingTools();

  late final InteractiveLayerBehaviour _interactiveLayerBehaviour;

  @override
  void dispose() {
    if (widget.panelSizeRepo == null) {
      _panelSizeRepo.dispose();
    }
    super.dispose();
  }

I assumed a minimal dispose implementation (super.dispose();) for the _DerivChartState class. If your actual dispose method already removes listeners (e.g. from panelSizeRepo, Chart, or other ChangeNotifiers), make sure to keep that existing cleanup logic and only insert the conditional _panelSizeRepo.dispose(); before super.dispose(); as shown. The SEARCH block for dispose should be adjusted to match your current method body exactly.

Comment on lines +61 to +65
onVerticalDragEnd: (_) {
setState(() => _isDragging = false);
widget.onDragEnd?.call();
},
onVerticalDragCancel: () => setState(() => _isDragging = false),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

suggestion: Align drag-cancel behavior with drag-end to ensure persistence hooks run consistently.

onVerticalDragEnd clears _isDragging and triggers onDragEnd, which persists the panel fractions, but onVerticalDragCancel only clears _isDragging. If a drag is cancelled after updates, the UI resets while the persisted fractions remain stale.

To keep cancellation behavior consistent with drag end, have onVerticalDragCancel call the same cleanup logic:

onVerticalDragEnd: (_) {
  setState(() => _isDragging = false);
  widget.onDragEnd?.call();
},
onVerticalDragCancel: () {
  setState(() => _isDragging = false);
  widget.onDragEnd?.call();
},
Suggested change
onVerticalDragEnd: (_) {
setState(() => _isDragging = false);
widget.onDragEnd?.call();
},
onVerticalDragCancel: () => setState(() => _isDragging = false),
onVerticalDragEnd: (_) {
setState(() => _isDragging = false);
widget.onDragEnd?.call();
},
onVerticalDragCancel: () {
setState(() => _isDragging = false);
widget.onDragEnd?.call();
},

@behnam-deriv behnam-deriv changed the title Feat indicators panel resize and toolbar feat: indicators panel resize and toolbar Jul 14, 2026
@github-actions

github-actions Bot commented Jul 17, 2026

Copy link
Copy Markdown

Claude finished @behnam-deriv's task in 2m 18s —— View job


🤖 Claude PR Review Complete

Model: claude-sonnet-4-6 | Review Type: 🔄 Follow-up Review

Summary

0 of 4 issues from the previous review have been resolved. The incremental diff since the last review adds configSummary overrides to several indicator configs and fixes the title display for empty summaries — none of the four flagged issues were touched. All four issues remain exactly as reported.

Recommendation: REQUEST CHANGES


🟠 High Priority Issues

🟠 1. `PanelSizeRepository` created in state but never disposed — memory leaklib/src/deriv_chart/deriv_chart.dart:221

Details

Severity File Lines
HIGH lib/src/deriv_chart/deriv_chart.dart 221

❌ Problematic Code:

class _DerivChartState extends State<DerivChart> {
  // ...
  final PanelSizeRepository _panelSizeRepo = PanelSizeRepository();
  // ...
  // No dispose() override anywhere in _DerivChartState

📋 Issue: PanelSizeRepository extends ChangeNotifier. Flutter's ChangeNotifier.dispose() removes all registered listeners and signals that the notifier is no longer in use. Creating it inside state without calling dispose() means listeners accumulated over the widget's lifetime are never freed. The Chart widget's _ChartState.initState registers itself as a listener (widget.panelSizeRepo?.addListener(_onPanelSizeRepoChanged)), and _ChartState.dispose unregisters it correctly — but _panelSizeRepo itself is never torn down, which leaks its internal listener list.

⚠️ Impact: Memory grows each time a DerivChart is mounted and unmounted (e.g., navigating away and back), and spurious callbacks can reach already-disposed objects in debug builds.

✅ Fix:

@override
void dispose() {
  if (widget.panelSizeRepo == null) {
    _panelSizeRepo.dispose();
  }
  super.dispose();
}

💡 Explanation: ChangeNotifier.dispose() asserts no listeners remain (debug mode) and prevents further notifications after disposal. Since _DerivChartState owns _panelSizeRepo when widget.panelSizeRepo == null, it must dispose it.


🟡 Medium Priority Issues

🟡 2. Uncaught exceptions in `loadFromPrefs` can crash the chart on startuplib/src/deriv_chart/chart/panel_size/panel_size_repository.dart:61-68

Details

Severity File Lines
MEDIUM lib/src/deriv_chart/chart/panel_size/panel_size_repository.dart 61-68

❌ Problematic Code:

final Map<String, dynamic> decoded =
    jsonDecode(encoded) as Map<String, dynamic>;

fractions = decoded.map(
  (String key, dynamic value) => MapEntry<String, double>(
    key,
    (value as num).toDouble(),
  ),
);

📋 Issue: Both jsonDecode(encoded) as Map<String, dynamic> and (value as num).toDouble() will throw unhandled exceptions if the stored value has been corrupted, manually edited, or written by a future schema change. The loadFromPrefs caller in _DerivChartState._loadPanelSizes is a fire-and-forget async method — any exception thrown here is silently swallowed. Compare with loadSavedIndicatorsAndDrawingTools, which wraps its load in a try/on Exception block.

⚠️ Impact: A user upgrading from an older app version that wrote incompatible JSON into 'panelHeights' could experience a silent failure to load stored sizes, with no recovery path other than clearing storage.

✅ Fix:

void loadFromPrefs(SharedPreferences prefs) {
  _prefs = prefs;
  final String? encoded = prefs.getString(_storageKey);

  if (encoded == null) {
    fractions = <String, double>{};
    _loadGeneration++;
    notifyListeners();
    return;
  }

  try {
    final Map<String, dynamic> decoded =
        jsonDecode(encoded) as Map<String, dynamic>;
    fractions = decoded.map(
      (String key, dynamic value) => MapEntry<String, double>(
        key,
        (value as num).toDouble(),
      ),
    );
  } on Exception {
    // Corrupted or schema-incompatible data — start fresh.
    fractions = <String, double>{};
    _prefs?.remove(_storageKey);
  }

  _loadGeneration++;
  notifyListeners();
}

💡 Explanation: Graceful degradation (fall back to empty fractions) is better than a silent crash, and clearing the stale entry prevents the same error on every subsequent restart.


🟢 Low Priority Issues

🟢 3. Millisecond timestamp as `configId` risks collisionlib/src/add_ons/indicators_ui/indicators_dialog.dart:162-164

Details

Severity File Lines
LOW lib/src/add_ons/indicators_ui/indicators_dialog.dart 162-164

❌ Problematic Code:

repo.add(config.copyWith(
  number: postFixNumber,
  configId:
      DateTime.now().millisecondsSinceEpoch.toString(),
));

📋 Issue: Two indicators added in the same millisecond (e.g., from a batch add or a fast double-tap) will share the same configId, causing them to map to the same panel-size key. The second one silently overwrites the first's persisted size.

⚠️ Impact: Two identically-keyed panels both read and write to the same PanelSizeRepository entry; resizing one unexpectedly sets the other's size on the next app launch.

✅ Fix:

import 'dart:math' as math;

String _generateConfigId() {
  final int ms = DateTime.now().millisecondsSinceEpoch;
  final int rand = math.Random().nextInt(0xFFFFFF);
  return '${ms}_$rand';
}

// In the add handler:
repo.add(config.copyWith(
  number: postFixNumber,
  configId: _generateConfigId(),
));

Or, if the uuid package is already available, simply use const Uuid().v4().

💡 Explanation: Appending a random suffix makes same-millisecond collisions astronomically unlikely without adding a package dependency.

🟢 4. `context.read<ChartTheme>()` in `build()` won't react to theme changeslib/src/deriv_chart/chart/resizable_chart_divider.dart:52

Details

Severity File Lines
LOW lib/src/deriv_chart/chart/resizable_chart_divider.dart 52

❌ Problematic Code:

ChartTheme get _theme => context.read<ChartTheme>();

This getter is called from within build() at lines 72–74.

📋 Issue: context.read does not subscribe to the Provider — the widget will not rebuild when ChartTheme changes (e.g., system dark/light mode switch). The provider package's own documentation explicitly warns against calling read inside build.

⚠️ Impact: After a theme switch, the divider's line and handle keep rendering in stale colors until something else triggers a rebuild of this widget.

✅ Fix:

// Remove the getter and use context.watch directly in build():
@override
Widget build(BuildContext context) {
  final ChartTheme theme = context.watch<ChartTheme>();
  final Color color = _isDragging
      ? theme.panelDividerActiveColor
      : theme.crosshairLineDesktopColor;
  // ...
}

💡 Explanation: context.watch subscribes to the provided value and triggers a rebuild when it changes, which is the correct pattern for values consumed in build.


Summary Table

Priority Count Categories
🔴 Critical 0
🟠 High 1 Missing ChangeNotifier.dispose() in _DerivChartState
🟡 Medium 1 No error handling in PanelSizeRepository.loadFromPrefs
🟢 Low 2 Millisecond-based configId collision; context.read in build

Recommendations

  1. Add dispose() to _DerivChartState — one-liner fix, high impact; should block merge.
  2. Wrap loadFromPrefs in a try/catch — keeps chart working even on schema mismatch.
  3. Improve configId uniqueness — add a random suffix or use a UUID.
  4. Switch to context.watch in ResizableChartDivider.build — aligns with provider best practices.

Auto Fix Claude Reviews

Action Open Dashboard

@github-actions

Copy link
Copy Markdown

🤖 Claude PR Review Complete

Model: claude-sonnet-4-6 | Review Type: 🔄 Follow-up Review

Summary

0 of 4 issues from the previous review have been resolved. The incremental diff since the last review adds configSummary overrides to several indicator configs and fixes the title display for empty summaries — none of the four flagged issues were touched. All four issues remain exactly as reported.

Recommendation: REQUEST CHANGES


🟠 High Priority Issues

🟠 1. `PanelSizeRepository` created in state but never disposed — memory leaklib/src/deriv_chart/deriv_chart.dart:221

Details

Severity File Lines
HIGH lib/src/deriv_chart/deriv_chart.dart 221

❌ Problematic Code:

class _DerivChartState extends State<DerivChart> {
  // ...
  final PanelSizeRepository _panelSizeRepo = PanelSizeRepository();
  // ...
  // No dispose() override anywhere in _DerivChartState

📋 Issue: PanelSizeRepository extends ChangeNotifier. Flutter's ChangeNotifier.dispose() removes all registered listeners and signals that the notifier is no longer in use. Creating it inside state without calling dispose() means listeners accumulated over the widget's lifetime are never freed. The Chart widget's _ChartState.initState registers itself as a listener (widget.panelSizeRepo?.addListener(_onPanelSizeRepoChanged)), and _ChartState.dispose unregisters it correctly — but _panelSizeRepo itself is never torn down, which leaks its internal listener list.

⚠️ Impact: Memory grows each time a DerivChart is mounted and unmounted (e.g., navigating away and back), and spurious callbacks can reach already-disposed objects in debug builds.

✅ Fix:

@override
void dispose() {
  if (widget.panelSizeRepo == null) {
    _panelSizeRepo.dispose();
  }
  super.dispose();
}

💡 Explanation: ChangeNotifier.dispose() asserts no listeners remain (debug mode) and prevents further notifications after disposal. Since _DerivChartState owns _panelSizeRepo when widget.panelSizeRepo == null, it must dispose it.


🟡 Medium Priority Issues

🟡 2. Uncaught exceptions in `loadFromPrefs` can crash the chart on startuplib/src/deriv_chart/chart/panel_size/panel_size_repository.dart:61-68

Details

Severity File Lines
MEDIUM lib/src/deriv_chart/chart/panel_size/panel_size_repository.dart 61-68

❌ Problematic Code:

final Map<String, dynamic> decoded =
    jsonDecode(encoded) as Map<String, dynamic>;

fractions = decoded.map(
  (String key, dynamic value) => MapEntry<String, double>(
    key,
    (value as num).toDouble(),
  ),
);

📋 Issue: Both jsonDecode(encoded) as Map<String, dynamic> and (value as num).toDouble() will throw unhandled exceptions if the stored value has been corrupted, manually edited, or written by a future schema change. The loadFromPrefs caller in _DerivChartState._loadPanelSizes is a fire-and-forget async method — any exception thrown here is silently swallowed. Compare with loadSavedIndicatorsAndDrawingTools, which wraps its load in a try/on Exception block.

⚠️ Impact: A user upgrading from an older app version that wrote incompatible JSON into 'panelHeights' could experience a silent failure to load stored sizes, with no recovery path other than clearing storage.

✅ Fix:

void loadFromPrefs(SharedPreferences prefs) {
  _prefs = prefs;
  final String? encoded = prefs.getString(_storageKey);

  if (encoded == null) {
    fractions = <String, double>{};
    _loadGeneration++;
    notifyListeners();
    return;
  }

  try {
    final Map<String, dynamic> decoded =
        jsonDecode(encoded) as Map<String, dynamic>;
    fractions = decoded.map(
      (String key, dynamic value) => MapEntry<String, double>(
        key,
        (value as num).toDouble(),
      ),
    );
  } on Exception {
    // Corrupted or schema-incompatible data — start fresh.
    fractions = <String, double>{};
    _prefs?.remove(_storageKey);
  }

  _loadGeneration++;
  notifyListeners();
}

💡 Explanation: Graceful degradation (fall back to empty fractions) is better than a silent crash, and clearing the stale entry prevents the same error on every subsequent restart.


🟢 Low Priority Issues

🟢 3. Millisecond timestamp as `configId` risks collisionlib/src/add_ons/indicators_ui/indicators_dialog.dart:162-164

Details

Severity File Lines
LOW lib/src/add_ons/indicators_ui/indicators_dialog.dart 162-164

❌ Problematic Code:

repo.add(config.copyWith(
  number: postFixNumber,
  configId:
      DateTime.now().millisecondsSinceEpoch.toString(),
));

📋 Issue: Two indicators added in the same millisecond (e.g., from a batch add or a fast double-tap) will share the same configId, causing them to map to the same panel-size key. The second one silently overwrites the first's persisted size.

⚠️ Impact: Two identically-keyed panels both read and write to the same PanelSizeRepository entry; resizing one unexpectedly sets the other's size on the next app launch.

✅ Fix:

import 'dart:math' as math;

String _generateConfigId() {
  final int ms = DateTime.now().millisecondsSinceEpoch;
  final int rand = math.Random().nextInt(0xFFFFFF);
  return '${ms}_$rand';
}

// In the add handler:
repo.add(config.copyWith(
  number: postFixNumber,
  configId: _generateConfigId(),
));

Or, if the uuid package is already available, simply use const Uuid().v4().

💡 Explanation: Appending a random suffix makes same-millisecond collisions astronomically unlikely without adding a package dependency.

🟢 4. `context.read<ChartTheme>()` in `build()` won't react to theme changeslib/src/deriv_chart/chart/resizable_chart_divider.dart:52

Details

Severity File Lines
LOW lib/src/deriv_chart/chart/resizable_chart_divider.dart 52

❌ Problematic Code:

ChartTheme get _theme => context.read<ChartTheme>();

This getter is called from within build() at lines 72–74.

📋 Issue: context.read does not subscribe to the Provider — the widget will not rebuild when ChartTheme changes (e.g., system dark/light mode switch). The provider package's own documentation explicitly warns against calling read inside build.

⚠️ Impact: After a theme switch, the divider's line and handle keep rendering in stale colors until something else triggers a rebuild of this widget.

✅ Fix:

// Remove the getter and use context.watch directly in build():
@override
Widget build(BuildContext context) {
  final ChartTheme theme = context.watch<ChartTheme>();
  final Color color = _isDragging
      ? theme.panelDividerActiveColor
      : theme.crosshairLineDesktopColor;
  // ...
}

💡 Explanation: context.watch subscribes to the provided value and triggers a rebuild when it changes, which is the correct pattern for values consumed in build.


Summary Table

Priority Count Categories
🔴 Critical 0
🟠 High 1 Missing ChangeNotifier.dispose() in _DerivChartState
🟡 Medium 1 No error handling in PanelSizeRepository.loadFromPrefs
🟢 Low 2 Millisecond-based configId collision; context.read in build

Recommendations

  1. Add dispose() to _DerivChartState — one-liner fix, high impact; should block merge.
  2. Wrap loadFromPrefs in a try/catch — keeps chart working even on schema mismatch.
  3. Improve configId uniqueness — add a random suffix or use a UUID.
  4. Switch to context.watch in ResizableChartDivider.build — aligns with provider best practices.

Auto Fix Claude Reviews

Action Open Dashboard

View job run

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