From 4f2209012f1fb553e6e856234eaa868a94b27771 Mon Sep 17 00:00:00 2001 From: RamSuthar-Digia Date: Tue, 16 Dec 2025 12:37:45 +0530 Subject: [PATCH 1/3] feat: add preservePage property to navigation bar props and improve navigation handling and fixed alignment conversion --- .../utils/flutter_type_converters.dart | 4 +- .../navigation_bar_custom_props.dart | 3 + .../widget_props/navigation_bar_props.dart | 3 + lib/src/framework/widgets/navigation_bar.dart | 12 - .../widgets/navigation_bar_custom.dart | 17 +- lib/src/framework/widgets/scaffold.dart | 325 ++++++++++++------ 6 files changed, 232 insertions(+), 132 deletions(-) diff --git a/lib/src/framework/utils/flutter_type_converters.dart b/lib/src/framework/utils/flutter_type_converters.dart index ab568a09..00c3975b 100644 --- a/lib/src/framework/utils/flutter_type_converters.dart +++ b/lib/src/framework/utils/flutter_type_converters.dart @@ -440,13 +440,13 @@ abstract class To { static AlignmentDirectional stackChildAlignment(String? fit) => switch (fit) { 'center' => AlignmentDirectional.center, - 'topEnd' => AlignmentDirectional.topEnd, + 'topEnd' || 'topRight' => AlignmentDirectional.topEnd, 'topCenter' => AlignmentDirectional.topCenter, 'centerEnd' => AlignmentDirectional.centerEnd, 'centerStart' => AlignmentDirectional.centerStart, 'bottomStart' => AlignmentDirectional.bottomStart, 'bottomCenter' => AlignmentDirectional.bottomCenter, - 'bottomEnd' => AlignmentDirectional.bottomEnd, + 'bottomEnd' || 'bottomRight' => AlignmentDirectional.bottomEnd, _ => AlignmentDirectional.topStart }; diff --git a/lib/src/framework/widget_props/navigation_bar_custom_props.dart b/lib/src/framework/widget_props/navigation_bar_custom_props.dart index e3be6b31..f8d01627 100644 --- a/lib/src/framework/widget_props/navigation_bar_custom_props.dart +++ b/lib/src/framework/widget_props/navigation_bar_custom_props.dart @@ -10,6 +10,7 @@ class NavigationBarCustomProps { final ExprOr? overlayColor; final ExprOr? indicatorColor; final ExprOr? indicatorShape; + final ExprOr? preservePage; final List? shadow; final String? borderRadius; @@ -23,6 +24,7 @@ class NavigationBarCustomProps { this.overlayColor, this.indicatorColor, this.indicatorShape, + this.preservePage, }); factory NavigationBarCustomProps.fromJson(JsonLike json) { @@ -34,6 +36,7 @@ class NavigationBarCustomProps { overlayColor: ExprOr.fromJson(json['overlayColor']), indicatorColor: ExprOr.fromJson(json['indicatorColor']), indicatorShape: ExprOr.fromJson(json['indicatorShape']), + preservePage: ExprOr.fromJson(json['preservePage']), borderRadius: as$(json['borderRadius']), shadow: as$?>(json['shadow']), ); diff --git a/lib/src/framework/widget_props/navigation_bar_props.dart b/lib/src/framework/widget_props/navigation_bar_props.dart index 276e62d2..6377c994 100644 --- a/lib/src/framework/widget_props/navigation_bar_props.dart +++ b/lib/src/framework/widget_props/navigation_bar_props.dart @@ -12,6 +12,7 @@ class NavigationBarProps { final ExprOr? indicatorColor; final ExprOr? indicatorShape; final ExprOr? showLabels; + final ExprOr? preservePage; final List? shadow; final String? borderRadius; @@ -27,6 +28,7 @@ class NavigationBarProps { this.indicatorColor, this.indicatorShape, this.showLabels, + this.preservePage, }); factory NavigationBarProps.fromJson(JsonLike json) { @@ -40,6 +42,7 @@ class NavigationBarProps { indicatorColor: ExprOr.fromJson(json['indicatorColor']), indicatorShape: ExprOr.fromJson(json['indicatorShape']), showLabels: ExprOr.fromJson(json['showLabels']), + preservePage: ExprOr.fromJson(json['preservePage']), borderRadius: as$(json['borderRadius']), shadow: as$?>(json['shadow']), ); diff --git a/lib/src/framework/widgets/navigation_bar.dart b/lib/src/framework/widgets/navigation_bar.dart index f1212c5e..b3915574 100644 --- a/lib/src/framework/widgets/navigation_bar.dart +++ b/lib/src/framework/widgets/navigation_bar.dart @@ -1,6 +1,5 @@ import 'package:flutter/material.dart'; -import '../actions/base/action_flow.dart'; import '../base/virtual_stateless_widget.dart'; import '../internal_widgets/bottom_navigation_bar/bottom_navigation_bar.dart' as internal; @@ -26,17 +25,6 @@ class VWNavigationBar extends VirtualStatelessWidget { }); void handleDestinationSelected(int index, RenderPayload payload) { - final selectedChild = children?.elementAt(index); - if (selectedChild is VWNavigationBarItemDefault) { - final onPageSelected = selectedChild.props.onSelect; - final onPageSelectedAction = onPageSelected?['action']; - if (onPageSelectedAction != null) { - payload.executeAction( - ActionFlow.fromJson(onPageSelectedAction), - triggerType: 'onPageSelected', - ); - } - } onDestinationSelected?.call(index); } diff --git a/lib/src/framework/widgets/navigation_bar_custom.dart b/lib/src/framework/widgets/navigation_bar_custom.dart index db6cf47c..f8ae37e1 100644 --- a/lib/src/framework/widgets/navigation_bar_custom.dart +++ b/lib/src/framework/widgets/navigation_bar_custom.dart @@ -28,17 +28,6 @@ class VWNavigationBarCustom }); void handleDestinationSelected(int index, RenderPayload payload) { - final selectedChild = children?.elementAt(index); - if (selectedChild is VWNavigationBarItemCustom) { - final onPageSelected = selectedChild.props.onSelect; - final onPageSelectedAction = onPageSelected?['action']; - if (onPageSelectedAction != null) { - payload.executeAction( - ActionFlow.fromJson(onPageSelectedAction), - triggerType: 'onPageSelected', - ); - } - } onDestinationSelected?.call(index); } @@ -51,7 +40,11 @@ class VWNavigationBarCustom destinations.add( InheritedNavigationBarController( itemIndex: i, - child: navItems[i].toWidget(payload), + child: Builder(builder: (navItemContext) { + return navItems[i].toWidget(payload.copyWith( + buildContext: navItemContext, + )); + }), ), ); } diff --git a/lib/src/framework/widgets/scaffold.dart b/lib/src/framework/widgets/scaffold.dart index b0b8d31f..61dd377f 100644 --- a/lib/src/framework/widgets/scaffold.dart +++ b/lib/src/framework/widgets/scaffold.dart @@ -287,71 +287,6 @@ class VWScaffold extends VirtualStatelessWidget { ); }); } - - Widget? _buildBodyWithNavBar( - RenderPayload payload, int bottomNavBarIndex, bool enableSafeArea) { - final bottomNavBar = childOf('bottomNavigationBar'); - if (bottomNavBar is! VWNavigationBar && - bottomNavBar is! VWNavigationBarCustom) { - return null; - } - - final isDefaultNavBar = bottomNavBar is VWNavigationBar; - final navigationItems = isDefaultNavBar - ? bottomNavBar - .childrenOf('children') - ?.whereType() - .toList() - : (bottomNavBar as VWNavigationBarCustom) - .childrenOf('children') - ?.whereType() - .toList(); - - if (navigationItems == null || navigationItems.isEmpty) return null; - - final entityIds = navigationItems.map((item) { - if (isDefaultNavBar) { - return as$(((item as VWNavigationBarItemDefault) - .props - .onSelect?['entity'] as JsonLike?)?['id']); - } else { - return as$(((item as VWNavigationBarItemCustom) - .props - .onSelect?['entity'] as JsonLike?)?['id']); - } - }).toList(); - - if (entityIds.isEmpty || bottomNavBarIndex >= entityIds.length) return null; - final currentEntityId = entityIds[bottomNavBarIndex]; - if (currentEntityId == null) return null; - - final currentItem = navigationItems.firstWhere((item) { - if (isDefaultNavBar) { - return ((item as VWNavigationBarItemDefault).props.onSelect?['entity'] - as JsonLike?)?['id'] == - currentEntityId; - } else { - return ((item as VWNavigationBarItemCustom).props.onSelect?['entity'] - as JsonLike?)?['id'] == - currentEntityId; - } - }); - - final currentEntityArgs = isDefaultNavBar - ? (((currentItem as VWNavigationBarItemDefault) - .props - .onSelect?['entity'] as JsonLike?)?['args'] as JsonLike?) - : (((currentItem as VWNavigationBarItemCustom).props.onSelect?['entity'] - as JsonLike?)?['args'] as JsonLike?); - - final Widget entity = - DefaultActionExecutor.of(payload.buildContext).viewBuilder( - payload.buildContext, - currentEntityId, - currentEntityArgs, - ); - return enableSafeArea ? SafeArea(child: entity) : entity; - } } class _ScaffoldWithBottomNav extends StatefulWidget { @@ -365,64 +300,241 @@ class _ScaffoldWithBottomNav extends StatefulWidget { final VWScaffold parent; final bool resizeToAvoidBottomInset; - const _ScaffoldWithBottomNav( - {required this.appBarWidget, - required this.drawer, - required this.endDrawer, - required this.persistentFooterButtons, - required this.isCollapsibleAppBar, - required this.enableSafeArea, - required this.payload, - required this.parent, - required this.resizeToAvoidBottomInset}); + const _ScaffoldWithBottomNav({ + required this.appBarWidget, + required this.drawer, + required this.endDrawer, + required this.persistentFooterButtons, + required this.isCollapsibleAppBar, + required this.enableSafeArea, + required this.payload, + required this.parent, + required this.resizeToAvoidBottomInset, + }); @override State<_ScaffoldWithBottomNav> createState() => _ScaffoldWithBottomNavState(); } class _ScaffoldWithBottomNavState extends State<_ScaffoldWithBottomNav> { - int bottomNavBarIndex = 0; + int _currentIndex = 0; + + late List _navItems; + final Map _pageCache = {}; + + // ------------------------------------------------------------ + // Lifecycle + // ------------------------------------------------------------ + + @override + void initState() { + super.initState(); + _navItems = _extractNavigationItems(); + } + + @override + 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; + } + } + + // ------------------------------------------------------------ + // Navigation extraction + // ------------------------------------------------------------ + + List _extractNavigationItems() { + final navBar = widget.parent.childOf('bottomNavigationBar'); + + if (navBar is VWNavigationBar) { + return navBar + .childrenOf('children') + ?.whereType() + .toList() ?? + []; + } + + if (navBar is VWNavigationBarCustom) { + return navBar + .childrenOf('children') + ?.whereType() + .toList() ?? + []; + } + + return []; + } + + // ------------------------------------------------------------ + // Preserve page flag + // ------------------------------------------------------------ + + bool _getPreservePageState() { + final navBar = widget.parent.childOf('bottomNavigationBar'); + + if (navBar is VWNavigationBar) { + return navBar.props.preservePage?.evaluate(widget.payload.scopeContext) ?? + false; + } + + if (navBar is VWNavigationBarCustom) { + return navBar.props.preservePage?.evaluate(widget.payload.scopeContext) ?? + false; + } + + return false; + } + + // ------------------------------------------------------------ + // Page creation (lazy + cached) + // ------------------------------------------------------------ + + 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?['entity'] as JsonLike?; + final entityId = as$(entity?['id']); + final args = entity?['args'] as JsonLike?; + + final page = entityId != null + ? DefaultActionExecutor.of(widget.payload.buildContext).viewBuilder( + widget.payload.buildContext, + entityId, + args, + ) + : const SizedBox.shrink(); + + _pageCache[index] = page; + return page; + } + + // ------------------------------------------------------------ + // Action detection + // ------------------------------------------------------------ + + dynamic _getActionForIndex(int index) { + if (index >= _navItems.length) return null; + + final item = _navItems[index]; + final onSelect = item is VWNavigationBarItemDefault + ? item.props.onSelect + : (item as VWNavigationBarItemCustom).props.onSelect; + + return (onSelect != null && onSelect['type'] == 'action') + ? onSelect['action'] + : null; + } + + // ------------------------------------------------------------ + // Bottom nav tap handler (ONLY place actions execute) + // ------------------------------------------------------------ void onDestinationSelected(int index) { + final action = _getActionForIndex(index); + + if (action != null) { + widget.payload.executeAction( + ActionFlow.fromJson(action), + triggerType: 'onPageSelected', + ); + return; + } + + if (index == _currentIndex) return; + setState(() { - bottomNavBarIndex = index; + _currentIndex = index; }); } - Widget? buildBottomNavigationBar(RenderPayload payload) { - final child = widget.parent.childOf('bottomNavigationBar'); - if (child == null) return null; - if (child is! VWNavigationBar && child is! VWNavigationBarCustom) { - return null; - } + // ------------------------------------------------------------ + // Body builder (pure) + // ------------------------------------------------------------ - if (child is VWNavigationBarCustom) { - return VWNavigationBarCustom( - props: child.props, - commonProps: child.commonProps, - parent: widget.parent, - childGroups: child.childGroups, - selectedIndex: bottomNavBarIndex, - onDestinationSelected: onDestinationSelected, - ).toWidget(payload); - } else { - child as VWNavigationBar; - return VWNavigationBar( - props: child.props, - commonProps: child.commonProps, - parent: widget.parent, - childGroups: child.childGroups, - selectedIndex: bottomNavBarIndex, - onDestinationSelected: onDestinationSelected, - ).toWidget(payload); + Widget _buildBody() { + final preserve = _getPreservePageState(); + + if (preserve) { + return IndexedStack( + index: _currentIndex, + children: List.generate( + _navItems.length, + (i) => _buildPage(i), + ), + ); } + + return _buildPage(_currentIndex); + } + + // ------------------------------------------------------------ + // Bottom Navigation Bar + // ------------------------------------------------------------ + + Widget? buildBottomNavigationBar(RenderPayload payload) { + final navBar = widget.parent.childOf('bottomNavigationBar'); + + if (navBar == null) return null; + + return Builder(builder: (context) { + final updatedPayload = payload.copyWith(buildContext: context); + + if (navBar is VWNavigationBarCustom) { + return VWNavigationBarCustom( + props: navBar.props, + commonProps: navBar.commonProps, + parent: widget.parent, + childGroups: navBar.childGroups, + selectedIndex: _currentIndex, + onDestinationSelected: onDestinationSelected, + ).toWidget(updatedPayload); + } + + if (navBar is VWNavigationBar) { + return VWNavigationBar( + props: navBar.props, + commonProps: navBar.commonProps, + parent: widget.parent, + childGroups: navBar.childGroups, + selectedIndex: _currentIndex, + onDestinationSelected: onDestinationSelected, + ).toWidget(updatedPayload); + } + + return const SizedBox.shrink(); + }); } + // ------------------------------------------------------------ + // Build + // ------------------------------------------------------------ + @override Widget build(BuildContext context) { + final body = + widget.enableSafeArea ? SafeArea(child: _buildBody()) : _buildBody(); + return InheritedScaffoldController( + currentIndex: _currentIndex, setCurrentIndex: onDestinationSelected, - currentIndex: bottomNavBarIndex, child: Scaffold( resizeToAvoidBottomInset: widget.resizeToAvoidBottomInset, appBar: widget.isCollapsibleAppBar @@ -433,9 +545,10 @@ class _ScaffoldWithBottomNavState extends State<_ScaffoldWithBottomNav> { bottomNavigationBar: buildBottomNavigationBar(widget.payload), body: widget.isCollapsibleAppBar ? widget.parent._buildCollapsibleAppBarBody( - widget.payload, widget.enableSafeArea) - : widget.parent._buildBodyWithNavBar( - widget.payload, bottomNavBarIndex, widget.enableSafeArea), + widget.payload, + widget.enableSafeArea, + ) + : body, persistentFooterButtons: widget.persistentFooterButtons, ), ); From 9543f13ef93c8f74c6f8184622d05660840db10e Mon Sep 17 00:00:00 2001 From: RamSuthar-Digia Date: Tue, 16 Dec 2025 13:50:36 +0530 Subject: [PATCH 2/3] updated key name --- .../framework/widget_props/navigation_bar_custom_props.dart | 6 +++--- lib/src/framework/widget_props/navigation_bar_props.dart | 6 +++--- lib/src/framework/widgets/scaffold.dart | 6 ++++-- 3 files changed, 10 insertions(+), 8 deletions(-) diff --git a/lib/src/framework/widget_props/navigation_bar_custom_props.dart b/lib/src/framework/widget_props/navigation_bar_custom_props.dart index f8d01627..7b841145 100644 --- a/lib/src/framework/widget_props/navigation_bar_custom_props.dart +++ b/lib/src/framework/widget_props/navigation_bar_custom_props.dart @@ -10,7 +10,7 @@ class NavigationBarCustomProps { final ExprOr? overlayColor; final ExprOr? indicatorColor; final ExprOr? indicatorShape; - final ExprOr? preservePage; + final ExprOr? rebuildOnEveryLoad; final List? shadow; final String? borderRadius; @@ -24,7 +24,7 @@ class NavigationBarCustomProps { this.overlayColor, this.indicatorColor, this.indicatorShape, - this.preservePage, + this.rebuildOnEveryLoad, }); factory NavigationBarCustomProps.fromJson(JsonLike json) { @@ -36,7 +36,7 @@ class NavigationBarCustomProps { overlayColor: ExprOr.fromJson(json['overlayColor']), indicatorColor: ExprOr.fromJson(json['indicatorColor']), indicatorShape: ExprOr.fromJson(json['indicatorShape']), - preservePage: ExprOr.fromJson(json['preservePage']), + rebuildOnEveryLoad: ExprOr.fromJson(json['rebuildOnEveryLoad']), borderRadius: as$(json['borderRadius']), shadow: as$?>(json['shadow']), ); diff --git a/lib/src/framework/widget_props/navigation_bar_props.dart b/lib/src/framework/widget_props/navigation_bar_props.dart index 6377c994..6c961e22 100644 --- a/lib/src/framework/widget_props/navigation_bar_props.dart +++ b/lib/src/framework/widget_props/navigation_bar_props.dart @@ -12,7 +12,7 @@ class NavigationBarProps { final ExprOr? indicatorColor; final ExprOr? indicatorShape; final ExprOr? showLabels; - final ExprOr? preservePage; + final ExprOr? rebuildOnEveryLoad; final List? shadow; final String? borderRadius; @@ -28,7 +28,7 @@ class NavigationBarProps { this.indicatorColor, this.indicatorShape, this.showLabels, - this.preservePage, + this.rebuildOnEveryLoad, }); factory NavigationBarProps.fromJson(JsonLike json) { @@ -42,7 +42,7 @@ class NavigationBarProps { indicatorColor: ExprOr.fromJson(json['indicatorColor']), indicatorShape: ExprOr.fromJson(json['indicatorShape']), showLabels: ExprOr.fromJson(json['showLabels']), - preservePage: ExprOr.fromJson(json['preservePage']), + rebuildOnEveryLoad: ExprOr.fromJson(json['rebuildOnEveryLoad']), borderRadius: as$(json['borderRadius']), shadow: as$?>(json['shadow']), ); diff --git a/lib/src/framework/widgets/scaffold.dart b/lib/src/framework/widgets/scaffold.dart index 61dd377f..0f26566d 100644 --- a/lib/src/framework/widgets/scaffold.dart +++ b/lib/src/framework/widgets/scaffold.dart @@ -380,12 +380,14 @@ class _ScaffoldWithBottomNavState extends State<_ScaffoldWithBottomNav> { final navBar = widget.parent.childOf('bottomNavigationBar'); if (navBar is VWNavigationBar) { - return navBar.props.preservePage?.evaluate(widget.payload.scopeContext) ?? + return navBar.props.rebuildOnEveryLoad + ?.evaluate(widget.payload.scopeContext) ?? false; } if (navBar is VWNavigationBarCustom) { - return navBar.props.preservePage?.evaluate(widget.payload.scopeContext) ?? + return navBar.props.rebuildOnEveryLoad + ?.evaluate(widget.payload.scopeContext) ?? false; } From 5ff2c18cf13b921b791b7941ed8ebee62007aefc Mon Sep 17 00:00:00 2001 From: RamSuthar-Digia Date: Tue, 16 Dec 2025 15:24:04 +0530 Subject: [PATCH 3/3] updated type checking --- lib/src/framework/widgets/scaffold.dart | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/lib/src/framework/widgets/scaffold.dart b/lib/src/framework/widgets/scaffold.dart index 0f26566d..83c5cfd6 100644 --- a/lib/src/framework/widgets/scaffold.dart +++ b/lib/src/framework/widgets/scaffold.dart @@ -412,7 +412,9 @@ class _ScaffoldWithBottomNavState extends State<_ScaffoldWithBottomNav> { ? item.props.onSelect : (item as VWNavigationBarItemCustom).props.onSelect; - final entity = onSelect?['entity'] as JsonLike?; + final entity = (onSelect != null && onSelect['type'] == 'loadEntity') + ? onSelect['entity'] as JsonLike? + : null; final entityId = as$(entity?['id']); final args = entity?['args'] as JsonLike?;