Skip to content

fix: drawing tools sync issue in mobile app#484

Merged
behnam-deriv merged 3 commits into
masterfrom
fix-drawing-tools-sync-issue-mobile
Jun 25, 2026
Merged

fix: drawing tools sync issue in mobile app#484
behnam-deriv merged 3 commits into
masterfrom
fix-drawing-tools-sync-issue-mobile

Conversation

@behnam-deriv

@behnam-deriv behnam-deriv commented Jun 24, 2026

Copy link
Copy Markdown
Collaborator

Clickup link:
Fixes issue: #

This PR contains the following changes:
Release-mode timing exposed several sync failures between the drawing
tools repository, the interactive layer state machine, and the painted
layer:

  • updateStateTo could await a TickerFuture that never completes when a
    keyed remount disposes the AnimationController mid-animation, leaving
    the state machine stuck; awaited animations now use .orCancel with
    mounted guards, and a sequence guard drops superseded transitions.
  • The add flow selected the preview drawing instance without the
    persisted configId, breaking later edits/deletes; the repo-backed
    config is now bound to the selected instance and reconciliation
    unifies map/selected instances sharing the same id.
  • The gesture handler painted and hit-tested a snapshot drawings list
    that could go stale when a rebuild failed to propagate (bottom-sheet
    "clear all"); it now reads the reconciled map live and repaints via a
    drawings-changed notifier independent of rebuild delivery.

Adds debug-only [chart-diag] logging and regression tests covering
remount-cancelled transitions, superseded transitions, instance
unification, and clear-all repaint.

  • ✨ 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

Improve synchronization between drawing tools repository, interactive layer state machine, and painted layer to prevent stuck states and ghost drawings in mobile charts.

Bug Fixes:

  • Ensure state transitions complete or are safely cancelled when animations are interrupted by keyed remounts, preventing the interactive layer state machine from hanging.
  • Bind newly added drawing tools to their persisted configs so selections, drags, and deletions operate on the correct repo-backed instance instead of a transient preview copy.
  • Keep the painted and hit-tested drawings list in sync with the live repository map, including clear-all and repository swaps, so no stale or ghost drawings remain visible.

Enhancements:

  • Add robust reconciliation between the interactive layer and drawing tools repository, including reuse of the currently selected drawing instance and automatic state reset when the selected tool is deleted.
  • Introduce a drawings-changed notifier and live getters in the gesture handler so painting and hit-testing always reflect the latest reconciled drawings, independent of widget rebuild timing.
  • Guard interactive layer updates and animations against disposed states and superseded transitions using mounted checks, sequence IDs, and scheduler-aware notifications.
  • Add chart diagnostics logging utilities for interactive-layer sync issues and improve debuggability with detailed state transition traces.

Tests:

  • Add widget and integration tests covering repo/layer synchronization, remount-cancelled and superseded transitions, instance unification between selected and painted drawings, and clear-all repaint behavior in DerivChart and InteractiveLayer.

@sourcery-ai

sourcery-ai Bot commented Jun 24, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

Refactors the interactive drawing layer to keep the painted drawings, interaction state machine, and drawing tools repository strictly in sync across remounts and repo changes, adds guarded animation/state transitions, wires a live drawings notifier into the gesture handler, and introduces diagnostics plus regression tests for the previously flaky mobile drawing-tools flows.

Sequence diagram for guarded interactive state transitions

sequenceDiagram
  participant Behaviour as InteractiveLayerBehaviour
  participant Layer as _InteractiveLayerGestureHandlerState
  participant Controller as AnimationController

  Behaviour->>Behaviour: updateStateTo(newState, direction, waitForAnimation, animate)
  Behaviour->>Behaviour: increment _stateTransitionSeq
  alt waitForAnimation
    Behaviour->>Layer: animateStateChange(direction, animate)
    opt [layer is not mounted]
      Layer-->>Behaviour: return (no animation)
    end
    alt direction == forward
      Layer->>Controller: reset()
      alt animate
        Layer->>Controller: forward().orCancel
        Controller-->>Layer: TickerFuture completes
      else [animate == false]
        Layer->>Controller: value = 1.0
      end
    else direction == reverse
      alt animate
        Layer->>Controller: reverse().orCancel
        Controller-->>Layer: TickerFuture completes
      else [animate == false]
        Layer->>Controller: value = 0.0
      end
    end
    Layer-->>Behaviour: animation completed
    alt [transition superseded]
      Behaviour-->>Behaviour: return (drop stale transition)
    else [still current]
      Behaviour->>Behaviour: _controller.currentState = newState
      Behaviour->>Behaviour: onUpdate()
    end
  else [waitForAnimation == false]
    Behaviour->>Layer: animateStateChange(direction, animate) (unawaited)
    Behaviour->>Behaviour: _controller.currentState = newState
    Behaviour->>Behaviour: onUpdate()
  end
Loading

File-Level Changes

Change Details Files
Make interactive layer reconcile its internal drawings map with the drawing tools repository on every build and notify a drawings-changed listener to keep the paint layer and hit-testing in sync, even when widget rebuilds are missed.
  • Add an InteractionNotifier for drawings changes and wire it between the interactive layer state and the gesture handler.
  • Introduce _reconcileDrawings, called from both the repo listener and build, to add/remove drawings based on repo contents, reuse the selected InteractableDrawing instance when ids match, and reset state when a selected tool is deleted.
  • Guard reconciliation against uninitialized behaviour or zero-size drawing context, with post-frame retries and diagnostic logging.
  • Change the gesture handler to consume a ValueGetter of drawings plus a Listenable drawingsChanged, and use these for painting and gesture hit-testing via MultipleAnimatedBuilder.
lib/src/deriv_chart/interactive_layer/interactive_layer.dart
Harden the interactive behaviour state machine transitions against cancelled animations and superseded transitions, preventing stuck states and stale transitions from overwriting newer ones.
  • Add an isInitialized flag to InteractiveLayerBehaviour and a monotonic transition sequence id to track state transitions.
  • Wrap animateStateChange in a try/catch using .orCancel on the AnimationController futures to ensure awaiting transitions complete even when animations are cancelled via TickerCanceled.
  • Drop transitions whose sequence id no longer matches the latest, so a late-resuming transition cannot override a newer state.
  • Enhance diagnostics by describing states, including selected drawing ids, when logging transitions.
lib/src/deriv_chart/interactive_layer/interactive_layer_behaviours/interactive_layer_behaviour.dart
lib/src/deriv_chart/interactive_layer/interactive_layer.dart
Fix the drawing-tools add flow so the selected drawing instance is backed by the persisted repository config (with configId) and the selection transition does not await animation, avoiding ghost tools and non-persisting edits.
  • Change InteractiveAddingToolState to clear the preview, add the drawing via interactiveLayer.addDrawing, capture the returned config, and assign it back to the preview’s interactableDrawing before selecting.
  • Update the subsequent updateStateTo call to skip waiting for animation, ensuring the selection state is applied promptly without racing keyed remounts.
lib/src/deriv_chart/interactive_layer/interactive_layer_states/interactive_adding_tool_state.dart
Add chart-diagnostics logging hooks and instrument AddOnsRepository for loading/saving and interactive layer components for lifecycle and repaint behaviour, gated behind a debug-only flag.
  • Introduce chart_diagnostics.dart with kChartDiagnosticsEnabled and chartDiag helper using debugPrint with a [chart-diag] prefix.
  • Log repository load/add/remove/clear operations including config ids and keys.
  • Log interactive layer init/dispose, repo switching, reconciliation outcomes, and paint-layer changes, plus gesture handler init/dispose and state-transition events.
lib/src/misc/chart_diagnostics.dart
lib/src/add_ons/add_ons_repository.dart
lib/src/deriv_chart/interactive_layer/interactive_layer.dart
lib/src/deriv_chart/interactive_layer/interactive_layer_behaviours/interactive_layer_behaviour.dart
lib/src/deriv_chart/interactive_layer/interactive_layer.dart
Add regression tests and integration tests to cover repo/layer sync, remount behaviour, animation-cancel races, clear-all repaint, and full DerivChart integration with SharedPreferences-backed repos.
  • Create interactive_layer_sync_test.dart to cover pre-mount repo items, add/remove, repo instance swapping, selected-instance reuse, remount-cancelled transitions, instance unification, clear() behaviour, and superseded transitions.
  • Create deriv_chart_drawing_tools_integration_test.dart to mirror TradeChart wiring, including symbol switches, SharedPreferences-backed repo reloads, deletes, and pre-mount loads.
  • Use a desktop behaviour and mobile behaviour respectively to approximate both platforms in tests, and ensure pumps cover post-frame sync and animation windows.
test/deriv_chart/chart/interactive_layer/interactive_layer_sync_test.dart
test/deriv_chart/chart/interactive_layer/deriv_chart_drawing_tools_integration_test.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 reviewed your changes and they look great!


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.

@github-actions

github-actions Bot commented Jun 25, 2026

Copy link
Copy Markdown

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


🤖 Claude PR Review Complete

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

Summary

1 of 3 issues from the previous review have been resolved.

The latest commit addressed two regressions in main_chart.dart (null-guard for markerSeries during animation ticks, and using a live XAxisModel reference in _buildAnnotations). Neither of the two outstanding interactive_layer.dart issues was touched in this commit — the catch block in syncDrawingsWithConfigs still silently discards exceptions in release builds, and the zero-size retry path still accumulates unguarded addPostFrameCallback calls.

Recommendation: REQUEST CHANGES


🟠 High Priority Issues

🟠 1. Silent exception swallowing with debug-only diagnostics in release buildslib/src/deriv_chart/interactive_layer/interactive_layer.dart:208-218

Details

Severity File Lines
HIGH lib/src/deriv_chart/interactive_layer/interactive_layer.dart 208-218

❌ Problematic Code:

try {
  _reconcileDrawings();
} on Object catch (error, stackTrace) {
  // Diagnostics: a throw between the map mutation and setState would
  // leave the painted drawings stale with no visible error in release.
  // Log it and still rebuild from whatever state the map is in.
  if (kChartDiagnosticsEnabled) {
    chartDiag('layer#$hashCode reconcile threw: $error\n$stackTrace');
  }
}
setState(() {});

📋 Issue: kChartDiagnosticsEnabled is kDebugMode, which is false in release builds (a compile-time constant). Any exception thrown inside _reconcileDrawings() — including null-assertion failures on config.configId! for legacy stored configs — is completely silently swallowed in production. The only consequence is broken or frozen drawing state with no crash, no log, and no signal reaching any error-monitoring service.

⚠️ Impact: Production drawing-tool failures become invisible. Users see broken behavior (ghost drawings, stuck state) with no way to diagnose the root cause from crash reports.

✅ Fix:

try {
  _reconcileDrawings();
} on Object catch (error, stackTrace) {
  // Continue with whatever partial state the map is in; a throw mid-mutation
  // would otherwise leave painted drawings stale. Report the error so it
  // surfaces in production monitoring even though we handle it gracefully.
  FlutterError.reportError(FlutterErrorDetails(
    exception: error,
    stack: stackTrace,
    library: 'deriv_chart',
    context: ErrorDescription('reconciling drawing tools with repository'),
  ));
}
setState(() {});

💡 Explanation: FlutterError.reportError is visible in all build modes and integrates with Flutter's error reporting pipeline (Crashlytics, Sentry, etc. via FlutterError.onError). It preserves the same graceful-recovery behavior (no rethrow, state rebuilt from whatever the map has) while making the failure observable in production.


🟢 Low Priority Issues

🟢 2. Missing deduplication guard for zero-size post-frame retry in _reconcileDrawingslib/src/deriv_chart/interactive_layer/interactive_layer.dart:249-260

Details

Severity File Lines
LOW lib/src/deriv_chart/interactive_layer/interactive_layer.dart 249-260

❌ Problematic Code:

if (drawingContext.fullSize == Size.zero) {
  if (kChartDiagnosticsEnabled) {
    chartDiag('layer#$hashCode reconcile skipped: drawing context has zero size...');
  }
  // Drawing context not ready yet, schedule retry after next frame
  WidgetsBinding.instance.addPostFrameCallback((_) {
    if (mounted) {
      syncDrawingsWithConfigs();
    }
  });
  return;
}

📋 Issue: _reconcileDrawings() is called on every build() (line ~467). If drawingContext.fullSize is Size.zero during multiple consecutive builds (rapid rebuilds before first layout), a new addPostFrameCallback is registered on each build. Each callback calls syncDrawingsWithConfigs()setState() → another rebuild. Unlike _scheduleStateReset() — which guards with a _stateResetScheduled boolean to prevent duplicate scheduling — the zero-size retry path has no such guard, so redundant callbacks can accumulate.

⚠️ Impact: Minor: a few redundant rebuilds during widget initialization. No visible behavioral difference, but inconsistent with the guarding pattern already established in the same class.

✅ Fix:

bool _zeroSizeRetryScheduled = false;

// In _reconcileDrawings:
if (drawingContext.fullSize == Size.zero) {
  if (!_zeroSizeRetryScheduled) {
    _zeroSizeRetryScheduled = true;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _zeroSizeRetryScheduled = false;
      if (mounted) {
        syncDrawingsWithConfigs();
      }
    });
  }
  return;
}

💡 Explanation: Consistent with the _stateResetScheduled pattern already used for _scheduleStateReset() in the same class. Prevents multiple pending retry callbacks from stacking up during multi-build sequences before first layout.


Summary Table

Priority Count Categories
🔴 Critical 0
🟠 High 1 Silent production exception swallowing
🟡 Medium 0
🟢 Low 1 Missing retry deduplication guard

Recommendations

  1. Must fix before merge: Replace the debug-only chartDiag in the syncDrawingsWithConfigs catch block with FlutterError.reportError so production exceptions remain observable (Issue Phase 1 initial #1). This is a single targeted change in one location.

  2. Low-risk follow-up: Add _zeroSizeRetryScheduled guard to match the _stateResetScheduled pattern (Issue Ramin/animate_to_current_tick #2).

The main_chart.dart changes in the latest commit are correct and welcome: the null-guard on markerSeries prevents the release-mode ErrorWidget crash, and reading XAxisModel via context.watch<XAxisModel>() inside a LayoutBuilder ensures annotations track scroll/zoom changes with live closures rather than a stale capture.


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

1 of 3 issues from the previous review have been resolved.

The latest commit addressed two regressions in main_chart.dart (null-guard for markerSeries during animation ticks, and using a live XAxisModel reference in _buildAnnotations). Neither of the two outstanding interactive_layer.dart issues was touched in this commit — the catch block in syncDrawingsWithConfigs still silently discards exceptions in release builds, and the zero-size retry path still accumulates unguarded addPostFrameCallback calls.

Recommendation: REQUEST CHANGES


🟠 High Priority Issues

🟠 1. Silent exception swallowing with debug-only diagnostics in release buildslib/src/deriv_chart/interactive_layer/interactive_layer.dart:208-218

Details

Severity File Lines
HIGH lib/src/deriv_chart/interactive_layer/interactive_layer.dart 208-218

❌ Problematic Code:

try {
  _reconcileDrawings();
} on Object catch (error, stackTrace) {
  // Diagnostics: a throw between the map mutation and setState would
  // leave the painted drawings stale with no visible error in release.
  // Log it and still rebuild from whatever state the map is in.
  if (kChartDiagnosticsEnabled) {
    chartDiag('layer#$hashCode reconcile threw: $error\n$stackTrace');
  }
}
setState(() {});

📋 Issue: kChartDiagnosticsEnabled is kDebugMode, which is false in release builds (a compile-time constant). Any exception thrown inside _reconcileDrawings() — including null-assertion failures on config.configId! for legacy stored configs — is completely silently swallowed in production. The only consequence is broken or frozen drawing state with no crash, no log, and no signal reaching any error-monitoring service.

⚠️ Impact: Production drawing-tool failures become invisible. Users see broken behavior (ghost drawings, stuck state) with no way to diagnose the root cause from crash reports.

✅ Fix:

try {
  _reconcileDrawings();
} on Object catch (error, stackTrace) {
  // Continue with whatever partial state the map is in; a throw mid-mutation
  // would otherwise leave painted drawings stale. Report the error so it
  // surfaces in production monitoring even though we handle it gracefully.
  FlutterError.reportError(FlutterErrorDetails(
    exception: error,
    stack: stackTrace,
    library: 'deriv_chart',
    context: ErrorDescription('reconciling drawing tools with repository'),
  ));
}
setState(() {});

💡 Explanation: FlutterError.reportError is visible in all build modes and integrates with Flutter's error reporting pipeline (Crashlytics, Sentry, etc. via FlutterError.onError). It preserves the same graceful-recovery behavior (no rethrow, state rebuilt from whatever the map has) while making the failure observable in production.


🟢 Low Priority Issues

🟢 2. Missing deduplication guard for zero-size post-frame retry in _reconcileDrawingslib/src/deriv_chart/interactive_layer/interactive_layer.dart:249-260

Details

Severity File Lines
LOW lib/src/deriv_chart/interactive_layer/interactive_layer.dart 249-260

❌ Problematic Code:

if (drawingContext.fullSize == Size.zero) {
  if (kChartDiagnosticsEnabled) {
    chartDiag('layer#$hashCode reconcile skipped: drawing context has zero size...');
  }
  // Drawing context not ready yet, schedule retry after next frame
  WidgetsBinding.instance.addPostFrameCallback((_) {
    if (mounted) {
      syncDrawingsWithConfigs();
    }
  });
  return;
}

📋 Issue: _reconcileDrawings() is called on every build() (line ~467). If drawingContext.fullSize is Size.zero during multiple consecutive builds (rapid rebuilds before first layout), a new addPostFrameCallback is registered on each build. Each callback calls syncDrawingsWithConfigs()setState() → another rebuild. Unlike _scheduleStateReset() — which guards with a _stateResetScheduled boolean to prevent duplicate scheduling — the zero-size retry path has no such guard, so redundant callbacks can accumulate.

⚠️ Impact: Minor: a few redundant rebuilds during widget initialization. No visible behavioral difference, but inconsistent with the guarding pattern already established in the same class.

✅ Fix:

bool _zeroSizeRetryScheduled = false;

// In _reconcileDrawings:
if (drawingContext.fullSize == Size.zero) {
  if (!_zeroSizeRetryScheduled) {
    _zeroSizeRetryScheduled = true;
    WidgetsBinding.instance.addPostFrameCallback((_) {
      _zeroSizeRetryScheduled = false;
      if (mounted) {
        syncDrawingsWithConfigs();
      }
    });
  }
  return;
}

💡 Explanation: Consistent with the _stateResetScheduled pattern already used for _scheduleStateReset() in the same class. Prevents multiple pending retry callbacks from stacking up during multi-build sequences before first layout.


Summary Table

Priority Count Categories
🔴 Critical 0
🟠 High 1 Silent production exception swallowing
🟡 Medium 0
🟢 Low 1 Missing retry deduplication guard

Recommendations

  1. Must fix before merge: Replace the debug-only chartDiag in the syncDrawingsWithConfigs catch block with FlutterError.reportError so production exceptions remain observable (Issue Phase 1 initial #1). This is a single targeted change in one location.

  2. Low-risk follow-up: Add _zeroSizeRetryScheduled guard to match the _stateResetScheduled pattern (Issue Ramin/animate_to_current_tick #2).

The main_chart.dart changes in the latest commit are correct and welcome: the null-guard on markerSeries prevents the release-mode ErrorWidget crash, and reading XAxisModel via context.watch<XAxisModel>() inside a LayoutBuilder ensures annotations track scroll/zoom changes with live closures rather than a stale capture.


Auto Fix Claude Reviews

Action Open Dashboard

View job run

@behnam-deriv behnam-deriv merged commit 7bdd28a into master Jun 25, 2026
7 checks passed
@behnam-deriv behnam-deriv deleted the fix-drawing-tools-sync-issue-mobile branch June 25, 2026 09:13
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