diff --git a/CHANGELOG.md b/CHANGELOG.md index f3b50ee..b9da6df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,56 @@ # Changelog +## 2.0.0 + +### Breaking Changes +- **Builder signature updated**: `PlayxRoute` and `PlayxShellBranch` now use `PlayxRouteWidgetBuilder` which includes an `isInitialized` parameter: + ```dart + // Before (1.x): + builder: (context, state) => MyPage() + // After (2.0): + builder: (context, state, isInitialized) => MyPage() + ``` + +### New Features +- **Shell Builder**: Added `shellBuilder` parameter to `PlayxRoute`, `PlayxShellBranch`, and `PlayxPageConfig`. The shell (AppBar, Drawer, Scaffold) renders immediately during navigation transitions, preventing blank frames. Only the body content waits for the binding's `onEnter` to complete. + ```dart + PlayxRoute( + path: '/channels', + shellBuilder: (context, state, isInitialized, child) => Scaffold( + appBar: AppBar(title: Text('Channels')), + drawer: MyDrawer(), + body: child, + ), + builder: (context, state, isInitialized) => ChannelsListView(), + binding: ChannelsBinding(), + ) + ``` +- **Non-blocking initialization**: Added `waitForBinding` parameter to `PlayxRoute`, `PlayxShellBranch`, and `PlayxPageConfig`. When set to `false`, the page renders immediately with `isInitialized = false` while `onEnter` runs in the background. +- **Global page configuration via `PlayxPageConfig`**: Added `config` parameter to `PlayxNavigationBuilder` to set global defaults for `loadingWidget`, `waitForBinding`, and `shellBuilder`. Individual routes can override any of these settings. + ```dart + PlayxNavigationBuilder( + router: router, + config: PlayxPageConfig( + loadingWidget: Center(child: CircularProgressIndicator()), + waitForBinding: false, + shellBuilder: (context, state, isInitialized, child) => Scaffold( + appBar: AppBar(title: Text('My App')), + body: child, + ), + ), + builder: (context) => MyApp(), + ) + ``` +- **New typedefs**: `PlayxRouteWidgetBuilder` and `PlayxShellWidgetBuilder` for type-safe builder signatures. +- **Initialization transition animation**: Added `initTransitionDuration` parameter to `PlayxRoute`, `PlayxShellBranch`, and `PlayxPageConfig`. When set, an `AnimatedSwitcher` crossfade smoothly transitions from the loading widget to the page content. + +### Configuration Resolution +Route-level parameter → Global `PlayxPageConfig` → Built-in default: +- `loadingWidget`: Route > Global > `SizedBox.shrink()` +- `waitForBinding`: Route > Global > `true` +- `shellBuilder`: Route > Global > `null` +- `initTransitionDuration`: Route > Global > `null` (no animation) + ## 1.0.0 ### New Features diff --git a/README.md b/README.md index 6b8b47f..9c19e8f 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,9 @@ - **App Initialization Lifecycle**: Register app-level dependencies (repositories, datasources, services) via `onInitApp` in your bindings, called automatically during boot. - **Initialization Awaiting**: Use `PlayxNavigation.ensureInitialized` to gate startup logic until all bindings are initialized. - **Binding Registry**: Access any registered binding by type via `PlayxNavigation.findBinding()`. +- **Shell Builder**: Render page chrome (AppBar, Drawer, Scaffold) immediately during transitions — no more blank frames while bindings initialize. +- **Non-Blocking Initialization**: Optionally render pages immediately with `waitForBinding: false`, letting the builder react to `isInitialized` state. +- **Global Page Configuration**: Set default loading widgets, shell builders, and initialization behavior for all routes via `PlayxPageConfig`. - **Advanced Route Configuration**: Fine-tune the behavior of your routes with extensive configuration options, including custom transitions, modal behavior, and state management. - **Route Management**: Easily navigate to routes, replace routes, and handle navigation stacks without the need for buildcontext. - **Custom Page Transitions**: Use predefined transitions or create your own to enhance the user experience. @@ -20,7 +23,7 @@ Add `Playx Navigation` to your `pubspec.yaml`: ```yaml dependencies: - playx_navigation: ^0.0.1 + playx_navigation: ^2.0.0 ``` Then, run: @@ -83,19 +86,19 @@ final router = GoRouter( PlayxRoute( path: Paths.home, name: Routes.home, - builder: (context, state) => const HomePage(), + builder: (context, state, isInitialized) => const HomePage(), binding: HomeBinding(), ), PlayxRoute( path: Paths.products, name: Routes.products, - builder: (context, state) => ProductsPage(), + builder: (context, state, isInitialized) => ProductsPage(), binding: ProductsBinding(), routes: [ PlayxRoute( path: Paths.details, name: Routes.details, - builder: (context, state) => + builder: (context, state, isInitialized) => ProductDetailsPage(product: state.extra as Product), binding: DetailsBinding(), ), @@ -116,7 +119,10 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return PlayxNavigationBuilder( - router: router, + router: router, + config: PlayxPageConfig( + loadingWidget: Center(child: CircularProgressIndicator()), + ), builder: (context) { return MaterialApp.router( title: 'Playx', @@ -279,8 +285,9 @@ After initialization, all discovered `PlayxBinding` instances are stored and can - **App Initialization:** Register app-level dependencies (repositories, datasources, services) via `onInitApp`, called once at startup. - **Widget-Lifecycle Driven:** `onEnter` fires from `PlayxPage.initState` (once on mount), `onExit` fires from `PlayxPage.dispose` (only when truly removed from the tree). -- **Initialization Blocking:** The route's child widget is only built after its binding's `onEnter` completes. This guarantees any dependencies registered in `onEnter` (like GetX controllers) are available during the first build. -- **Custom Loading Widget:** Each route can provide a `loadingWidget` to be displayed while `onEnter` is initializing (defaults to `SizedBox.shrink()`). +- **Initialization Blocking:** By default, the route's child widget is only built after its binding's `onEnter` completes. This guarantees any dependencies registered in `onEnter` (like GetX controllers) are available during the first build. Set `waitForBinding: false` to render immediately. +- **Shell Builder:** Provide a `shellBuilder` to render page chrome (AppBar, Drawer, Scaffold) immediately during transitions while only the body waits for initialization. +- **Custom Loading Widget:** Each route can provide a `loadingWidget` to be displayed while `onEnter` is initializing (defaults to `SizedBox.shrink()`). Set a global default via `PlayxPageConfig.loadingWidget`. - **Route-Change Driven:** `onHidden` and `onReEnter` fire from the route-change listener when the top route changes. - **Shell Route Aware:** In `StatefulShellRoute`, branch switching fires `onHidden`/`onReEnter` (not `onExit`/`onEnter`) because the widget stays alive in its branch. - **Binding Access:** Retrieve any binding by type via `PlayxNavigation.findBinding()` after initialization. @@ -370,6 +377,95 @@ class MyRouteBinding extends PlayxBinding { By extending `PlayxBinding`, you can efficiently manage both app-level initialization and route-specific lifecycle, ensuring that resources are used optimally. +## Global Page Configuration + +### `PlayxPageConfig` + +Provide a `PlayxPageConfig` to `PlayxNavigationBuilder` to set global defaults for all routes. Individual route parameters override these globals. + +```dart +PlayxNavigationBuilder( + router: router, + config: PlayxPageConfig( + // Default loading widget for all routes: + loadingWidget: Center(child: CircularProgressIndicator()), + // Don't block page build by default: + waitForBinding: false, + // Global shell for persistent AppBar: + shellBuilder: (context, state, isInitialized, child) => Scaffold( + appBar: AppBar(title: Text('My App')), + body: child, + ), + ), + builder: (context) => MyApp(), +) +``` + +**Resolution order:** Route-level parameter → Global `PlayxPageConfig` → Built-in default. + +| Setting | Route Param | Global Config | Default | +|---|---|---|---| +| Loading widget | `PlayxRoute.loadingWidget` | `PlayxPageConfig.loadingWidget` | `SizedBox.shrink()` | +| Wait for binding | `PlayxRoute.waitForBinding` | `PlayxPageConfig.waitForBinding` | `true` | +| Shell builder | `PlayxRoute.shellBuilder` | `PlayxPageConfig.shellBuilder` | `null` | +| Init transition | `PlayxRoute.initTransitionDuration` | `PlayxPageConfig.initTransitionDuration` | `null` (no animation) | + +### Shell Builder + +The `shellBuilder` renders page chrome (AppBar, Drawer, Scaffold) **immediately** during navigation transitions. Only the body content waits for the binding's `onEnter` to complete, preventing blank frames. + +```dart +PlayxRoute( + path: '/channels', + name: 'channels', + shellBuilder: (context, state, isInitialized, child) => Scaffold( + appBar: AppBar(title: Text('Channels')), + drawer: isInitialized ? MyDrawer() : null, + body: child, // loading widget or actual content + ), + builder: (context, state, isInitialized) => ChannelsListView(), + binding: ChannelsBinding(), +) +``` + +### Non-Blocking Initialization + +Set `waitForBinding: false` to render the page immediately while `onEnter` runs in the background. The builder receives `isInitialized` so it can handle its own loading state: + +```dart +PlayxRoute( + path: '/profile', + name: 'profile', + builder: (context, state, isInitialized) { + if (!isInitialized) return ProfileSkeleton(); + return ProfilePage(); + }, + binding: ProfileBinding(), + waitForBinding: false, +) +``` + +### Initialization Animation + +Set `initTransitionDuration` to smoothly crossfade from the loading widget to the page content using an `AnimatedSwitcher`: + +```dart +// Global: apply to all routes +PlayxPageConfig( + initTransitionDuration: Duration(milliseconds: 300), +) + +// Per-route: override for a specific route +PlayxRoute( + path: '/dashboard', + builder: (context, state, isInitialized) => DashboardPage(), + binding: DashboardBinding(), + initTransitionDuration: Duration(milliseconds: 500), +) +``` + +Set to `Duration.zero` to explicitly disable animation for a specific route when a global duration is set. + ## Configuring Routes ### Advanced Routing and Custom Transitions with `PlayxRoute` @@ -380,6 +476,8 @@ The `PlayxRoute` class extends the functionality of the `GoRoute` class, providi `PlayxRoute` is designed to enhance navigation by offering: - **Lifecycle Management**: Attach custom logic that runs when a route is entered or exited, enabling better control over the state and behavior of your app. +- **Shell Builder**: Render page chrome immediately during transitions to prevent blank frames. +- **Non-Blocking Initialization**: Render pages immediately with `isInitialized` state for custom loading UIs. - **Page Configuration**: Customize various settings like page title, transition duration, and modal behavior. - **Custom Transitions**: Apply predefined or custom animations for transitioning between pages. @@ -394,7 +492,7 @@ The `PlayxRoute` class extends the functionality of the `GoRoute` class, providi PlayxRoute( path: '/dashboard', name: 'dashboard', - builder: (context, state) => DashboardPage(), + builder: (context, state, isInitialized) => DashboardPage(), binding: DashboardBinding(), ); ``` @@ -443,7 +541,7 @@ Example: PlayxRoute( path: '/custom', name: 'customRoute', - builder: (context, state) => CustomPage(), + builder: (context, state, isInitialized) => CustomPage(), binding: CustomBinding(), pageConfiguration: PlayxPageConfiguration.customTransition( transitionsBuilder: (context, animation, secondaryAnimation, child) { diff --git a/example/lib/navigation/pages.dart b/example/lib/navigation/pages.dart index e55e84b..6c336e1 100644 --- a/example/lib/navigation/pages.dart +++ b/example/lib/navigation/pages.dart @@ -34,7 +34,7 @@ class AppPages { PlayxRoute( path: Paths.splash, name: Routes.splash, - builder: (context, state) => const SplashPage(), + builder: (context, state, isInitialized) => const SplashPage(), binding: SplashBinding(), ), @@ -68,7 +68,7 @@ class AppPages { PlayxShellBranch( name: Routes.home, path: Paths.home, - builder: (context, state) => const HomePage(), + builder: (context, state, isInitialized) => const HomePage(), binding: HomeBinding(), ), @@ -76,13 +76,13 @@ class AppPages { PlayxShellBranch( path: Paths.products, name: Routes.products, - builder: (context, state) => const ProductsPage(), + builder: (context, state, isInitialized) => const ProductsPage(), binding: ProductsBinding(), routes: [ PlayxRoute( path: Paths.productDetails, name: Routes.productDetails, - builder: (context, state) => + builder: (context, state, isInitialized) => ProductDetailsPage(product: state.extra as Product?), binding: DetailsBinding(), loadingWidget: const Center( @@ -96,13 +96,13 @@ class AppPages { PlayxShellBranch( path: Paths.explore, name: Routes.explore, - builder: (context, state) => const ExplorePage(), + builder: (context, state, isInitialized) => const ExplorePage(), binding: ExploreBinding(), routes: [ PlayxRoute( path: Paths.exploreDetails, name: Routes.exploreDetails, - builder: (context, state) => + builder: (context, state, isInitialized) => ExploreDetailsPage(product: state.extra as Product?), binding: ExploreDetailsBinding(), ), diff --git a/example/pubspec.lock b/example/pubspec.lock index 3d22bc4..2a1b087 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -21,10 +21,10 @@ packages: dependency: transitive description: name: characters - sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + sha256: faf38497bda5ead2a8c7615f4f7939df04333478bf32e4173fcb06d428b5716b url: "https://pub.dev" source: hosted - version: "1.4.0" + version: "1.4.1" clock: dependency: transitive description: @@ -84,10 +84,10 @@ packages: dependency: transitive description: name: go_router - sha256: "7974313e217a7771557add6ff2238acb63f635317c35fa590d348fb238f00896" + sha256: "5540e4a3f416dd4a93458257b908eb88353cbd0fb5b0a3d1bd7d849ba1e88735" url: "https://pub.dev" source: hosted - version: "17.1.0" + version: "17.2.1" leak_tracker: dependency: transitive description: @@ -132,18 +132,18 @@ packages: dependency: transitive description: name: matcher - sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + sha256: dc0b7dc7651697ea4ff3e69ef44b0407ea32c487a39fff6a4004fa585e901861 url: "https://pub.dev" source: hosted - version: "0.12.17" + version: "0.12.19" material_color_utilities: dependency: transitive description: name: material_color_utilities - sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + sha256: "9c337007e82b1889149c82ed242ed1cb24a66044e30979c44912381e9be4c48b" url: "https://pub.dev" source: hosted - version: "0.11.1" + version: "0.13.0" meta: dependency: transitive description: @@ -166,7 +166,7 @@ packages: path: ".." relative: true source: path - version: "1.0.0" + version: "2.0.0" sky_engine: dependency: transitive description: flutter @@ -216,10 +216,10 @@ packages: dependency: transitive description: name: test_api - sha256: ab2726c1a94d3176a45960b6234466ec367179b87dd74f1611adb1f3b5fb9d55 + sha256: "8161c84903fd860b26bfdefb7963b3f0b68fee7adea0f59ef805ecca346f0c7a" url: "https://pub.dev" source: hosted - version: "0.7.7" + version: "0.7.10" vector_math: dependency: transitive description: diff --git a/lib/playx_navigation.dart b/lib/playx_navigation.dart index 1ab8bee..31a173a 100644 --- a/lib/playx_navigation.dart +++ b/lib/playx_navigation.dart @@ -3,6 +3,8 @@ library; export 'package:go_router/go_router.dart'; export 'src/binding/playx_binding.dart'; +export 'src/models/playx_page_config.dart' + show PlayxPageConfig, PlayxRouteWidgetBuilder, PlayxShellWidgetBuilder; export 'src/models/playx_page_configuration.dart'; export 'src/models/playx_page_transition.dart'; export 'src/playx_navigation.dart'; diff --git a/lib/src/models/playx_page_config.dart b/lib/src/models/playx_page_config.dart new file mode 100644 index 0000000..cac5528 --- /dev/null +++ b/lib/src/models/playx_page_config.dart @@ -0,0 +1,130 @@ +import 'package:flutter/widgets.dart'; +import 'package:go_router/go_router.dart'; + +/// Builder function for route content that includes initialization state. +/// +/// Unlike [GoRouterWidgetBuilder], this builder exposes [isInitialized] so +/// the route widget can react to the binding's `onEnter` completion state. +/// +/// - [context]: The [BuildContext] of the route. +/// - [state]: The [GoRouterState] containing metadata about the current route. +/// - [isInitialized]: Whether the binding's `onEnter` has completed. +/// Always `true` when no binding is attached. +typedef PlayxRouteWidgetBuilder = Widget Function( + BuildContext context, + GoRouterState state, + bool isInitialized, +); + +/// Builder function for a page's persistent shell (AppBar, Drawer, Scaffold). +/// +/// The shell is rendered **immediately** during navigation transitions, avoiding +/// blank frames. The [child] parameter contains either the actual page content +/// (when initialized) or a loading widget (while `onEnter` is running). +/// +/// This allows the page's outer chrome (AppBar, Drawer, bottom bar) to be +/// visible throughout the transition animation, while only the body area +/// shows a loading state. +/// +/// - [context]: The [BuildContext] of the route. +/// - [state]: The [GoRouterState] containing metadata about the current route. +/// - [isInitialized]: Whether the binding's `onEnter` has completed. +/// - [child]: The content widget — either the page body or a loading placeholder. +typedef PlayxShellWidgetBuilder = Widget Function( + BuildContext context, + GoRouterState state, + bool isInitialized, + Widget child, +); + +/// Global configuration for [PlayxPage] behavior. +/// +/// Provide this to [PlayxNavigationBuilder] to set defaults for all routes. +/// Individual route settings (on [PlayxRoute]) override these globals. +/// +/// ### Example: +/// ```dart +/// PlayxNavigationBuilder( +/// router: myRouter, +/// config: PlayxPageConfig( +/// loadingWidget: Center(child: CircularProgressIndicator()), +/// waitForBinding: false, +/// shellBuilder: (context, state, isInitialized, child) => Scaffold( +/// appBar: AppBar(title: Text('My App')), +/// body: child, +/// ), +/// ), +/// builder: (context) => MyApp(), +/// ) +/// ``` +class PlayxPageConfig { + /// Default widget shown while a binding's `onEnter` is executing. + /// + /// Used when a route doesn't specify its own `loadingWidget`. + /// Defaults to [SizedBox.shrink()] when not provided. + final Widget? loadingWidget; + + /// Whether routes should block their build on `onEnter` completion by default. + /// + /// When `true` (default), routes show a loading widget until `onEnter` completes. + /// When `false`, routes render their content immediately and `onEnter` runs in + /// the background. The builder receives `isInitialized = false` until ready. + /// + /// Individual routes can override this via [PlayxRoute.waitForBinding]. + final bool waitForBinding; + + /// Default shell builder applied to all routes that don't specify their own. + /// + /// When provided, every route with a binding will render this shell immediately + /// during navigation, wrapping either the page content (when initialized) or + /// a loading widget (while `onEnter` is running). + /// + /// Individual routes can override this via [PlayxRoute.shellBuilder]. + final PlayxShellWidgetBuilder? shellBuilder; + + /// Default duration for the crossfade animation between the loading widget + /// and the initialized page content. + /// + /// When set, an [AnimatedSwitcher] with a fade transition is applied to + /// smoothly transition from the loading state to the actual content. + /// + /// Set to `null` or [Duration.zero] to disable the animation (no transition). + /// Individual routes can override this via [PlayxRoute.initTransitionDuration]. + final Duration? initTransitionDuration; + + /// Creates a [PlayxPageConfig] with the specified defaults. + const PlayxPageConfig({ + this.loadingWidget, + this.waitForBinding = true, + this.shellBuilder, + this.initTransitionDuration, + }); +} + +/// An [InheritedWidget] that provides [PlayxPageConfig] to descendant widgets. +/// +/// This is used internally by [PlayxNavigationBuilder] to propagate global +/// configuration down to [PlayxPage] instances. +class PlayxPageConfigProvider extends InheritedWidget { + /// The global page configuration. + final PlayxPageConfig config; + + const PlayxPageConfigProvider({ + super.key, + required this.config, + required super.child, + }); + + /// Retrieves the nearest [PlayxPageConfig] from the widget tree, or `null` + /// if no [PlayxPageConfigProvider] is found. + static PlayxPageConfig? of(BuildContext context) { + return context + .dependOnInheritedWidgetOfExactType() + ?.config; + } + + @override + bool updateShouldNotify(PlayxPageConfigProvider oldWidget) { + return config != oldWidget.config; + } +} diff --git a/lib/src/routes/playx_page.dart b/lib/src/routes/playx_page.dart index 56f5f12..a53cc15 100644 --- a/lib/src/routes/playx_page.dart +++ b/lib/src/routes/playx_page.dart @@ -1,6 +1,7 @@ import 'package:flutter/widgets.dart'; import 'package:playx_navigation/playx_navigation.dart'; import 'package:playx_navigation/src/binding/playx_page_state.dart'; +import 'package:playx_navigation/src/models/playx_page_config.dart'; /// An internal widget that wraps a route's content and manages the /// [PlayxBinding] lifecycle via standard widget lifecycle methods. @@ -12,26 +13,68 @@ import 'package:playx_navigation/src/binding/playx_page_state.dart'; /// the visible route. This prevents unnecessary controller registration /// and API calls for pages the user hasn't navigated to yet. /// +/// **Shell builder support:** +/// When a [shellBuilder] is provided (either per-route or globally via +/// [PlayxPageConfig]), the shell (AppBar, Drawer, Scaffold) is rendered +/// immediately during the page transition. Only the body content waits +/// for initialization, preventing blank frames during navigation. +/// +/// **Non-blocking initialization:** +/// When [waitForBinding] is `false`, the page content renders immediately +/// with `isInitialized = false`. The binding's `onEnter` still runs in the +/// background and triggers a rebuild when complete. +/// /// **Lifecycle:** -/// - Mount as top route → `onEnter` fires immediately, blocking build. -/// - Mount as backstack → deferred, shows empty widget, fires `onEnter` +/// - Mount as top route → `onEnter` fires immediately. +/// - Mount as backstack → deferred, shows loading/shell, fires `onEnter` /// when the page becomes visible (e.g., user pops back to it). /// - Dispose → `onExit` fires only if `onEnter` was previously called. class PlayxPage extends StatefulWidget { final PlayxBinding binding; final GoRouterState state; - final Widget child; - /// An optional widget that is displayed while the [binding]'s `onEnter` is being initialized. - /// Defaults to [SizedBox.shrink()]. + /// Builder function for the page content. Receives [isInitialized] to allow + /// the content to react to the binding's initialization state. + final PlayxRouteWidgetBuilder childBuilder; + + /// Optional shell builder for persistent chrome (AppBar, Drawer, Scaffold). + /// When provided, the shell is rendered immediately — only the body content + /// depends on initialization state. + /// Overrides the global [PlayxPageConfig.shellBuilder]. + final PlayxShellWidgetBuilder? shellBuilder; + + /// An optional widget that is displayed while the [binding]'s `onEnter` is + /// being initialized. Overrides the global [PlayxPageConfig.loadingWidget]. + /// Defaults to [SizedBox.shrink()] when neither route-level nor global is set. final Widget? loadingWidget; + /// Whether to block the page build until `onEnter` completes. + /// + /// - `null`: Use the global default from [PlayxPageConfig.waitForBinding]. + /// - `true`: Block the build — show loading/shell until `onEnter` completes. + /// - `false`: Render immediately — `onEnter` runs in background, builder + /// receives `isInitialized = false` until ready. + final bool? waitForBinding; + + /// Duration for the crossfade animation between loading and content. + /// + /// When set, an [AnimatedSwitcher] smoothly transitions from the loading + /// widget to the page content after `onEnter` completes. + /// + /// - `null`: Use the global default from [PlayxPageConfig.initTransitionDuration]. + /// - [Duration.zero]: No animation. + /// - Any positive duration: Crossfade animation with that duration. + final Duration? initTransitionDuration; + const PlayxPage({ super.key, required this.binding, required this.state, - required this.child, + required this.childBuilder, + this.shellBuilder, this.loadingWidget, + this.waitForBinding, + this.initTransitionDuration, }); @override @@ -108,11 +151,61 @@ class _PlayxPageState extends State { super.dispose(); } + /// Wraps [child] in an [AnimatedSwitcher] if a transition duration is set. + /// + /// Uses [ValueKey] based on [_initialized] to trigger the crossfade + /// when the initialization state changes. + Widget _withTransition(Widget child, Duration? duration) { + if (duration == null || duration == Duration.zero) { + return child; + } + return AnimatedSwitcher( + duration: duration, + child: KeyedSubtree( + key: ValueKey(_initialized), + child: child, + ), + ); + } + @override Widget build(BuildContext context) { - if (!_initialized) { - return widget.loadingWidget ?? const SizedBox.shrink(); + // Resolve effective configuration: route-level > global > defaults. + final globalConfig = PlayxPageConfigProvider.of(context); + + final effectiveShell = widget.shellBuilder ?? globalConfig?.shellBuilder; + final effectiveLoading = widget.loadingWidget ?? + globalConfig?.loadingWidget ?? + const SizedBox.shrink(); + final effectiveWait = + widget.waitForBinding ?? globalConfig?.waitForBinding ?? true; + final effectiveDuration = + widget.initTransitionDuration ?? globalConfig?.initTransitionDuration; + + // --- Shell builder present: shell always renders, child depends on init --- + if (effectiveShell != null) { + final child = _initialized + ? widget.childBuilder(context, widget.state, true) + : effectiveLoading; + return effectiveShell( + context, + widget.state, + _initialized, + _withTransition(child, effectiveDuration), + ); + } + + // --- No shell: respect waitForBinding --- + if (!_initialized && effectiveWait) { + // Blocking mode (current default behavior): show loading until ready. + return _withTransition(effectiveLoading, effectiveDuration); } - return widget.child; + + // Either initialized, or non-blocking mode: render the child. + // The child receives _initialized so it can handle its own loading state. + return _withTransition( + widget.childBuilder(context, widget.state, _initialized), + effectiveDuration, + ); } } diff --git a/lib/src/routes/playx_route.dart b/lib/src/routes/playx_route.dart index 6834704..c5a51a7 100644 --- a/lib/src/routes/playx_route.dart +++ b/lib/src/routes/playx_route.dart @@ -3,6 +3,7 @@ import 'package:go_router/go_router.dart'; import 'package:playx_navigation/src/routes/playx_page.dart'; import '../binding/playx_binding.dart'; +import '../models/playx_page_config.dart'; import '../models/playx_page_configuration.dart'; import '../models/playx_page_transition.dart'; @@ -13,30 +14,59 @@ import '../models/playx_page_transition.dart'; /// capabilities, including custom animations for page transitions, configurable /// settings for pages, and bindings for route-specific lifecycle events. /// +/// **Shell builder support:** +/// When [shellBuilder] is provided, the page's outer chrome (AppBar, Drawer, +/// Scaffold) is rendered immediately during navigation transitions. Only the +/// body content waits for the binding's `onEnter` to complete, preventing +/// blank frames during page transitions. +/// +/// **Non-blocking initialization:** +/// Set [waitForBinding] to `false` to render the page content immediately while +/// `onEnter` runs in the background. The builder receives `isInitialized = false` +/// until the binding is ready. +/// /// **Example Usage:** /// ```dart +/// // With shell builder — AppBar shows immediately: +/// PlayxRoute( +/// path: '/channels', +/// name: 'channels', +/// shellBuilder: (context, state, isInitialized, child) => Scaffold( +/// appBar: AppBar(title: Text('Channels')), +/// drawer: MyDrawer(), +/// body: child, +/// ), +/// builder: (context, state, isInitialized) => ChannelsListView(), +/// binding: ChannelsBinding(), +/// ) +/// +/// // Without shell — uses isInitialized to handle loading: /// PlayxRoute( /// path: '/profile', /// name: 'profile', -/// builder: (BuildContext context, GoRouterState state) { -/// return ProfilePage(userId: state.pathParameters['userId']); +/// builder: (context, state, isInitialized) { +/// if (!isInitialized) return ProfileSkeleton(); +/// return ProfilePage(); /// }, -/// transition: PlayxPageTransition.fade, -/// pageConfiguration: PlayxPageConfiguration( -/// title: 'User Profile', -/// ), -/// binding: MyProfileBinding(), -/// ); +/// binding: ProfileBinding(), +/// waitForBinding: false, +/// ) /// ``` /// /// **Parameters:** /// - `path`: The URL path of the route, for example, `/profile`. /// - `name`: An optional name for the route, which is useful for navigation and redirection. -/// - `builder`: A function that builds the widget for this route, given the current context and state. +/// - `builder`: A [PlayxRouteWidgetBuilder] that builds the widget for this route, +/// receiving the current context, state, and initialization status. +/// - `shellBuilder`: An optional [PlayxShellWidgetBuilder] that wraps the page +/// content with persistent chrome (AppBar, Drawer). Overrides global default. /// - `transition`: Specifies the page transition animation to be used. Defaults to [PlayxPageTransition.cupertino]. /// - `pageConfiguration`: Configures various settings for the page, such as title and key. Defaults to [PlayxPageConfiguration()]. /// - `parentNavigatorKey`: An optional key for the parent navigator. /// - `binding`: An optional [PlayxBinding] instance used to handle route-specific lifecycle events. +/// - `loadingWidget`: An optional widget shown while `onEnter` is executing. Overrides global default. +/// - `waitForBinding`: Whether to block the build on `onEnter`. `null` uses the global default. +/// - `initTransitionDuration`: Duration for crossfade animation between loading and content. `null` uses global default. /// - `redirect`: An optional callback function for custom redirection logic. /// - `onExit`: An optional callback function for handling logic when the route is exited. /// - `routes`: A list of nested subroutes for this route. Defaults to an empty list. @@ -82,18 +112,60 @@ class PlayxRoute extends GoRoute { final PlayxPageConfiguration pageConfiguration; /// An optional widget that is displayed while the [binding]'s `onEnter` is being initialized. - /// Defaults to [SizedBox.shrink()]. + /// + /// Overrides the global [PlayxPageConfig.loadingWidget]. + /// Defaults to [SizedBox.shrink()] when neither route-level nor global is set. final Widget? loadingWidget; + /// Optional shell builder for persistent page chrome (AppBar, Drawer, Scaffold). + /// + /// When provided, the shell is rendered **immediately** during navigation + /// transitions. The `child` parameter passed to the shell builder contains + /// either the actual page content (when initialized) or a loading widget + /// (while `onEnter` is running). + /// + /// Overrides the global [PlayxPageConfig.shellBuilder]. + /// + /// ```dart + /// shellBuilder: (context, state, isInitialized, child) => Scaffold( + /// appBar: AppBar(title: Text('My Page')), + /// drawer: isInitialized ? MyDrawer() : null, + /// body: child, + /// ), + /// ``` + final PlayxShellWidgetBuilder? shellBuilder; + + /// Whether to block the page build until `onEnter` completes. + /// + /// - `null` (default): Use the global default from [PlayxPageConfig.waitForBinding]. + /// - `true`: Block the build — show loading widget or shell until `onEnter` completes. + /// - `false`: Render content immediately — `onEnter` runs in the background, + /// and the builder receives `isInitialized = false` until ready. + final bool? waitForBinding; + + /// Duration for the crossfade animation between the loading widget and + /// the initialized page content. + /// + /// When set, an [AnimatedSwitcher] smoothly transitions from the loading + /// state to the actual content after `onEnter` completes. + /// + /// - `null` (default): Use the global default from [PlayxPageConfig.initTransitionDuration]. + /// - [Duration.zero]: No animation (instant swap). + /// - Any positive duration: Crossfade with that duration (e.g., `Duration(milliseconds: 300)`). + final Duration? initTransitionDuration; + PlayxRoute({ required super.path, super.name, - required GoRouterWidgetBuilder builder, + required PlayxRouteWidgetBuilder builder, this.transition = PlayxPageTransition.cupertino, this.pageConfiguration = const PlayxPageConfiguration(), super.parentNavigatorKey, this.binding, this.loadingWidget, + this.shellBuilder, + this.waitForBinding, + this.initTransitionDuration, super.redirect, super.onExit, super.routes = const [], @@ -102,12 +174,15 @@ class PlayxRoute extends GoRoute { return transition.buildPage( config: pageConfiguration, child: binding == null - ? builder(ctx, state) + ? builder(ctx, state, true) : PlayxPage( binding: binding, state: state, + childBuilder: builder, + shellBuilder: shellBuilder, loadingWidget: loadingWidget, - child: builder(ctx, state), + waitForBinding: waitForBinding, + initTransitionDuration: initTransitionDuration, ), state: state, ); diff --git a/lib/src/routes/playx_shell_branch.dart b/lib/src/routes/playx_shell_branch.dart index 7345d7e..a5ae72b 100644 --- a/lib/src/routes/playx_shell_branch.dart +++ b/lib/src/routes/playx_shell_branch.dart @@ -7,7 +7,9 @@ class PlayxShellBranch extends StatefulShellBranch { /// /// The [path] is the route path that this branch will handle. /// The [name] is the name of the route. - /// The [builder] is the widget builder for the route. + /// The [builder] is the widget builder for the route, receiving `isInitialized` state. + /// The [shellBuilder] wraps the page content with persistent chrome (AppBar, Drawer). + /// The [waitForBinding] controls whether to block the build on `onEnter`. /// The [transition] is the page transition for the route. /// The [pageConfiguration] is the page configuration for the route. /// The [parentNavigatorKey] is the parent navigator key. @@ -22,12 +24,15 @@ class PlayxShellBranch extends StatefulShellBranch { super.preload = false, required String path, String? name, - required GoRouterWidgetBuilder builder, + required PlayxRouteWidgetBuilder builder, PlayxPageTransition transition = PlayxPageTransition.cupertino, PlayxPageConfiguration pageConfiguration = const PlayxPageConfiguration(), GlobalKey? parentNavigatorKey, PlayxBinding? binding, Widget? loadingWidget, + PlayxShellWidgetBuilder? shellBuilder, + bool? waitForBinding, + Duration? initTransitionDuration, GoRouterRedirect? redirect, ExitCallback? onExit, List routes = const [], @@ -41,6 +46,9 @@ class PlayxShellBranch extends StatefulShellBranch { parentNavigatorKey: parentNavigatorKey, binding: binding, loadingWidget: loadingWidget, + shellBuilder: shellBuilder, + waitForBinding: waitForBinding, + initTransitionDuration: initTransitionDuration, redirect: redirect, onExit: onExit, routes: routes, diff --git a/lib/src/widgets/playx_navigation_builder.dart b/lib/src/widgets/playx_navigation_builder.dart index 1023159..26c0c6b 100644 --- a/lib/src/widgets/playx_navigation_builder.dart +++ b/lib/src/widgets/playx_navigation_builder.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:go_router/go_router.dart'; import 'package:playx_navigation/src/binding/playx_binding.dart'; import 'package:playx_navigation/src/binding/playx_page_state.dart'; +import 'package:playx_navigation/src/models/playx_page_config.dart'; import 'package:playx_navigation/src/playx_navigation.dart'; import '../routes/playx_route.dart'; @@ -15,6 +16,11 @@ import '../routes/playx_route.dart'; /// between routes — specifically handling [PlayxBinding.onHidden] and /// [PlayxBinding.onReEnter] lifecycle events. /// +/// **Global configuration:** +/// Use the [config] parameter to set default behavior for all routes, +/// including a global loading widget, shell builder, and whether routes +/// should block their build on `onEnter` completion. +/// /// **Lifecycle responsibilities:** /// - `onHidden`: Fired when the current route is covered by another route /// (push, branch switch) but remains in the navigation stack. @@ -29,6 +35,14 @@ import '../routes/playx_route.dart'; /// ```dart /// PlayxNavigationBuilder( /// router: myRouter, +/// config: PlayxPageConfig( +/// loadingWidget: Center(child: CircularProgressIndicator()), +/// waitForBinding: false, +/// shellBuilder: (context, state, isInitialized, child) => Scaffold( +/// appBar: AppBar(title: Text('My App')), +/// body: child, +/// ), +/// ), /// builder: (context) { /// return MyApp(); /// }, @@ -48,7 +62,23 @@ class PlayxNavigationBuilder extends StatefulWidget { /// called elsewhere in your code. final GoRouter? router; - const PlayxNavigationBuilder({super.key, required this.builder, this.router}); + /// Optional global configuration for all [PlayxPage] instances. + /// + /// Sets defaults for [loadingWidget], [waitForBinding], and [shellBuilder] + /// that individual routes can override. + /// + /// When not provided, routes use built-in defaults: + /// - `loadingWidget`: [SizedBox.shrink()] + /// - `waitForBinding`: `true` (block build until `onEnter` completes) + /// - `shellBuilder`: `null` (no shell) + final PlayxPageConfig? config; + + const PlayxNavigationBuilder({ + super.key, + required this.builder, + this.router, + this.config, + }); @override State createState() => _PlayxNavigationBuilderState(); @@ -177,6 +207,16 @@ class _PlayxNavigationBuilderState extends State { @override Widget build(BuildContext context) { - return Builder(builder: widget.builder); + Widget child = Builder(builder: widget.builder); + + // Wrap with config provider if a config is specified. + if (widget.config != null) { + child = PlayxPageConfigProvider( + config: widget.config!, + child: child, + ); + } + + return child; } } diff --git a/pubspec.yaml b/pubspec.yaml index b91f027..0e42ee0 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,6 @@ name: playx_navigation description: Playx Navigation is a Flutter package that enhances app navigation with advanced features like route lifecycle management, custom transitions, and flexible configuration. -version: 1.0.0 +version: 2.0.0 homepage: https://sourcya.io repository: https://github.com/playx-flutter/playx_navigation issue_tracker: https://github.com/playx-flutter/playx_navigation/issues @@ -21,7 +21,7 @@ dependencies: sdk: flutter flutter_web_plugins: sdk: flutter - go_router: ^17.1.0 + go_router: ^17.2.1 dev_dependencies: flutter_test: