Skip to content

Conversation

@RamSuthar-Digia
Copy link
Collaborator

…avigation handling and fixed alignment conversion

…avigation handling and fixed alignment conversion
@coderabbitai
Copy link

coderabbitai bot commented Dec 16, 2025

Walkthrough

Adds optional rebuildOnEveryLoad props for navigation bars, broadens alignment alias mapping for stack child alignment, removes per-item action execution from navigation bar selection, introduces per-item BuildContext propagation in custom navigation items, and refactors Scaffold bottom-navigation to use lazy page creation, caching, and optional page preservation.

Changes

Cohort / File(s) Summary
Alignment type converter aliases
lib/src/framework/utils/flutter_type_converters.dart
Expanded accepted alignment alias strings so topRight maps to AlignmentDirectional.topEnd and bottomRight maps to AlignmentDirectional.bottomEnd.
Navigation bar property additions
lib/src/framework/widget_props/navigation_bar_props.dart, lib/src/framework/widget_props/navigation_bar_custom_props.dart
Added optional field final ExprOr<bool>? rebuildOnEveryLoad;, added constructor parameter, and added JSON deserialization via ExprOr.fromJson<bool>(...) in both props classes.
Navigation bar action flow removal & per-item context
lib/src/framework/widgets/navigation_bar.dart, lib/src/framework/widgets/navigation_bar_custom.dart
Removed per-item action execution on destination selection; handleDestinationSelected now only invokes the external callback. navigation_bar_custom.dart wraps each destination widget with a Builder and passes a per-item BuildContext into the item payload (payload.copyWith(buildContext: ...)).
Scaffold navigation restructuring
lib/src/framework/widgets/scaffold.dart
Reworked bottom-navigation internals: added _currentIndex, lazy page creation with _buildPage and _pageCache, navigation item extraction for both bar types, _getActionForIndex, updated onDestinationSelected to execute actions or update index, and body rendering that respects preservePage (uses IndexedStack when preserving).

Possibly related PRs

Pre-merge checks and finishing touches

✅ Passed checks (3 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately summarizes the main changes: adding preservePage property and improving navigation handling with alignment conversion fixes.
Description check ✅ Passed The description is related to the changeset, mentioning preservePage property, navigation handling improvements, and alignment conversion fixes.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch bottomNavFixes

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (4)
lib/src/framework/utils/flutter_type_converters.dart (1)

441-451: Consider adding symmetric aliases for topLeft/bottomLeft.

The new aliases map topRighttopEnd and bottomRightbottomEnd, but there's no corresponding topLefttopStart or bottomLeftbottomStart. This asymmetry may confuse users expecting consistent behavior.

 static AlignmentDirectional stackChildAlignment(String? fit) => switch (fit) {
       'center' => AlignmentDirectional.center,
       'topEnd' || 'topRight' => AlignmentDirectional.topEnd,
       'topCenter' => AlignmentDirectional.topCenter,
       'centerEnd' => AlignmentDirectional.centerEnd,
       'centerStart' => AlignmentDirectional.centerStart,
-      'bottomStart' => AlignmentDirectional.bottomStart,
+      'bottomStart' || 'bottomLeft' => AlignmentDirectional.bottomStart,
       'bottomCenter' => AlignmentDirectional.bottomCenter,
       'bottomEnd' || 'bottomRight' => AlignmentDirectional.bottomEnd,
-      _ => AlignmentDirectional.topStart
+      'topStart' || 'topLeft' || _ => AlignmentDirectional.topStart
     };
lib/src/framework/widgets/scaffold.dart (3)

336-347: Consider resetting _currentIndex only when valid.

When the bottom navigation bar changes, _currentIndex is reset to 0. If the new navigation bar has fewer items and the current index is still valid, this could cause an unnecessary state reset.

 if (oldNav != newNav) {
   _navItems = _extractNavigationItems();
   _pageCache.clear();
-  _currentIndex = 0;
+  if (_currentIndex >= _navItems.length) {
+    _currentIndex = 0;
+  }
 }

408-411: Simplify type access with pattern matching.

The conditional access pattern is correct but could be cleaner. Since _extractNavigationItems already filters to specific types, this is safe, but consider using pattern matching for clarity.

 final item = _navItems[index];
-final onSelect = item is VWNavigationBarItemDefault
-    ? item.props.onSelect
-    : (item as VWNavigationBarItemCustom).props.onSelect;
+final onSelect = switch (item) {
+  VWNavigationBarItemDefault(:final props) => props.onSelect,
+  VWNavigationBarItemCustom(:final props) => props.onSelect,
+  _ => null,
+};

475-482: IndexedStack builds all pages eagerly, defeating lazy loading.

When preserve is true, List.generate calls _buildPage(i) for every index immediately, constructing all pages on first render. This negates the lazy loading benefit and may cause performance issues with many navigation items or complex pages.

Consider building only visited pages:

 if (preserve) {
   return IndexedStack(
     index: _currentIndex,
     children: List.generate(
       _navItems.length,
-      (i) => _buildPage(i),
+      (i) => _pageCache.containsKey(i) || i == _currentIndex
+          ? _buildPage(i)
+          : const SizedBox.shrink(),
     ),
   );
 }

This ensures only the current page and previously visited pages are built, while maintaining placeholder slots for unvisited pages.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 437591b and 4f22090.

📒 Files selected for processing (6)
  • lib/src/framework/utils/flutter_type_converters.dart (1 hunks)
  • lib/src/framework/widget_props/navigation_bar_custom_props.dart (3 hunks)
  • lib/src/framework/widget_props/navigation_bar_props.dart (3 hunks)
  • lib/src/framework/widgets/navigation_bar.dart (0 hunks)
  • lib/src/framework/widgets/navigation_bar_custom.dart (1 hunks)
  • lib/src/framework/widgets/scaffold.dart (2 hunks)
💤 Files with no reviewable changes (1)
  • lib/src/framework/widgets/navigation_bar.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: run-format-and-analyse
🔇 Additional comments (7)
lib/src/framework/widget_props/navigation_bar_custom_props.dart (1)

13-13: LGTM!

The preservePage property is added consistently with the existing property patterns and follows the established JSON parsing convention using ExprOr.fromJson<bool>().

Also applies to: 27-27, 39-39

lib/src/framework/widgets/navigation_bar_custom.dart (2)

43-47: LGTM!

Wrapping each navigation item with Builder to provide a per-item BuildContext is the correct pattern for ensuring proper widget tree context propagation. The payload.copyWith(buildContext: navItemContext) correctly passes this context to child widgets.


30-32: LGTM!

The simplification of handleDestinationSelected to just delegate to the callback aligns with the architectural change that moves action execution responsibility to the scaffold level.

lib/src/framework/widget_props/navigation_bar_props.dart (1)

15-15: LGTM!

The preservePage property addition is consistent with NavigationBarCustomProps and follows the established property definition and JSON parsing patterns.

Also applies to: 31-31, 45-45

lib/src/framework/widgets/scaffold.dart (3)

450-466: LGTM!

The navigation handler correctly prioritizes action execution over index updates, and the early return for same-index selection (when no action) prevents unnecessary state updates.


492-524: LGTM!

The unified bottom navigation bar builder correctly handles both VWNavigationBar and VWNavigationBarCustom types, propagating selectedIndex and onDestinationSelected while using Builder for proper context isolation.


546-551: Verify: preservePage not applied with collapsible app bar.

When isCollapsibleAppBar is true, the scaffold uses _buildCollapsibleAppBarBody instead of _buildBody(), bypassing the preservePage logic. If this is intentional, consider documenting this limitation.

int _currentIndex = 0;

late List<VirtualWidget> _navItems;
final Map<int, Widget> _pageCache = {};

Choose a reason for hiding this comment

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

This doesn't seem right. Widget caching isn't a recommended way of building this feture. Check some other approach.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
lib/src/framework/widgets/scaffold.dart (1)

320-323: Widget caching is not the recommended pattern for preserving page state.

Caching Widget instances doesn't preserve widget state—Flutter's framework manages state separately. Consider using AutomaticKeepAliveClientMixin on child pages or PageStorage/PageStorageKey for scroll positions and form data.

🧹 Nitpick comments (1)
lib/src/framework/widgets/scaffold.dart (1)

474-488: IndexedStack eagerly builds all pages, negating lazy loading.

When preserve is true, List.generate builds all pages upfront. For navigation bars with many items or expensive pages, this can cause performance issues on first render.

Consider lazy-building only visited pages:

  Widget _buildBody() {
    final preserve = _getPreservePageState();

    if (preserve) {
      return IndexedStack(
        index: _currentIndex,
        children: List.generate(
          _navItems.length,
-         (i) => _buildPage(i),
+         (i) => _pageCache.containsKey(i) || i == _currentIndex
+             ? _buildPage(i)
+             : const SizedBox.shrink(),
        ),
      );
    }

    return _buildPage(_currentIndex);
  }

This ensures only visited tabs are built while maintaining IndexedStack's state preservation.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 4f22090 and 9543f13.

📒 Files selected for processing (3)
  • lib/src/framework/widget_props/navigation_bar_custom_props.dart (3 hunks)
  • lib/src/framework/widget_props/navigation_bar_props.dart (3 hunks)
  • lib/src/framework/widgets/scaffold.dart (2 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • lib/src/framework/widget_props/navigation_bar_custom_props.dart
  • lib/src/framework/widget_props/navigation_bar_props.dart
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: run-format-and-analyse
🔇 Additional comments (6)
lib/src/framework/widgets/scaffold.dart (6)

303-313: LGTM!

Constructor is clean and properly passes all required parameters.


353-373: LGTM!

Extraction logic correctly handles both navigation bar types and safely returns an empty list for unrecognized types.


401-429: LGTM!

Page building has proper null safety with fallback to SizedBox.shrink. The cache is bounded by the number of navigation items.


435-446: LGTM!

Action detection correctly distinguishes between action-type and entity-type selections.


452-468: LGTM!

Handler correctly separates action execution from navigation. The early return optimization when index is unchanged is a good practice.


548-553: Collapsible app bar body bypasses preserve-page logic.

When isCollapsibleAppBar is true, the body uses _buildCollapsibleAppBarBody which doesn't utilize the page caching or preserve-page behavior implemented in _buildBody. If preservePage should work with collapsible app bars, this path needs to integrate the same logic.

Is this intentional? If collapsible app bars should also support page preservation, the body construction will need adjustment.

Comment on lines +336 to +347
void didUpdateWidget(covariant _ScaffoldWithBottomNav oldWidget) {
super.didUpdateWidget(oldWidget);

final oldNav = oldWidget.parent.childOf('bottomNavigationBar');
final newNav = widget.parent.childOf('bottomNavigationBar');

if (oldNav != newNav) {
_navItems = _extractNavigationItems();
_pageCache.clear();
_currentIndex = 0;
}
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Reference comparison may not reliably detect navigation bar changes.

Comparing oldNav != newNav relies on object identity. If the parent rebuilds and creates new widget instances with the same configuration, the cache will be unnecessarily cleared. Consider comparing a stable identifier (e.g., a key or hash of navigation item IDs) instead.

Also, unconditionally resetting _currentIndex = 0 when navigation changes could cause unexpected tab jumps for users.

Comment on lines +379 to +395
bool _getPreservePageState() {
final navBar = widget.parent.childOf('bottomNavigationBar');

if (navBar is VWNavigationBar) {
return navBar.props.rebuildOnEveryLoad
?.evaluate(widget.payload.scopeContext) ??
false;
}

if (navBar is VWNavigationBarCustom) {
return navBar.props.rebuildOnEveryLoad
?.evaluate(widget.payload.scopeContext) ??
false;
}

return false;
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Semantic inversion: rebuildOnEveryLoad logic appears inverted.

The method _getPreservePageState returns rebuildOnEveryLoad, but the semantics seem backwards:

  • If rebuildOnEveryLoad = true, you'd expect pages to not be preserved (rebuild each time)
  • If rebuildOnEveryLoad = false, you'd expect pages to be preserved

However, _buildBody uses preserve = true to enable IndexedStack (which preserves pages). This means when rebuildOnEveryLoad = true, pages are preserved—opposite of what the name suggests.

Either rename the property to preservePageState or invert the logic:

  bool _getPreservePageState() {
    final navBar = widget.parent.childOf('bottomNavigationBar');

    if (navBar is VWNavigationBar) {
-     return navBar.props.rebuildOnEveryLoad
+     return !(navBar.props.rebuildOnEveryLoad
              ?.evaluate(widget.payload.scopeContext) ??
-         false;
+         true);
    }
    // ... similar for VWNavigationBarCustom
  }

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In lib/src/framework/widgets/scaffold.dart around lines 379 to 395, the method
_getPreservePageState currently returns the raw value of rebuildOnEveryLoad
which inverts semantics (true means rebuild, so pages should NOT be preserved);
change the method to return the negation of rebuildOnEveryLoad (i.e., preserve =
!(rebuildOnEveryLoad?.evaluate(...) ?? false)) for both VWNavigationBar and
VWNavigationBarCustom branches so preservePageState correctly reflects that
rebuildOnEveryLoad = true disables preservation.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (4)
lib/src/framework/widgets/scaffold.dart (4)

320-333: Widget caching concern already flagged.

The _pageCache approach for widget caching has been flagged in a previous review. Widget caching can lead to stale state, incorrect context, or lifecycle issues when the cached widgets don't rebuild with updated data.


336-347: Reference comparison concern already flagged.

The concern about oldNav != newNav using object identity comparison has been raised in a previous review. This can cause unnecessary cache clears or miss actual changes depending on how parent widgets rebuild.


379-395: Semantic inversion concern already flagged.

The inverted semantics between rebuildOnEveryLoad and _getPreservePageState has been raised in a previous review. When rebuildOnEveryLoad = true, the current logic preserves pages (via IndexedStack), which is the opposite of what the property name implies.


401-431: Cached pages may use stale BuildContext.

Pages are built using widget.payload.buildContext and then cached. When the widget rebuilds, the cached pages retain references to the old context, which can become invalid and cause looking up a deactivated widget's ancestor errors.

Consider either:

  1. Not caching widget instances, but caching the data/state needed to build them
  2. Using context from the current build method via a Builder pattern
  Widget _buildPage(int index) {
-   if (_pageCache.containsKey(index)) {
-     return _pageCache[index]!;
-   }
-
    if (index >= _navItems.length) {
      return const SizedBox.shrink();
    }

    final item = _navItems[index];
    final onSelect = item is VWNavigationBarItemDefault
        ? item.props.onSelect
        : (item as VWNavigationBarItemCustom).props.onSelect;

    final entity = (onSelect != null && onSelect['type'] == 'loadEntity')
        ? onSelect['entity'] as JsonLike?
        : null;
    final entityId = as$<String?>(entity?['id']);
    final args = entity?['args'] as JsonLike?;

-   final page = entityId != null
+   return entityId != null
        ? DefaultActionExecutor.of(widget.payload.buildContext).viewBuilder(
            widget.payload.buildContext,
            entityId,
            args,
          )
        : const SizedBox.shrink();
-
-   _pageCache[index] = page;
-   return page;
  }

Note: This removes caching entirely. If caching is required for performance, consider caching entity metadata instead and rebuilding widgets each time, or use AutomaticKeepAliveClientMixin with a PageView/TabBarView approach.

🧹 Nitpick comments (1)
lib/src/framework/widgets/scaffold.dart (1)

476-490: IndexedStack builds all pages upfront.

When preserve is true, IndexedStack instantiates all page widgets regardless of which tab is selected. With many navigation items or heavy pages, this can cause:

  • Slower initial load time
  • Increased memory usage
  • All pages running their lifecycle simultaneously

Consider lazy initialization where pages are only built when first visited:

  Widget _buildBody() {
    final preserve = _getPreservePageState();

    if (preserve) {
      return IndexedStack(
        index: _currentIndex,
        children: List.generate(
          _navItems.length,
-         (i) => _buildPage(i),
+         (i) => _visitedIndices.contains(i) ? _buildPage(i) : const SizedBox.shrink(),
        ),
      );
    }

    return _buildPage(_currentIndex);
  }

You'd need to track visited indices and add the current index to _visitedIndices in onDestinationSelected.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9543f13 and 5ff2c18.

📒 Files selected for processing (1)
  • lib/src/framework/widgets/scaffold.dart (2 hunks)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: run-format-and-analyse
🔇 Additional comments (5)
lib/src/framework/widgets/scaffold.dart (5)

353-373: LGTM!

The navigation item extraction logic correctly handles both VWNavigationBar and VWNavigationBarCustom types with appropriate type filtering.


437-448: LGTM!

The action detection logic correctly identifies action-type navigation items and returns null for entity-based navigation.


454-470: LGTM!

The tap handler correctly separates action execution from page navigation. Action items execute without changing the selected index, while entity items update the index only when changed.


496-528: LGTM!

The navigation bar builder correctly creates new instances with the current index and callback, using a Builder to ensure fresh context for the navigation bar widget.


550-555: Collapsible app bar bypasses navigation-aware body building.

When isCollapsibleAppBar is true, the body uses _buildCollapsibleAppBarBody from the parent widget, which doesn't use the new _buildBody() logic with preserve-page-state or page caching. This means:

  • Tabs won't preserve state when switching in collapsible mode
  • The rebuildOnEveryLoad setting has no effect

If this is intentional, consider documenting the limitation. Otherwise, the collapsible body should integrate with the navigation-aware body building.

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.

2 participants