Skip to content

fix: stabilize home stack interaction lifecycle - #8951

Open
Puuuuup wants to merge 1 commit into
AppFlowy-IO:mainfrom
Puuuuup:agent/fix-secondary-view-lifecycle
Open

fix: stabilize home stack interaction lifecycle#8951
Puuuuup wants to merge 1 commit into
AppFlowy-IO:mainfrom
Puuuuup:agent/fix-secondary-view-lifecycle

Conversation

@Puuuuup

@Puuuuup Puuuuup commented Aug 14, 2026

Copy link
Copy Markdown

Fixes #8950

Why

The desktop home stack owns several short-lived interaction states. These include the secondary-view animation, delayed resize-handle hover, active resize drag, and selected tab state.

These states were not always cleaned up or kept in sync.

Removing a view during a delayed callback or animation could leave asynchronous work running after the view was gone. Canceling a resize gesture could leave stale resize feedback on screen. Reordering tabs could also make the selected tab disagree with the page that was actually visible.

This PR makes the lifecycle of these states explicit and keeps the rendered UI aligned with the current tab state.

What changed

  • Dispose the secondary-view animation controller
  • Handle animation cancellation with .orCancel and TickerCanceled
  • Cancel the delayed resize-handle hover callback and guard it before updating state
  • Clear resize feedback when a resize gesture is canceled
  • Use TabsState.currentIndex as the source of truth for the rendered selection
  • Preserve page identity with stable keys
  • Replace pointer-only sidebar actions with FlowyIconButton
  • Use the correct directional sidebar icon
  • Add focused widget regression tests for the affected behavior

Verification

The targeted widget tests pass.

flutter test test/widget_test/secondary_view_resizer_test.dart --no-pub

2 tests passed.

The changed resizer code and its tests also pass static analysis.

flutter analyze lib/workspace/presentation/home/secondary_view_resizer.dart test/widget_test/secondary_view_resizer_test.dart --no-pub

No issues found.

git diff --check also passes.

Full analysis of home_stack.dart reaches source analysis, but this checkout is missing 9 generated Rust and Protobuf symbols. These errors come from generated dependencies and are unrelated to the source changes in this PR.

Risk

Low to medium.

The resize behavior and visual layout are unchanged. This PR does not change the minimum width, resize hit target, animation duration, or visual dimensions.

The main behavior changes are limited to state cleanup, cancellation handling, and keeping tab selection in sync with the visible page.


PR Checklist

  • My code adheres to AppFlowy's Conventions
  • I've listed at least one issue that this PR fixes in the description above.
  • I've added a test(s) to validate changes in this PR, or this PR only contains semantic changes.
  • All existing tests are passing.

Summary by Sourcery

Stabilize the desktop home stack’s secondary view and tab interaction lifecycle, ensuring UI state stays consistent and is properly cleaned up during animations, resizing, and tab changes.

Bug Fixes:

  • Keep the selected tab in sync with the visible page by using TabsState.currentIndex as the source of truth.
  • Prevent animation-related crashes or leaks by disposing secondary view animations and handling canceled tickers safely.
  • Avoid stale delayed callbacks by canceling post-frame and hover timers when widgets are disposed or views are removed.
  • Ensure resize feedback is cleared correctly when a drag gesture is canceled, and maintain width within defined bounds.

Enhancements:

  • Introduce a dedicated SecondaryViewResizer widget with explicit minimum width and hover behavior constants.
  • Preserve secondary page identity across reordering using stable keys in the home stack.
  • Simplify sidebar toggle interactions by switching to FlowyIconButton with proper tooltip and directional icon.
  • Streamline secondary view width animation logic to avoid unnecessary Tween allocations while preserving existing visual behavior.

Tests:

  • Add focused widget tests for SecondaryViewResizer to verify delayed hover cancellation, resize bounds, and state reset on gesture cancel.

@CLAassistant

CLAassistant commented Aug 14, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@sourcery-ai

sourcery-ai Bot commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

Reviewer's Guide

This PR stabilizes the home stack’s interaction lifecycle by centralizing secondary view resizing logic, tightening animation and delayed-callback lifecycles, and making tab selection and sidebar interactions reflect the true UI state, backed by focused widget tests.

Sequence diagram for stabilized secondary view resizer interaction lifecycle

sequenceDiagram
  actor User
  participant SecondaryViewResizer
  participant ValueNotifier_width as ValueNotifier_double
  participant HoverTimer as _showHoverTimer

  User->>SecondaryViewResizer: MouseRegion.onEnter
  SecondaryViewResizer->>HoverTimer: Timer(_secondaryViewResizerHoverDelay)
  Note over HoverTimer,SecondaryViewResizer: Timer scheduled to show hover after delay

  HoverTimer-->>SecondaryViewResizer: callback
  SecondaryViewResizer->>SecondaryViewResizer: [mounted] setState(isHovered = true)

  User->>SecondaryViewResizer: MouseRegion.onExit
  SecondaryViewResizer->>HoverTimer: _showHoverTimer.cancel()
  SecondaryViewResizer->>SecondaryViewResizer: setState(isHovered = false)

  User->>SecondaryViewResizer: GestureDetector.onHorizontalDragStart
  SecondaryViewResizer->>SecondaryViewResizer: setState(isDragging = true)

  User->>SecondaryViewResizer: GestureDetector.onHorizontalDragUpdate
  SecondaryViewResizer->>SecondaryViewResizer: compute newWidth
  SecondaryViewResizer->>ValueNotifier_width: notifier.value = newWidth (>= secondaryViewMinimumWidth)

  alt drag ends normally
    User->>SecondaryViewResizer: GestureDetector.onHorizontalDragEnd
    SecondaryViewResizer->>SecondaryViewResizer: setState(isDragging = false)
  else drag is canceled
    User->>SecondaryViewResizer: GestureDetector.onHorizontalDragCancel
    SecondaryViewResizer->>SecondaryViewResizer: setState(isDragging = false)
  end

  SecondaryViewResizer->>SecondaryViewResizer: dispose()
  SecondaryViewResizer->>HoverTimer: _showHoverTimer.cancel()
Loading

File-Level Changes

Change Details Files
Align home stack tab selection and page rendering with TabsState and stable page identities.
  • Remove local selectedIndex state from HomeStack and rely on TabsState.currentIndex for both TabsManager callbacks and IndexedStack index.
  • Add ObjectKey based on PageManager to each IndexedStack child to preserve page identity across tab reordering and updates.
frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart
Harden the secondary view animation lifecycle and clean up resources on disposal.
  • Initialize AnimationController with the correct starting value derived from initial width instead of always starting at 0.
  • Introduce a dedicated CurvedAnimation field, dispose it and the AnimationController in SecondaryViewState.dispose.
  • Drive width animation via value * curveAnimation.value rather than recreating Tweens on width updates.
  • Use a named constant secondaryViewMinimumWidth instead of hard-coded 450.0 for secondary view minimum width.
  • Guard showSecondaryPluginNotifier animation transitions with .orCancel and handle TickerCanceled to avoid exceptions when animations are interrupted or the widget is disposed.
  • Keep hasSecondaryView in sync with secondaryNotifier via setState only when the underlying pluginType actually changes.
frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart
frontend/appflowy_flutter/lib/workspace/presentation/home/secondary_view_resizer.dart
Extract and stabilize secondary view resize behavior in a dedicated widget with explicit hover/dismiss lifecycle.
  • Move SecondaryViewResizer into its own file, stripping PageManager dependency and keeping only width ValueNotifier and child.
  • Introduce OverlayPortal-based resizer with MouseRegion and GestureDetector that controls hover and drag states.
  • Add a delayed hover highlight using a Timer with a shared _secondaryViewResizerHoverDelay constant and cancel the timer on exit or dispose, guarding setState with mounted.
  • Ensure drag updates respect secondaryViewMinimumWidth and maintain the last valid width even if the pointer moves away from the indicator.
  • Handle drag cancel and drag end by clearing isDragging and thus removing the primary-colored feedback bar.
frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart
frontend/appflowy_flutter/lib/workspace/presentation/home/secondary_view_resizer.dart
Replace pointer-only sidebar toggle interaction with a FlowyIconButton that supports proper tooltip and click semantics.
  • Swap FlowyTooltip+Listener+FlowyHover+Container sidebar toggle composition for a single FlowyIconButton with width, padding, rotated icon, and richTooltipText.
  • Move menu expansion logic into the FlowyIconButton.onPressed callback while preserving existing MenuStatus toggling behavior.
frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart
Make FadingIndexedStack’s post-frame opacity transition safe against removal during callbacks.
  • Wrap the setState call inside the SchedulerBinding.addPostFrameCallback in a mounted check before updating _targetOpacity back to 1.
  • Keep fading behavior unchanged while avoiding exceptions when the widget is removed between frames, such as during rapid plugin or tab changes.
frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart
Add focused widget tests validating secondary view resizer lifecycle and visual feedback states.
  • Add tests that verify the resizer cancels its delayed hover Timer when disposed, ensuring no setState-after-dispose exceptions.
  • Add tests that validate resize behavior within bounds, primary-colored feedback during active drag, width stability when the pointer moves away, and clearing of visual feedback on drag cancel.
frontend/appflowy_flutter/test/widget_test/secondary_view_resizer_test.dart

Assessment against linked issues

Issue Objective Addressed Explanation
#8950 Stabilize the secondary view and resizer interaction lifecycle so that delayed hover callbacks, resize gestures, and animations are safely canceled/cleaned up when the view is removed, and a canceled resize gesture clears its active visual state.
#8950 Keep HomeStack tab/page selection synchronized with TabsState.currentIndex and preserve the correct page identity when tabs are closed or reordered.
#8950 Improve secondary-view reopen and sidebar actions so they behave like standard accessible buttons (pointer + keyboard + focus + tooltip + semantics) and use the correct directional sidebar icon.

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

@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 1 issue

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="frontend/appflowy_flutter/lib/workspace/presentation/home/home_stack.dart" line_range="272" />
<code_context>
     widget.pageManager.showSecondaryPluginNotifier
         .removeListener(onShowSecondaryChanged);
     widget.pageManager.secondaryNotifier.removeListener(onSecondaryViewChanged);
+    curveAnimation.dispose();
+    animationController.dispose();
     widthNotifier.dispose();
</code_context>
<issue_to_address>
**issue (bug_risk):** CurvedAnimation does not implement dispose; this will not compile.

CurvedAnimation extends Animation<double> and does not define dispose(), so curveAnimation.dispose() will not compile. The AnimationController manages the animation’s lifecycle; remove curveAnimation.dispose() and keep only animationController.dispose().
</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.

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.

[Bug] Home stack tab and secondary-view interaction state can become stale

2 participants