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..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,6 +10,7 @@ class NavigationBarCustomProps { final ExprOr? overlayColor; final ExprOr? indicatorColor; final ExprOr? indicatorShape; + final ExprOr? rebuildOnEveryLoad; final List? shadow; final String? borderRadius; @@ -23,6 +24,7 @@ class NavigationBarCustomProps { this.overlayColor, this.indicatorColor, this.indicatorShape, + this.rebuildOnEveryLoad, }); 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']), + 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 276e62d2..6c961e22 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? rebuildOnEveryLoad; final List? shadow; final String? borderRadius; @@ -27,6 +28,7 @@ class NavigationBarProps { this.indicatorColor, this.indicatorShape, this.showLabels, + this.rebuildOnEveryLoad, }); 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']), + rebuildOnEveryLoad: ExprOr.fromJson(json['rebuildOnEveryLoad']), 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..83c5cfd6 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,245 @@ 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.rebuildOnEveryLoad + ?.evaluate(widget.payload.scopeContext) ?? + false; + } + + if (navBar is VWNavigationBarCustom) { + return navBar.props.rebuildOnEveryLoad + ?.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 != null && onSelect['type'] == 'loadEntity') + ? onSelect['entity'] as JsonLike? + : null; + 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 +549,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, ), );