From da38e5c1410bb70bfcc1e7f98c63cb8e513d2036 Mon Sep 17 00:00:00 2001 From: Fernando Ferrara Date: Fri, 19 Jun 2026 12:20:47 -0300 Subject: [PATCH 1/4] refactor: remove the get dependency Reimplements all services on native Flutter APIs and drops `get`: - NavigationService now uses the native Navigator through an owned GlobalKey, with a StackedRouting model and a StackedNavigationConfig (replacing Get.routing / Get.config). - Transitions are built with PageRouteBuilder (buildTransitionRoute). - StackObserver feeds StackedRouting using native route types only. - DialogService uses showGeneralDialog; BottomSheetService uses showModalBottomSheet; GetPlatform.isAndroid -> defaultTargetPlatform. - SnackbarService uses a vendored GetSnackBar widget + controller (MIT, attributed) hosted on the navigator overlay; SnackbarConfig is preserved so the public API is unchanged. - get removed from pubspec.yaml. Adds a behavioural test suite (navigation, dialog, bottom sheet, snackbar, observer, transitions) as a safety net for the migration. BREAKING: StackedService.navigatorKey is now an owned key (was Get.key). config(enableLog/defaultGlobalState), popGesture and the bottom sheet enter/exit durations are kept for compatibility but are now no-ops. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01LqYch1wfAZ5J9fGBh38AKU --- CHANGELOG.md | 13 + README.md | 8 +- .../bottom_sheet/bottom_sheet_service.dart | 79 ++- lib/src/dialog/dialog_service.dart | 34 +- lib/src/navigation/navigation_service.dart | 205 ++++--- lib/src/navigation/route_observer.dart | 134 ++--- lib/src/navigation/route_transition.dart | 91 ++- lib/src/navigation/stacked_routing.dart | 43 ++ lib/src/snackbar/snackbar_service.dart | 52 +- .../stacked_snackbar_customizations.dart | 26 - .../snackbar/vendored/stacked_snackbar.dart | 534 ++++++++++++++++++ .../vendored/stacked_snackbar_controller.dart | 405 +++++++++++++ lib/src/stacked_service.dart | 28 +- lib/stacked_services.dart | 1 + pubspec.yaml | 3 - .../bottom_sheet_service_test.dart | 120 ++++ test/dialog/dialog_service_test.dart | 125 ++++ test/helpers/test_app.dart | 68 +++ test/navigation/navigation_service_test.dart | 145 +++++ test/navigation/route_observer_test.dart | 102 ++++ test/navigation/stacked_service_test.dart | 29 + test/navigation/transition_test.dart | 57 ++ test/snackbar/snackbar_service_test.dart | 93 +++ 23 files changed, 2090 insertions(+), 305 deletions(-) create mode 100644 lib/src/navigation/stacked_routing.dart create mode 100644 lib/src/snackbar/vendored/stacked_snackbar.dart create mode 100644 lib/src/snackbar/vendored/stacked_snackbar_controller.dart create mode 100644 test/bottom_sheet/bottom_sheet_service_test.dart create mode 100644 test/dialog/dialog_service_test.dart create mode 100644 test/helpers/test_app.dart create mode 100644 test/navigation/navigation_service_test.dart create mode 100644 test/navigation/route_observer_test.dart create mode 100644 test/navigation/stacked_service_test.dart create mode 100644 test/navigation/transition_test.dart create mode 100644 test/snackbar/snackbar_service_test.dart diff --git a/CHANGELOG.md b/CHANGELOG.md index ffe4f66..977aabc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,16 @@ +# Unreleased + +### Breaking Changes + +* **Removes the `get` dependency.** All navigation, dialog, bottom sheet and snackbar + functionality is now implemented with the native Flutter `Navigator`, `showGeneralDialog`, + `showModalBottomSheet` and an `Overlay`-based snackbar (vendored from `get`, MIT licensed). + The public API of the services is preserved. Notable behaviour notes: + * `StackedService.navigatorKey` is now an owned `GlobalKey` instead of `Get.key`. + * `NavigationService.config`'s `enableLog` and `defaultGlobalState` parameters are now no-ops. + * `popGesture` and the bottom sheet `enterBottomSheetDuration` / `exitBottomSheetDuration` + parameters are kept for compatibility but are currently no-ops. + # [1.6.0](https://github.com/Stacked-Org/services/compare/v1.5.1...v1.6.0) (2024-11-07) diff --git a/README.md b/README.md index d5a3a64..e9b859d 100644 --- a/README.md +++ b/README.md @@ -33,10 +33,10 @@ completer(DialogResponse(...)); The following services are included in the package -- **NavigationService:** Makes use of the [Get](https://pub.dev/packages/get) package to expose basic navigation functionalities -- **DialogService**: Makes use of the [Get](https://pub.dev/packages/get) package to expose functionality that allows the dev to show dialogs from the ViewModels -- **SnackbarService**: Makes use of the [Get](https://pub.dev/packages/get) to expose the snack bar functionality to devs. -- **BottomSheetService**: Makes use of the [Get](https://pub.dev/packages/get) to expose the bottom sheet functionality. +- **NavigationService:** Exposes basic navigation functionalities using the native Flutter [Navigator] through a global navigator key +- **DialogService**: Exposes functionality that allows the dev to show dialogs from the ViewModels +- **SnackbarService**: Exposes the snack bar functionality to devs +- **BottomSheetService**: Exposes the bottom sheet functionality The services can be registered with get_it normally as you would usually diff --git a/lib/src/bottom_sheet/bottom_sheet_service.dart b/lib/src/bottom_sheet/bottom_sheet_service.dart index 940cd13..3d16752 100644 --- a/lib/src/bottom_sheet/bottom_sheet_service.dart +++ b/lib/src/bottom_sheet/bottom_sheet_service.dart @@ -3,9 +3,9 @@ import 'dart:convert'; import 'package:crypto/crypto.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:stacked_services/src/models/overlay_request.dart'; import 'package:stacked_services/src/models/overlay_response.dart'; +import 'package:stacked_services/src/stacked_service.dart'; import 'bottom_sheet_ui.dart'; @@ -37,19 +37,10 @@ class BottomSheetService { bool useRootNavigator = false, double elevation = 1, }) { - return Get.bottomSheet( - Material( - type: MaterialType.transparency, - child: GeneralBottomSheet( - title: title, - description: description ?? '', - confirmButtonTitle: confirmButtonTitle, - cancelButtonTitle: cancelButtonTitle, - onConfirmTapped: () => completeSheet(SheetResponse(confirmed: true)), - onCancelTapped: () => completeSheet(SheetResponse(confirmed: false)), - ), - ), - backgroundColor: Theme.of(Get.context!).brightness == Brightness.light + final context = StackedService.navigatorKey!.currentContext!; + return showModalBottomSheet( + context: context, + backgroundColor: Theme.of(context).brightness == Brightness.light ? Colors.white : Colors.grey[800], elevation: elevation, @@ -62,15 +53,24 @@ class BottomSheetService { isDismissible: barrierDismissible, isScrollControlled: isScrollControlled, enableDrag: barrierDismissible && enableDrag, - exitBottomSheetDuration: exitBottomSheetDuration, - enterBottomSheetDuration: enterBottomSheetDuration, - ignoreSafeArea: ignoreSafeArea, - settings: RouteSettings( + useSafeArea: !(ignoreSafeArea ?? true), + useRootNavigator: useRootNavigator, + routeSettings: RouteSettings( name: 'general_${_hashConcateator([ title, description, ])}'), - useRootNavigator: useRootNavigator, + builder: (_) => Material( + type: MaterialType.transparency, + child: GeneralBottomSheet( + title: title, + description: description ?? '', + confirmButtonTitle: confirmButtonTitle, + cancelButtonTitle: cancelButtonTitle, + onConfirmTapped: () => completeSheet(SheetResponse(confirmed: true)), + onCancelTapped: () => completeSheet(SheetResponse(confirmed: false)), + ), + ), ); } @@ -126,11 +126,26 @@ class BottomSheetService { final sheetBuilder = _sheetBuilders![variant]; - return Get.bottomSheet>( - Material( + return showModalBottomSheet>( + context: StackedService.navigatorKey!.currentContext!, + barrierColor: barrierColor, + elevation: elevation, + isDismissible: barrierDismissible, + isScrollControlled: isScrollControlled, + enableDrag: barrierDismissible && enableDrag, + useSafeArea: !(ignoreSafeArea ?? true), + useRootNavigator: useRootNavigator, + routeSettings: RouteSettings( + name: '$variant\_${_hashConcateator([ + title, + description, + mainButtonTitle, + secondaryButtonTitle, + ])}'), + builder: (sheetContext) => Material( type: MaterialType.transparency, child: sheetBuilder!( - Get.context!, + sheetContext, SheetRequest( title: title, description: description, @@ -151,31 +166,15 @@ class BottomSheetService { completeSheet, ), ), - barrierColor: barrierColor, - elevation: elevation, - isDismissible: barrierDismissible, - isScrollControlled: isScrollControlled, - enableDrag: barrierDismissible && enableDrag, - exitBottomSheetDuration: exitBottomSheetDuration, - enterBottomSheetDuration: enterBottomSheetDuration, - ignoreSafeArea: ignoreSafeArea, - settings: RouteSettings( - name: '$variant\_${_hashConcateator([ - title, - description, - mainButtonTitle, - secondaryButtonTitle, - ])}'), - useRootNavigator: useRootNavigator, ); } /// Check if bottomsheet is open - bool? get isBottomSheetOpen => Get.isBottomSheetOpen; + bool? get isBottomSheetOpen => StackedService.routing.isBottomSheet; /// Completes the dialog and passes the [response] to the caller void completeSheet(SheetResponse response) { - Get.back(result: response); + StackedService.navigatorKey?.currentState?.pop(response); } } diff --git a/lib/src/dialog/dialog_service.dart b/lib/src/dialog/dialog_service.dart index cb3a63e..d3c4a4e 100644 --- a/lib/src/dialog/dialog_service.dart +++ b/lib/src/dialog/dialog_service.dart @@ -1,10 +1,11 @@ import 'dart:async'; +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:stacked_services/src/dialog/platform_dialog.dart'; import 'package:stacked_services/src/models/overlay_request.dart'; import 'package:stacked_services/src/models/overlay_response.dart'; +import 'package:stacked_services/src/stacked_service.dart'; typedef DialogBuilder = Widget Function( BuildContext context, @@ -30,7 +31,7 @@ class DialogService { @Deprecated('Prefer to use the StackedServices.navigatorKey instead of using this key. This will be removed in the next major version update for stacked.') get navigatorKey { - return Get.key; + return StackedService.navigatorKey; } /// Registers a custom dialog builder. The builder function has been updated to include the function to call @@ -51,7 +52,7 @@ class DialogService { } /// Check if dialog is open - bool? get isDialogOpen => Get.isDialogOpen; + bool? get isDialogOpen => StackedService.routing.isDialog; /// Shows a dialog to the user /// @@ -86,7 +87,9 @@ class DialogService { navigatorKey: navigatorKey, ); } else { - var _dialogType = GetPlatform.isAndroid ? DialogPlatform.Material : DialogPlatform.Cupertino; + var _dialogType = defaultTargetPlatform == TargetPlatform.android + ? DialogPlatform.Material + : DialogPlatform.Cupertino; return _showDialog( title: title, description: description, @@ -115,8 +118,18 @@ class DialogService { GlobalKey? navigatorKey, }) { var isConfirmationDialog = cancelTitle != null; - return Get.dialog( - PlatformDialog( + final key = navigatorKey ?? StackedService.navigatorKey; + return showGeneralDialog( + context: key!.currentContext!, + barrierDismissible: barrierDismissible, + barrierLabel: MaterialLocalizations.of(key.currentContext!) + .modalBarrierDismissLabel, + barrierColor: Colors.black54, + transitionDuration: const Duration(milliseconds: 200), + routeSettings: routeSettings, + transitionBuilder: (_, animation, __, child) => + FadeTransition(opacity: animation, child: child), + pageBuilder: (_, __, ___) => PlatformDialog( key: Key('dialog_view'), dialogPlatform: dialogPlatform, title: title, @@ -154,9 +167,6 @@ class DialogService { ), ], ), - barrierDismissible: barrierDismissible, - routeSettings: routeSettings, - navigatorKey: navigatorKey, ); } @@ -207,13 +217,13 @@ class DialogService { 'You have to call registerCustomDialogBuilder to use this function. Look at the custom dialog UI section in the stacked_services readme.', ); - return Get.generalDialog>( + return showGeneralDialog>( + context: (navigatorKey ?? StackedService.navigatorKey)!.currentContext!, barrierColor: barrierColor, transitionDuration: const Duration(milliseconds: 200), barrierDismissible: barrierDismissible, barrierLabel: barrierLabel, routeSettings: routeSettings, - navigatorKey: navigatorKey, transitionBuilder: transitionBuilder, pageBuilder: (BuildContext buildContext, _, __) { final child = Builder( @@ -273,6 +283,6 @@ class DialogService { /// Completes the dialog and passes the [response] to the caller void completeDialog(DialogResponse response) { - Get.back(result: response); + StackedService.navigatorKey?.currentState?.pop(response); } } diff --git a/lib/src/navigation/navigation_service.dart b/lib/src/navigation/navigation_service.dart index 58e8739..157efa2 100644 --- a/lib/src/navigation/navigation_service.dart +++ b/lib/src/navigation/navigation_service.dart @@ -1,5 +1,4 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart' as G; import 'package:stacked_services/stacked_services.dart'; @Deprecated( @@ -18,9 +17,9 @@ class NavigationTransition { /// Provides a service that can be injected into the ViewModels for navigation. /// -/// Uses the Get library for all navigation requirements +/// Uses the native Flutter [Navigator] through the [StackedService.navigatorKey]. class NavigationService { - Map _transitions = { + final Map _transitions = { Transition.fade.name: Transition.fade, Transition.rightToLeft.name: Transition.rightToLeft, Transition.leftToRight.name: Transition.leftToRight, @@ -32,26 +31,36 @@ class NavigationService { Transition.noTransition.name: Transition.noTransition, }; + /// Returns the navigator state for the given nested navigation [id], or the + /// root navigator when [id] is null. + NavigatorState? _navigator([int? id]) => (id != null + ? StackedService.nestedNavigationKey(id) + : StackedService.navigatorKey) + ?.currentState; + @Deprecated( 'Prefer to use the StackedServices.navigatorKey instead of using this key. This will be removed in the next major version update for stacked.') - GlobalKey? get navigatorKey => G.Get.key; + GlobalKey? get navigatorKey => StackedService.navigatorKey; /// Returns the previous route - String get previousRoute => G.Get.previousRoute; + String get previousRoute => StackedService.routing.previous; /// Returns the current route - String get currentRoute => G.Get.currentRoute; + String get currentRoute => StackedService.routing.current; /// Returns the current arguments - dynamic get currentArguments => G.Get.arguments; + dynamic get currentArguments => StackedService.routing.args; /// Creates and/or returns a new navigator key based on the index passed in @Deprecated( 'Prefer to use the StackedServices.nestedNavigationKey instead of using this property. This will be removed in the next major version update for stacked.') GlobalKey? nestedNavigationKey(int index) => - G.Get.nestedKey(index); + StackedService.nestedNavigationKey(index); /// Allows you to configure the default behaviour for navigation. + /// + /// [enableLog] and [defaultGlobalState] were specific to the `get` package and + /// are now no-ops, kept for backwards compatibility. void config({ bool? enableLog, bool? defaultPopGesture, @@ -63,14 +72,18 @@ class NavigationService { 'Prefer to use the defaultTransitionStyle instead of using this property. This will be removed in the next major version update for stacked.') String? defaultTransition, }) { - G.Get.config( - enableLog: enableLog, - defaultPopGesture: defaultPopGesture, - defaultOpaqueRoute: defaultOpaqueRoute, - defaultDurationTransition: defaultDurationTransition, - defaultGlobalState: defaultGlobalState, - defaultTransition: defaultTransitionStyle?.toGet ?? - _getTransitionOrDefault(defaultTransition!)); + final config = StackedService.navigationConfig; + if (defaultPopGesture != null) config.defaultPopGesture = defaultPopGesture; + if (defaultOpaqueRoute != null) { + config.defaultOpaqueRoute = defaultOpaqueRoute; + } + if (defaultDurationTransition != null) { + config.defaultDuration = defaultDurationTransition; + } + config.defaultTransition = defaultTransitionStyle ?? + (defaultTransition != null + ? _getTransitionOrDefault(defaultTransition) + : config.defaultTransition); } /// Pushes [page] onto the navigation stack. This uses the [page] itself (Widget) instead @@ -78,10 +91,7 @@ class NavigationService { /// /// [id] is for when you are using nested navigation, as explained in documentation. /// - /// If you want the same behavior of ios that pops a route when the user drag, you can set [popGesture] to true. - /// - /// [preventDuplicates] will prevent you from pushing a route that you already in, if you want to push anyway, - /// set to false. + /// [popGesture] is kept for backwards compatibility and is currently a no-op. /// /// [duration] transition duration. /// @@ -106,20 +116,23 @@ class NavigationService { Transition? transitionStyle, String? routeName, }) { - return G.Get.to( - () => page, - transition: transitionStyle?.toGet ?? - transitionClass?.toGet ?? + if (preventDuplicates && + routeName != null && + routeName == StackedService.routing.current) { + return null; + } + + return _navigator(id)?.push(buildTransitionRoute( + page, + transition: transitionStyle ?? + transitionClass ?? _getTransitionOrDefault(transition), - duration: duration ?? G.Get.defaultTransitionDuration, - popGesture: popGesture ?? G.Get.isPopGestureEnable, - opaque: opaque ?? G.Get.isOpaqueRouteDefault, - id: id, - preventDuplicates: preventDuplicates, - curve: curve, + duration: duration ?? StackedService.navigationConfig.defaultDuration, + opaque: opaque ?? StackedService.navigationConfig.defaultOpaqueRoute, fullscreenDialog: fullscreenDialog, - routeName: routeName, - ); + curve: curve ?? Curves.linear, + settings: routeName != null ? RouteSettings(name: routeName) : null, + )); } /// Replaces current view in the navigation stack. This uses the [page] itself (Widget) instead @@ -127,10 +140,7 @@ class NavigationService { /// /// [id] is for when you are using nested navigation, as explained in documentation. /// - /// If you want the same behavior of ios that pops a route when the user drag, you can set [popGesture] to true. - /// - /// [preventDuplicates] will prevent you from pushing a route that you already in, if you want to push anyway, - /// set to false. + /// [popGesture] is kept for backwards compatibility and is currently a no-op. /// /// [duration] transition duration. /// @@ -155,20 +165,17 @@ class NavigationService { Transition? transitionStyle, String? routeName, }) { - return G.Get.off( - () => page, - transition: transitionStyle?.toGet ?? - transitionClass?.toGet ?? + return _navigator(id)?.pushReplacement(buildTransitionRoute( + page, + transition: transitionStyle ?? + transitionClass ?? _getTransitionOrDefault(transition), - duration: duration ?? G.Get.defaultTransitionDuration, - popGesture: popGesture ?? G.Get.isPopGestureEnable, - opaque: opaque ?? G.Get.isOpaqueRouteDefault, - id: id, - preventDuplicates: preventDuplicates, - curve: curve, + duration: duration ?? StackedService.navigationConfig.defaultDuration, + opaque: opaque ?? StackedService.navigationConfig.defaultOpaqueRoute, fullscreenDialog: fullscreenDialog, - routeName: routeName, - ); + curve: curve ?? Curves.linear, + settings: routeName != null ? RouteSettings(name: routeName) : null, + )); } /// Pops the current scope and indicates if you can pop again @@ -176,20 +183,24 @@ class NavigationService { /// [result] is the data that will returned to the previous route /// you can use this feature to exchange data between two routes bool back({dynamic result, int? id}) { - G.Get.back(result: result, id: id); - return G.Get.key.currentState?.canPop() ?? false; + final navigator = _navigator(id); + navigator?.pop(result); + return navigator?.canPop() ?? false; } /// Pops the back stack until the predicate is satisfied /// /// [id] is for when you are using nested navigation, as explained in documentation. void popUntil(RoutePredicate predicate, {int? id}) { - G.Get.until(predicate, id: id); + _navigator(id)?.popUntil(predicate); } /// Pops the back stack the number of times you indicate with [popTimes] void popRepeated(int popTimes) { - G.Get.close(popTimes); + final navigator = _navigator(); + for (var i = 0; i < popTimes; i++) { + navigator?.pop(); + } } /// Pushes [routeName] onto the navigation stack @@ -206,14 +217,15 @@ class NavigationService { Map? parameters, RouteTransitionsBuilder? transition, }) { - return G.Get.toNamed( - routeName, + if (preventDuplicates && routeName == StackedService.routing.current) { + return null; + } + + return _navigator(id)?.pushNamed( + _routeNameWithParameters(routeName, parameters), arguments: transition != null ? {'arguments': arguments, 'transition': transition} : arguments, - id: id, - preventDuplicates: preventDuplicates, - parameters: parameters, ); } @@ -221,10 +233,7 @@ class NavigationService { /// /// [id] is for when you are using nested navigation, as explained in documentation. /// - /// If you want the same behavior of ios that pops a route when the user drag, you can set [popGesture] to true. - /// - /// [preventDuplicates] will prevent you from pushing a route that you already in, if you want to push anyway, - /// set to false. + /// [popGesture] is kept for backwards compatibility and is currently a no-op. /// /// [duration] transition duration. /// @@ -244,18 +253,17 @@ class NavigationService { Transition? transition, Transition? transitionStyle, }) { - return G.Get.to( - () => view, - arguments: arguments, - id: id, - opaque: opaque, - preventDuplicates: preventDuplicates, - curve: curve, - duration: duration, + return _navigator(id)?.push(buildTransitionRoute( + view, + transition: transitionStyle ?? + transition ?? + StackedService.navigationConfig.defaultTransition, + duration: duration ?? StackedService.navigationConfig.defaultDuration, + opaque: opaque ?? StackedService.navigationConfig.defaultOpaqueRoute, fullscreenDialog: fullscreenDialog, - popGesture: popGesture, - transition: transitionStyle?.toGet ?? transition?.toGet, - ); + curve: curve ?? Curves.linear, + settings: RouteSettings(arguments: arguments), + )); } /// Replaces the current route with the [routeName] @@ -272,14 +280,15 @@ class NavigationService { Map? parameters, RouteTransitionsBuilder? transition, }) { - return G.Get.offNamed( - routeName, + if (preventDuplicates && routeName == StackedService.routing.current) { + return null; + } + + return _navigator(id)?.pushReplacementNamed( + _routeNameWithParameters(routeName, parameters), arguments: transition != null ? {'arguments': arguments, 'transition': transition} : arguments, - id: id, - preventDuplicates: preventDuplicates, - parameters: parameters, ); } @@ -292,11 +301,10 @@ class NavigationService { int? id, Map? parameters, }) { - return G.Get.offAllNamed( - routeName, + return _navigator(id)?.pushNamedAndRemoveUntil( + _routeNameWithParameters(routeName, parameters), + (route) => false, arguments: arguments, - id: id, - parameters: parameters, ); } @@ -306,10 +314,15 @@ class NavigationService { dynamic arguments, int? id, }) { - return G.Get.offAll( - view, - arguments: arguments, - id: id, + return _navigator(id)?.pushAndRemoveUntil( + buildTransitionRoute( + view, + transition: StackedService.navigationConfig.defaultTransition, + duration: StackedService.navigationConfig.defaultDuration, + opaque: StackedService.navigationConfig.defaultOpaqueRoute, + settings: RouteSettings(arguments: arguments), + ), + (route) => false, ); } @@ -352,11 +365,10 @@ class NavigationService { /// [id] is for when you are using nested navigation, as explained in documentation. Future? pushNamedAndRemoveUntil(String routeName, {RoutePredicate? predicate, dynamic arguments, int? id}) { - return G.Get.offAllNamed( + return _navigator(id)?.pushNamedAndRemoveUntil( routeName, - predicate: predicate, + predicate ?? (route) => false, arguments: arguments, - id: id, ); } @@ -366,8 +378,19 @@ class NavigationService { ?.popUntil((Route route) => route.isFirst); } - G.Transition? _getTransitionOrDefault(String transition) { - String _transition = transition.toLowerCase(); - return _transitions[_transition]?.toGet ?? G.Get.defaultTransition; + /// Folds [parameters] into [routeName] as a query string, matching the + /// behaviour `get` used for named navigation with parameters. + String _routeNameWithParameters( + String routeName, + Map? parameters, + ) { + if (parameters == null) return routeName; + return Uri(path: routeName, queryParameters: parameters).toString(); + } + + Transition _getTransitionOrDefault(String transition) { + final _transition = transition.toLowerCase(); + return _transitions[_transition] ?? + StackedService.navigationConfig.defaultTransition; } } diff --git a/lib/src/navigation/route_observer.dart b/lib/src/navigation/route_observer.dart index c30dd63..764b34f 100644 --- a/lib/src/navigation/route_observer.dart +++ b/lib/src/navigation/route_observer.dart @@ -1,22 +1,23 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; -import 'package:get/get_navigation/src/dialog/dialog_route.dart'; -import 'package:get/get_navigation/src/router_report.dart'; + +import 'stacked_routing.dart'; + +bool _isDialogRoute(Route? route) => route is RawDialogRoute; + +bool _isBottomSheetRoute(Route? route) => route is ModalBottomSheetRoute; + +bool _isPageRoute(Route? route) => route is PageRoute; String? _extractRouteName(Route? route) { if (route?.settings.name != null) { return route!.settings.name; } - if (route is GetPageRoute) { - return route.routeName; - } - - if (route is GetDialogRoute) { + if (_isDialogRoute(route)) { return 'DIALOG ${route.hashCode}'; } - if (route is GetModalBottomSheetRoute) { + if (_isBottomSheetRoute(route)) { return 'BOTTOMSHEET ${route.hashCode}'; } @@ -24,14 +25,14 @@ String? _extractRouteName(Route? route) { } class _RouteData { - final bool isGetPageRoute; + final bool isPageRoute; final bool isBottomSheet; final bool isDialog; final String? name; _RouteData({ required this.name, - required this.isGetPageRoute, + required this.isPageRoute, required this.isBottomSheet, required this.isDialog, }); @@ -39,38 +40,38 @@ class _RouteData { factory _RouteData.ofRoute(Route? route) { return _RouteData( name: _extractRouteName(route), - isGetPageRoute: route is GetPageRoute, - isDialog: route is GetDialogRoute, - isBottomSheet: route is GetModalBottomSheetRoute, + isPageRoute: _isPageRoute(route), + isDialog: _isDialogRoute(route), + isBottomSheet: _isBottomSheetRoute(route), ); } } +/// Observes navigation events and keeps a [StackedRouting] up to date so the +/// services can expose currentRoute / previousRoute / isDialogOpen, etc. class StackObserver extends NavigatorObserver { - StackObserver({Routing? routeSend}) : _routeSend = routeSend ?? Get.routing; + StackObserver({StackedRouting? routing}) + : routing = routing ?? StackedRouting(); - final Routing? _routeSend; + final StackedRouting routing; @override void didPush(Route route, Route? previousRoute) { super.didPush(route, previousRoute); final newRoute = _RouteData.ofRoute(route); - RouterReportManager.reportCurrentRoute(route); - _routeSend?.update((value) { - if (route is PageRoute) { - value.current = newRoute.name ?? ''; - } - - value.args = route.settings.arguments; - value.route = route; - value.isBack = false; - value.removed = ''; - value.previous = _extractRouteName(previousRoute) ?? ''; - value.isBottomSheet = - newRoute.isBottomSheet ? true : value.isBottomSheet ?? false; - value.isDialog = newRoute.isDialog ? true : value.isDialog ?? false; - }); + if (newRoute.isPageRoute) { + routing.current = newRoute.name ?? ''; + } + + routing.args = route.settings.arguments; + routing.route = route; + routing.isBack = false; + routing.removed = ''; + routing.previous = _extractRouteName(previousRoute) ?? ''; + routing.isBottomSheet = + newRoute.isBottomSheet ? true : routing.isBottomSheet ?? false; + routing.isDialog = newRoute.isDialog ? true : routing.isDialog ?? false; } @override @@ -78,21 +79,17 @@ class StackObserver extends NavigatorObserver { super.didPop(route, previousRoute); final newRoute = _RouteData.ofRoute(previousRoute); - RouterReportManager.reportCurrentRoute(route); - _routeSend?.update((value) { - if (previousRoute is PageRoute) { - value.current = _extractRouteName(previousRoute) ?? ''; - } - - value.args = route.settings.arguments; - value.route = previousRoute; - value.isBack = true; - value.removed = ''; - value.previous = newRoute.name ?? ''; - - value.isBottomSheet = newRoute.isBottomSheet; - value.isDialog = newRoute.isDialog; - }); + if (_isPageRoute(previousRoute)) { + routing.current = _extractRouteName(previousRoute) ?? ''; + } + + routing.args = route.settings.arguments; + routing.route = previousRoute; + routing.isBack = true; + routing.removed = ''; + routing.previous = newRoute.name ?? ''; + routing.isBottomSheet = newRoute.isBottomSheet; + routing.isDialog = newRoute.isDialog; } @override @@ -102,22 +99,18 @@ class StackObserver extends NavigatorObserver { final oldName = _extractRouteName(oldRoute); final currentRoute = _RouteData.ofRoute(oldRoute); - if (newRoute != null) RouterReportManager.reportCurrentRoute(newRoute); - _routeSend?.update((value) { - if (newRoute is PageRoute) { - value.current = newName ?? ''; - } - - value.args = newRoute?.settings.arguments; - value.route = newRoute; - value.isBack = false; - value.removed = ''; - value.previous = '$oldName'; - - value.isBottomSheet = - currentRoute.isBottomSheet ? false : value.isBottomSheet; - value.isDialog = currentRoute.isDialog ? false : value.isDialog; - }); + if (_isPageRoute(newRoute)) { + routing.current = newName ?? ''; + } + + routing.args = newRoute?.settings.arguments; + routing.route = newRoute; + routing.isBack = false; + routing.removed = ''; + routing.previous = '$oldName'; + routing.isBottomSheet = + currentRoute.isBottomSheet ? false : routing.isBottomSheet; + routing.isDialog = currentRoute.isDialog ? false : routing.isDialog; } @override @@ -126,15 +119,12 @@ class StackObserver extends NavigatorObserver { final routeName = _extractRouteName(route); final currentRoute = _RouteData.ofRoute(route); - _routeSend?.update((value) { - value.route = previousRoute; - value.isBack = false; - value.removed = routeName ?? ''; - value.previous = routeName ?? ''; - - value.isBottomSheet = - currentRoute.isBottomSheet ? false : value.isBottomSheet; - value.isDialog = currentRoute.isDialog ? false : value.isDialog; - }); + routing.route = previousRoute; + routing.isBack = false; + routing.removed = routeName ?? ''; + routing.previous = routeName ?? ''; + routing.isBottomSheet = + currentRoute.isBottomSheet ? false : routing.isBottomSheet; + routing.isDialog = currentRoute.isDialog ? false : routing.isDialog; } } diff --git a/lib/src/navigation/route_transition.dart b/lib/src/navigation/route_transition.dart index a229210..2409350 100644 --- a/lib/src/navigation/route_transition.dart +++ b/lib/src/navigation/route_transition.dart @@ -1,4 +1,4 @@ -import 'package:get/get.dart' as G; +import 'package:flutter/material.dart'; enum Transition { fade, @@ -12,30 +12,71 @@ enum Transition { zoom, } -extension ToGetTransition on Transition { - G.Transition get toGet { - switch (this) { - case Transition.fade: - return G.Transition.fade; - case Transition.leftToRight: - return G.Transition.leftToRight; - case Transition.rightToLeft: - return G.Transition.rightToLeft; - case Transition.upToDown: - return G.Transition.upToDown; - case Transition.downToUp: - return G.Transition.downToUp; - case Transition.zoom: - return G.Transition.zoom; - case Transition.leftToRightWithFade: - return G.Transition.leftToRightWithFade; - case Transition.rightToLeftWithFade: - return G.Transition.rightToLeftWithFade; - case Transition.noTransition: - return G.Transition.noTransition; +/// Applies the visual effect for [transition] to [child], driven by +/// [animation]. This is the native (get-free) replacement for the transitions +/// that the `get` package used to provide. +Widget applyTransition( + Transition transition, + Animation animation, + Curve curve, + Widget child, +) { + final curved = CurvedAnimation(parent: animation, curve: curve); - default: - return G.Transition.rightToLeft; - } + Widget slide(Offset begin) => SlideTransition( + position: Tween(begin: begin, end: Offset.zero).animate(curved), + child: child, + ); + + Widget slideWithFade(Offset begin) => FadeTransition( + opacity: curved, + child: slide(begin), + ); + + switch (transition) { + case Transition.fade: + return FadeTransition(opacity: curved, child: child); + case Transition.rightToLeft: + return slide(const Offset(1, 0)); + case Transition.leftToRight: + return slide(const Offset(-1, 0)); + case Transition.upToDown: + return slide(const Offset(0, -1)); + case Transition.downToUp: + return slide(const Offset(0, 1)); + case Transition.zoom: + return ScaleTransition(scale: curved, child: child); + case Transition.rightToLeftWithFade: + return slideWithFade(const Offset(1, 0)); + case Transition.leftToRightWithFade: + return slideWithFade(const Offset(-1, 0)); + case Transition.noTransition: + return child; } } + +/// Builds a [PageRouteBuilder] that renders [page] using the given [transition]. +/// +/// This is the native replacement for `Get.to`'s transition handling. The +/// transition argument map convention used by named navigation lives in +/// `RouteDataV1` (stacked) and is unaffected by this builder. +Route buildTransitionRoute( + Widget page, { + required Transition transition, + Duration duration = const Duration(milliseconds: 300), + bool opaque = true, + bool fullscreenDialog = false, + Curve curve = Curves.linear, + RouteSettings? settings, +}) { + return PageRouteBuilder( + settings: settings, + opaque: opaque, + fullscreenDialog: fullscreenDialog, + transitionDuration: duration, + reverseTransitionDuration: duration, + pageBuilder: (_, __, ___) => page, + transitionsBuilder: (_, animation, __, child) => + applyTransition(transition, animation, curve, child), + ); +} diff --git a/lib/src/navigation/stacked_routing.dart b/lib/src/navigation/stacked_routing.dart new file mode 100644 index 0000000..007f2b6 --- /dev/null +++ b/lib/src/navigation/stacked_routing.dart @@ -0,0 +1,43 @@ +import 'package:flutter/widgets.dart'; + +import 'route_transition.dart'; + +/// Holds the current navigation state, maintained by the `StackObserver`. +/// +/// This is the native (get-free) replacement for `Get.routing`. The +/// [NavigationService] reads [current], [previous] and [args] from here, and +/// the dialog/bottom sheet services read [isDialog] / [isBottomSheet]. +class StackedRouting { + /// Name of the current page route. + String current = ''; + + /// Name of the previous route. + String previous = ''; + + /// Arguments of the most recent route. + dynamic args; + + /// The most recent route involved in a navigation event. + Route? route; + + /// Whether the last navigation event was a back navigation. + bool isBack = false; + + /// Name of the route that was removed (for didRemove events). + String removed = ''; + + /// Whether a bottom sheet is currently open. + bool? isBottomSheet; + + /// Whether a dialog is currently open. + bool? isDialog; +} + +/// Default navigation behaviour, configurable through +/// `NavigationService.config`. Native replacement for `Get.config`. +class StackedNavigationConfig { + Transition defaultTransition = Transition.rightToLeft; + Duration defaultDuration = const Duration(milliseconds: 300); + bool defaultOpaqueRoute = true; + bool defaultPopGesture = false; +} diff --git a/lib/src/snackbar/snackbar_service.dart b/lib/src/snackbar/snackbar_service.dart index f17f984..62c4b76 100644 --- a/lib/src/snackbar/snackbar_service.dart +++ b/lib/src/snackbar/snackbar_service.dart @@ -1,12 +1,13 @@ import 'dart:async'; import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'package:stacked_shared/stacked_shared.dart' as sc; import 'package:stacked_services/src/exceptions/custom_snackbar_exception.dart'; import 'package:stacked_services/src/snackbar/snackbar_config.dart'; import 'stacked_snackbar_customizations.dart'; +import 'vendored/stacked_snackbar.dart'; +import 'vendored/stacked_snackbar_controller.dart'; /// A service that allows the user to show the snackbar from a ViewModel class SnackbarService { @@ -22,7 +23,7 @@ class SnackbarService { SnackbarConfig? _snackbarConfig; /// Checks if there is a snackbar open - bool? get isOpen => Get.isSnackbarOpen; + bool? get isOpen => StackedSnackbarController.isSnackbarBeingShown; /// Saves the [config] to be used for the [showSnackbar] function void registerSnackbarConfig(SnackbarConfig config) => @@ -49,7 +50,7 @@ class SnackbarService { } /// Check if snackbar is open - bool get isSnackbarOpen => Get.isSnackbarOpen; + bool get isSnackbarOpen => StackedSnackbarController.isSnackbarBeingShown; /// Shows a snack bar with the details passed in void showSnackbar({ @@ -66,9 +67,7 @@ class SnackbarService { config: _snackbarConfig, ); - Get.snackbar( - title, - message, + StackedSnackbar( titleText: title.isNotEmpty ? Text( title, @@ -97,39 +96,42 @@ class SnackbarService { textAlign: _snackbarConfig?.messageTextAlign ?? TextAlign.left, ) : SizedBox.shrink(), - shouldIconPulse: _snackbarConfig?.shouldIconPulse, + shouldIconPulse: _snackbarConfig?.shouldIconPulse ?? true, onTap: onTap, - barBlur: _snackbarConfig?.barBlur, + barBlur: _snackbarConfig?.barBlur ?? 7.0, isDismissible: _snackbarConfig?.isDismissible ?? true, - duration: duration ?? _snackbarConfig?.duration, - snackPosition: _snackbarConfig?.snackPosition.toGet, - backgroundColor: _snackbarConfig?.backgroundColor ?? Colors.grey[800], + duration: duration ?? _snackbarConfig?.duration ?? const Duration(seconds: 3), + snackPosition: _snackbarConfig?.snackPosition ?? SnackPosition.TOP, + backgroundColor: _snackbarConfig?.backgroundColor ?? Colors.grey[800]!, margin: _snackbarConfig?.margin ?? const EdgeInsets.symmetric(horizontal: 10, vertical: 25), mainButton: mainButtonWidget, icon: _snackbarConfig?.icon, maxWidth: _snackbarConfig?.maxWidth, - padding: _snackbarConfig?.padding, - borderRadius: _snackbarConfig?.borderRadius, + padding: _snackbarConfig?.padding ?? const EdgeInsets.all(16), + borderRadius: _snackbarConfig?.borderRadius ?? 15, borderColor: _snackbarConfig?.borderColor, borderWidth: _snackbarConfig?.borderWidth, leftBarIndicatorColor: _snackbarConfig?.leftBarIndicatorColor, boxShadows: _snackbarConfig?.boxShadows, backgroundGradient: _snackbarConfig?.backgroundGradient, dismissDirection: _snackbarConfig?.dismissDirection, - showProgressIndicator: _snackbarConfig?.showProgressIndicator, + showProgressIndicator: _snackbarConfig?.showProgressIndicator ?? false, progressIndicatorController: _snackbarConfig?.progressIndicatorController, progressIndicatorBackgroundColor: _snackbarConfig?.progressIndicatorBackgroundColor, progressIndicatorValueColor: _snackbarConfig?.progressIndicatorValueColor, - snackStyle: _snackbarConfig?.snackStyle.toGet, - forwardAnimationCurve: _snackbarConfig?.forwardAnimationCurve, - reverseAnimationCurve: _snackbarConfig?.reverseAnimationCurve, - animationDuration: _snackbarConfig?.animationDuration, - overlayBlur: _snackbarConfig?.overlayBlur, - overlayColor: _snackbarConfig?.overlayColor, + snackStyle: _snackbarConfig?.snackStyle ?? SnackStyle.FLOATING, + forwardAnimationCurve: + _snackbarConfig?.forwardAnimationCurve ?? Curves.easeOutCirc, + reverseAnimationCurve: + _snackbarConfig?.reverseAnimationCurve ?? Curves.easeOutCirc, + animationDuration: + _snackbarConfig?.animationDuration ?? const Duration(seconds: 1), + overlayBlur: _snackbarConfig?.overlayBlur ?? 0.0, + overlayColor: _snackbarConfig?.overlayColor ?? Colors.transparent, userInputForm: _snackbarConfig?.userInputForm, - ); + ).show(); } Future? showCustomSnackBar({ @@ -175,7 +177,7 @@ class SnackbarService { config: snackbarConfig, ); - final getBar = GetSnackBar( + final getBar = StackedSnackbar( key: Key('snackbar_view'), titleText: title != null ? Text( @@ -229,8 +231,8 @@ class SnackbarService { progressIndicatorBackgroundColor: snackbarConfig.progressIndicatorBackgroundColor, progressIndicatorValueColor: snackbarConfig.progressIndicatorValueColor, - snackPosition: snackbarConfig.snackPosition.toGet, - snackStyle: snackbarConfig.snackStyle.toGet, + snackPosition: snackbarConfig.snackPosition, + snackStyle: snackbarConfig.snackStyle, forwardAnimationCurve: snackbarConfig.forwardAnimationCurve, reverseAnimationCurve: snackbarConfig.reverseAnimationCurve, animationDuration: snackbarConfig.animationDuration, @@ -255,7 +257,7 @@ class SnackbarService { /// Close the current snack bar Future closeSnackbar() async { if (isSnackbarOpen) { - return Get.closeCurrentSnackbar(); + return StackedSnackbarController.closeCurrentSnackbar(); } } diff --git a/lib/src/snackbar/stacked_snackbar_customizations.dart b/lib/src/snackbar/stacked_snackbar_customizations.dart index 3033983..1b65357 100644 --- a/lib/src/snackbar/stacked_snackbar_customizations.dart +++ b/lib/src/snackbar/stacked_snackbar_customizations.dart @@ -1,31 +1,5 @@ -import 'package:get/get.dart' as G; - /// Indicates if snack is going to start at the [TOP] or at the [BOTTOM] enum SnackPosition { TOP, BOTTOM } /// Indicates if snack will be attached to the edge of the screen or not enum SnackStyle { FLOATING, GROUNDED } - -extension ToGetSnackPosition on SnackPosition { - G.SnackPosition get toGet { - switch (this) { - case SnackPosition.TOP: - return G.SnackPosition.TOP; - - default: - return G.SnackPosition.BOTTOM; - } - } -} - -extension ToGetSnackStyle on SnackStyle { - G.SnackStyle get toGet { - switch (this) { - case SnackStyle.GROUNDED: - return G.SnackStyle.GROUNDED; - - default: - return G.SnackStyle.FLOATING; - } - } -} diff --git a/lib/src/snackbar/vendored/stacked_snackbar.dart b/lib/src/snackbar/vendored/stacked_snackbar.dart new file mode 100644 index 0000000..e95d535 --- /dev/null +++ b/lib/src/snackbar/vendored/stacked_snackbar.dart @@ -0,0 +1,534 @@ +// ignore_for_file: constant_identifier_names +// +// This file is adapted from the `get` package (GetSnackBar widget), +// Copyright (c) 2020 Jonny Borges, distributed under the MIT License +// (https://github.com/jonataslaw/getx/blob/master/LICENSE). It has been +// vendored into stacked_services to remove the runtime dependency on `get`. +// The behaviour and appearance are intentionally kept identical. + +import 'dart:async'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:flutter/scheduler.dart'; +import 'package:stacked_shared/stacked_shared.dart'; + +import '../stacked_snackbar_customizations.dart'; +import 'stacked_snackbar_controller.dart'; + +typedef OnTap = void Function(StackedSnackbar snack); + +typedef SnackbarStatusCallback = void Function(SnackbarStatus? status); + +class StackedSnackbar extends StatefulWidget { + /// A callback for you to listen to the different Snack status + final SnackbarStatusCallback? snackbarStatus; + + /// The title displayed to the user + final String? title; + + /// The direction in which the SnackBar can be dismissed. + final DismissDirection? dismissDirection; + + /// The message displayed to the user. + final String? message; + + /// Replaces [title]. + final Widget? titleText; + + /// Replaces [message]. + final Widget? messageText; + + /// Will be ignored if [backgroundGradient] is not null + final Color backgroundColor; + + /// If not null, shows a left vertical colored bar on notification. + final Color? leftBarIndicatorColor; + + /// The shadows generated by Snack. + final List? boxShadows; + + /// Give to Snackbar a gradient background. Makes [backgroundColor] ignored. + final Gradient? backgroundGradient; + + /// Icon shown as indication of the message kind. + final Widget? icon; + + /// An option to animate the icon (if present). Defaults to true. + final bool shouldIconPulse; + + /// An action that the user can take based on the snack bar. + final Widget? mainButton; + + /// A callback that registers the user's click anywhere. + final OnTap? onTap; + + /// How long until Snack will hide itself. Null makes it indefinite. + final Duration? duration; + + /// True if you want to show a [LinearProgressIndicator]. + final bool showProgressIndicator; + + /// An optional [AnimationController] to control the [LinearProgressIndicator]. + final AnimationController? progressIndicatorController; + + /// A [LinearProgressIndicator] configuration parameter. + final Color? progressIndicatorBackgroundColor; + + /// A [LinearProgressIndicator] configuration parameter. + final Animation? progressIndicatorValueColor; + + /// Determines if the user can swipe or tap the overlay to dismiss. + final bool isDismissible; + + /// Used to limit Snack width (usually on large screens) + final double? maxWidth; + + /// Adds a custom margin to Snack + final EdgeInsets margin; + + /// Adds a custom padding to Snack + final EdgeInsets padding; + + /// Adds a radius to all corners of Snack. + final double borderRadius; + + /// Adds a border to every side of Snack + final Color? borderColor; + + /// Changes the width of the border if [borderColor] is specified + final double? borderWidth; + + /// Snack can be based on [SnackPosition.TOP] or [SnackPosition.BOTTOM]. + final SnackPosition snackPosition; + + /// Snack can be floating or be grounded to the edge of the screen. + final SnackStyle snackStyle; + + /// The [Curve] animation used when show() is called. + final Curve forwardAnimationCurve; + + /// The [Curve] animation used when dismiss() is called. + final Curve reverseAnimationCurve; + + /// Use it to speed up or slow down the animation duration + final Duration animationDuration; + + /// If different than 0.0, blurs only Snack's background. + final double barBlur; + + /// If different than 0.0, creates a blurred overlay. + final double overlayBlur; + + /// Only takes effect if [overlayBlur] > 0.0. + final Color? overlayColor; + + /// A [Form] in case you want a simple user input. + final Form? userInputForm; + + const StackedSnackbar({ + Key? key, + this.title, + this.message, + this.titleText, + this.messageText, + this.icon, + this.shouldIconPulse = true, + this.maxWidth, + this.margin = const EdgeInsets.all(0.0), + this.padding = const EdgeInsets.all(16), + this.borderRadius = 0.0, + this.borderColor, + this.borderWidth = 1.0, + this.backgroundColor = const Color(0xFF303030), + this.leftBarIndicatorColor, + this.boxShadows, + this.backgroundGradient, + this.mainButton, + this.onTap, + this.duration, + this.isDismissible = true, + this.dismissDirection, + this.showProgressIndicator = false, + this.progressIndicatorController, + this.progressIndicatorBackgroundColor, + this.progressIndicatorValueColor, + this.snackPosition = SnackPosition.BOTTOM, + this.snackStyle = SnackStyle.FLOATING, + this.forwardAnimationCurve = Curves.easeOutCirc, + this.reverseAnimationCurve = Curves.easeOutCirc, + this.animationDuration = const Duration(seconds: 1), + this.barBlur = 0.0, + this.overlayBlur = 0.0, + this.overlayColor = Colors.transparent, + this.userInputForm, + this.snackbarStatus, + }) : super(key: key); + + @override + State createState() => StackedSnackbarState(); + + /// Show the snack. It's called [SnackbarStatus.OPENING] state + /// followed by [SnackbarStatus.OPEN] + StackedSnackbarController show() { + final controller = StackedSnackbarController(this); + controller.show(); + return controller; + } +} + +class StackedSnackbarState extends State + with TickerProviderStateMixin { + AnimationController? _fadeController; + late Animation _fadeAnimation; + + final Widget _emptyWidget = const SizedBox(width: 0.0, height: 0.0); + final double _initialOpacity = 1.0; + final double _finalOpacity = 0.4; + + final Duration _pulseAnimationDuration = const Duration(seconds: 1); + + late bool _isTitlePresent; + late double _messageTopMargin; + + FocusScopeNode? _focusNode; + late FocusAttachment _focusAttachment; + + final Completer _boxHeightCompleter = Completer(); + + late CurvedAnimation _progressAnimation; + + final _backgroundBoxKey = GlobalKey(); + + double get buttonPadding { + if (widget.padding.right - 12 < 0) { + return 4; + } else { + return widget.padding.right - 12; + } + } + + _RowStyle get _rowStyle { + if (widget.mainButton != null && widget.icon == null) { + return _RowStyle.action; + } else if (widget.mainButton == null && widget.icon != null) { + return _RowStyle.icon; + } else if (widget.mainButton != null && widget.icon != null) { + return _RowStyle.all; + } else { + return _RowStyle.none; + } + } + + @override + Widget build(BuildContext context) { + return Align( + heightFactor: 1.0, + child: Material( + color: widget.snackStyle == SnackStyle.FLOATING + ? Colors.transparent + : widget.backgroundColor, + child: SafeArea( + minimum: widget.snackPosition == SnackPosition.BOTTOM + ? EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom) + : EdgeInsets.only(top: MediaQuery.of(context).padding.top), + bottom: widget.snackPosition == SnackPosition.BOTTOM, + top: widget.snackPosition == SnackPosition.TOP, + left: false, + right: false, + child: Stack( + children: [ + FutureBuilder( + future: _boxHeightCompleter.future, + builder: (context, snapshot) { + if (snapshot.hasData) { + if (widget.barBlur == 0) { + return _emptyWidget; + } + return ClipRRect( + borderRadius: BorderRadius.circular(widget.borderRadius), + child: BackdropFilter( + filter: ImageFilter.blur( + sigmaX: widget.barBlur, sigmaY: widget.barBlur), + child: Container( + height: snapshot.data!.height, + width: snapshot.data!.width, + decoration: BoxDecoration( + color: Colors.transparent, + borderRadius: + BorderRadius.circular(widget.borderRadius), + ), + ), + ), + ); + } else { + return _emptyWidget; + } + }, + ), + if (widget.userInputForm != null) + _containerWithForm() + else + _containerWithoutForm() + ], + ), + ), + ), + ); + } + + @override + void dispose() { + _fadeController?.dispose(); + widget.progressIndicatorController?.removeListener(_updateProgress); + widget.progressIndicatorController?.dispose(); + + _focusAttachment.detach(); + _focusNode!.dispose(); + super.dispose(); + } + + @override + void initState() { + super.initState(); + + assert( + widget.userInputForm != null || + ((widget.message != null && widget.message!.isNotEmpty) || + widget.messageText != null), + ''' +You need to either use message[String], or messageText[Widget] or define a userInputForm[Form] in Snackbar'''); + + _isTitlePresent = (widget.title != null || widget.titleText != null); + _messageTopMargin = _isTitlePresent ? 6.0 : widget.padding.top; + + _configureLeftBarFuture(); + _configureProgressIndicatorAnimation(); + + if (widget.icon != null && widget.shouldIconPulse) { + _configurePulseAnimation(); + _fadeController?.forward(); + } + + _focusNode = FocusScopeNode(); + _focusAttachment = _focusNode!.attach(context); + } + + Widget _buildLeftBarIndicator() { + if (widget.leftBarIndicatorColor != null) { + return FutureBuilder( + future: _boxHeightCompleter.future, + builder: (buildContext, snapshot) { + if (snapshot.hasData) { + return Container( + color: widget.leftBarIndicatorColor, + width: 5.0, + height: snapshot.data!.height, + ); + } else { + return _emptyWidget; + } + }, + ); + } else { + return _emptyWidget; + } + } + + void _configureLeftBarFuture() { + ambiguate(SchedulerBinding.instance)?.addPostFrameCallback( + (_) { + final keyContext = _backgroundBoxKey.currentContext; + if (keyContext != null) { + final box = keyContext.findRenderObject() as RenderBox; + _boxHeightCompleter.complete(box.size); + } + }, + ); + } + + void _configureProgressIndicatorAnimation() { + if (widget.showProgressIndicator && + widget.progressIndicatorController != null) { + widget.progressIndicatorController!.addListener(_updateProgress); + + _progressAnimation = CurvedAnimation( + curve: Curves.linear, parent: widget.progressIndicatorController!); + } + } + + void _configurePulseAnimation() { + _fadeController = + AnimationController(vsync: this, duration: _pulseAnimationDuration); + _fadeAnimation = Tween(begin: _initialOpacity, end: _finalOpacity).animate( + CurvedAnimation( + parent: _fadeController!, + curve: Curves.linear, + ), + ); + + _fadeController!.addStatusListener((status) { + if (status == AnimationStatus.completed) { + _fadeController!.reverse(); + } + if (status == AnimationStatus.dismissed) { + _fadeController!.forward(); + } + }); + + _fadeController!.forward(); + } + + Widget _containerWithForm() { + return Container( + key: _backgroundBoxKey, + constraints: widget.maxWidth != null + ? BoxConstraints(maxWidth: widget.maxWidth!) + : null, + decoration: BoxDecoration( + color: widget.backgroundColor, + gradient: widget.backgroundGradient, + boxShadow: widget.boxShadows, + borderRadius: BorderRadius.circular(widget.borderRadius), + border: widget.borderColor != null + ? Border.all( + color: widget.borderColor!, + width: widget.borderWidth!, + ) + : null, + ), + child: Padding( + padding: + const EdgeInsets.only(left: 8.0, right: 8.0, bottom: 8.0, top: 16.0), + child: FocusScope( + node: _focusNode, + autofocus: true, + child: widget.userInputForm!, + ), + ), + ); + } + + Widget _containerWithoutForm() { + final iconPadding = widget.padding.left > 16.0 ? widget.padding.left : 0.0; + final left = _rowStyle == _RowStyle.icon || _rowStyle == _RowStyle.all + ? 4.0 + : widget.padding.left; + final right = _rowStyle == _RowStyle.action || _rowStyle == _RowStyle.all + ? 8.0 + : widget.padding.right; + return Container( + key: _backgroundBoxKey, + constraints: widget.maxWidth != null + ? BoxConstraints(maxWidth: widget.maxWidth!) + : null, + decoration: BoxDecoration( + color: widget.backgroundColor, + gradient: widget.backgroundGradient, + boxShadow: widget.boxShadows, + borderRadius: BorderRadius.circular(widget.borderRadius), + border: widget.borderColor != null + ? Border.all(color: widget.borderColor!, width: widget.borderWidth!) + : null, + ), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + widget.showProgressIndicator + ? LinearProgressIndicator( + value: widget.progressIndicatorController != null + ? _progressAnimation.value + : null, + backgroundColor: widget.progressIndicatorBackgroundColor, + valueColor: widget.progressIndicatorValueColor, + ) + : _emptyWidget, + Row( + mainAxisSize: MainAxisSize.max, + children: [ + _buildLeftBarIndicator(), + if (_rowStyle == _RowStyle.icon || _rowStyle == _RowStyle.all) + ConstrainedBox( + constraints: + BoxConstraints.tightFor(width: 42.0 + iconPadding), + child: _getIcon(), + ), + Expanded( + flex: 1, + child: Column( + crossAxisAlignment: CrossAxisAlignment.stretch, + mainAxisSize: MainAxisSize.min, + children: [ + if (_isTitlePresent) + Padding( + padding: EdgeInsets.only( + top: widget.padding.top, + left: left, + right: right, + ), + child: widget.titleText ?? + Text( + widget.title ?? "", + style: const TextStyle( + fontSize: 16.0, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ) + else + _emptyWidget, + Padding( + padding: EdgeInsets.only( + top: _messageTopMargin, + left: left, + right: right, + bottom: widget.padding.bottom, + ), + child: widget.messageText ?? + Text( + widget.message ?? "", + style: const TextStyle( + fontSize: 14.0, color: Colors.white), + ), + ), + ], + ), + ), + if (_rowStyle == _RowStyle.action || _rowStyle == _RowStyle.all) + Padding( + padding: EdgeInsets.only(right: buttonPadding), + child: widget.mainButton, + ), + ], + ), + ], + ), + ); + } + + Widget? _getIcon() { + if (widget.icon != null && widget.icon is Icon && widget.shouldIconPulse) { + return FadeTransition( + opacity: _fadeAnimation, + child: widget.icon, + ); + } else if (widget.icon != null) { + return widget.icon; + } else { + return _emptyWidget; + } + } + + void _updateProgress() => setState(() {}); +} + +enum _RowStyle { + icon, + action, + all, + none, +} + +/// Indicates Status of snackbar +enum SnackbarStatus { OPEN, CLOSED, OPENING, CLOSING } diff --git a/lib/src/snackbar/vendored/stacked_snackbar_controller.dart b/lib/src/snackbar/vendored/stacked_snackbar_controller.dart new file mode 100644 index 0000000..b94252d --- /dev/null +++ b/lib/src/snackbar/vendored/stacked_snackbar_controller.dart @@ -0,0 +1,405 @@ +// This file is adapted from the `get` package (SnackbarController), +// Copyright (c) 2020 Jonny Borges, distributed under the MIT License +// (https://github.com/jonataslaw/getx/blob/master/LICENSE). It has been +// vendored into stacked_services to remove the runtime dependency on `get`. +// The overlay host now comes from [StackedService.navigatorKey] and a small +// internal queue replaces `GetQueue`. + +import 'dart:async'; +import 'dart:math'; +import 'dart:ui'; + +import 'package:flutter/material.dart'; +import 'package:stacked_services/src/stacked_service.dart'; + +import '../stacked_snackbar_customizations.dart'; +import 'stacked_snackbar.dart'; + +class StackedSnackbarController { + static final _snackBarQueue = _SnackBarQueue(); + static bool get isSnackbarBeingShown => _snackBarQueue._isJobInProgress; + final key = GlobalKey(); + + late Animation _filterBlurAnimation; + late Animation _filterColorAnimation; + + final StackedSnackbar snackbar; + final _transitionCompleter = Completer(); + + late SnackbarStatusCallback? _snackbarStatus; + late final Alignment? _initialAlignment; + late final Alignment? _endAlignment; + + bool _wasDismissedBySwipe = false; + + bool _onTappedDismiss = false; + + Timer? _timer; + + late final Animation _animation; + + late final AnimationController _controller; + + SnackbarStatus? _currentStatus; + + final _overlayEntries = []; + + OverlayState? _overlayState; + + StackedSnackbarController(this.snackbar); + + Future get future => _transitionCompleter.future; + + /// Close the snackbar with animation + Future close({bool withAnimations = true}) async { + if (!withAnimations) { + _removeOverlay(); + return; + } + _removeEntry(); + await future; + } + + /// Adds Snackbar to a view queue. + /// Only one Snackbar will be displayed at a time, and this method returns + /// a future to when the snackbar disappears. + Future show() { + return _snackBarQueue._addJob(this); + } + + void _cancelTimer() { + if (_timer != null && _timer!.isActive) { + _timer!.cancel(); + } + } + + void _configureAlignment(SnackPosition snackPosition) { + switch (snackbar.snackPosition) { + case SnackPosition.TOP: + { + _initialAlignment = const Alignment(-1.0, -2.0); + _endAlignment = const Alignment(-1.0, -1.0); + break; + } + case SnackPosition.BOTTOM: + { + _initialAlignment = const Alignment(-1.0, 2.0); + _endAlignment = const Alignment(-1.0, 1.0); + break; + } + } + } + + void _configureOverlay() { + _overlayState = StackedService.navigatorKey!.currentState!.overlay; + _overlayEntries.clear(); + _overlayEntries.addAll(_createOverlayEntries(_getBodyWidget())); + _overlayState!.insertAll(_overlayEntries); + _configureSnackBarDisplay(); + } + + void _configureSnackBarDisplay() { + assert(!_transitionCompleter.isCompleted, + 'Cannot configure a snackbar after disposing it.'); + _controller = _createAnimationController(); + _configureAlignment(snackbar.snackPosition); + _snackbarStatus = snackbar.snackbarStatus; + _filterBlurAnimation = _createBlurFilterAnimation(); + _filterColorAnimation = _createColorOverlayColor(); + _animation = _createAnimation(); + _animation.addStatusListener(_handleStatusChanged); + _configureTimer(); + _controller.forward(); + } + + void _configureTimer() { + if (snackbar.duration != null) { + if (_timer != null && _timer!.isActive) { + _timer!.cancel(); + } + _timer = Timer(snackbar.duration!, _removeEntry); + } else { + if (_timer != null) { + _timer!.cancel(); + } + } + } + + Animation _createAnimation() { + assert(!_transitionCompleter.isCompleted, + 'Cannot create a animation from a disposed snackbar'); + return AlignmentTween(begin: _initialAlignment, end: _endAlignment).animate( + CurvedAnimation( + parent: _controller, + curve: snackbar.forwardAnimationCurve, + reverseCurve: snackbar.reverseAnimationCurve, + ), + ); + } + + AnimationController _createAnimationController() { + assert(!_transitionCompleter.isCompleted, + 'Cannot create a animationController from a disposed snackbar'); + assert(snackbar.animationDuration >= Duration.zero); + return AnimationController( + duration: snackbar.animationDuration, + debugLabel: '$runtimeType', + vsync: _overlayState!, + ); + } + + Animation _createBlurFilterAnimation() { + return Tween(begin: 0.0, end: snackbar.overlayBlur).animate( + CurvedAnimation( + parent: _controller, + curve: const Interval( + 0.0, + 0.35, + curve: Curves.easeInOutCirc, + ), + ), + ); + } + + Animation _createColorOverlayColor() { + return ColorTween(begin: const Color(0x00000000), end: snackbar.overlayColor) + .animate( + CurvedAnimation( + parent: _controller, + curve: const Interval( + 0.0, + 0.35, + curve: Curves.easeInOutCirc, + ), + ), + ); + } + + Iterable _createOverlayEntries(Widget child) { + return [ + if (snackbar.overlayBlur > 0.0) ...[ + OverlayEntry( + builder: (context) => GestureDetector( + onTap: () { + if (snackbar.isDismissible && !_onTappedDismiss) { + _onTappedDismiss = true; + close(); + } + }, + child: AnimatedBuilder( + animation: _filterBlurAnimation, + builder: (context, child) { + return BackdropFilter( + filter: ImageFilter.blur( + sigmaX: max(0.001, _filterBlurAnimation.value), + sigmaY: max(0.001, _filterBlurAnimation.value), + ), + child: Container( + constraints: const BoxConstraints.expand(), + color: _filterColorAnimation.value, + ), + ); + }, + ), + ), + maintainState: false, + opaque: false, + ), + ], + OverlayEntry( + builder: (context) => Semantics( + focused: false, + container: true, + explicitChildNodes: true, + child: AlignTransition( + alignment: _animation, + child: snackbar.isDismissible + ? _getDismissibleSnack(child) + : _getSnackbarContainer(child), + ), + ), + maintainState: false, + opaque: false, + ), + ]; + } + + Widget _getBodyWidget() { + return Builder(builder: (_) { + return GestureDetector( + onTap: + snackbar.onTap != null ? () => snackbar.onTap?.call(snackbar) : null, + child: snackbar, + ); + }); + } + + DismissDirection _getDefaultDismissDirection() { + if (snackbar.snackPosition == SnackPosition.TOP) { + return DismissDirection.up; + } + return DismissDirection.down; + } + + Widget _getDismissibleSnack(Widget child) { + return Dismissible( + direction: snackbar.dismissDirection ?? _getDefaultDismissDirection(), + resizeDuration: null, + confirmDismiss: (_) { + if (_currentStatus == SnackbarStatus.OPENING || + _currentStatus == SnackbarStatus.CLOSING) { + return Future.value(false); + } + return Future.value(true); + }, + key: const Key('dismissible'), + onDismissed: (_) { + _wasDismissedBySwipe = true; + _removeEntry(); + }, + child: _getSnackbarContainer(child), + ); + } + + Widget _getSnackbarContainer(Widget child) { + return Container( + margin: snackbar.margin, + child: child, + ); + } + + void _handleStatusChanged(AnimationStatus status) { + switch (status) { + case AnimationStatus.completed: + _currentStatus = SnackbarStatus.OPEN; + _snackbarStatus?.call(_currentStatus); + if (_overlayEntries.isNotEmpty) _overlayEntries.first.opaque = false; + + break; + case AnimationStatus.forward: + _currentStatus = SnackbarStatus.OPENING; + _snackbarStatus?.call(_currentStatus); + break; + case AnimationStatus.reverse: + _currentStatus = SnackbarStatus.CLOSING; + _snackbarStatus?.call(_currentStatus); + if (_overlayEntries.isNotEmpty) _overlayEntries.first.opaque = false; + break; + case AnimationStatus.dismissed: + assert(!_overlayEntries.first.opaque); + _currentStatus = SnackbarStatus.CLOSED; + _snackbarStatus?.call(_currentStatus); + _removeOverlay(); + break; + } + } + + void _removeEntry() { + assert( + !_transitionCompleter.isCompleted, + 'Cannot remove entry from a disposed snackbar', + ); + + _cancelTimer(); + + if (_wasDismissedBySwipe) { + Timer(const Duration(milliseconds: 200), _controller.reset); + _wasDismissedBySwipe = false; + } else { + _controller.reverse(); + } + } + + void _removeOverlay() { + for (var element in _overlayEntries) { + element.remove(); + } + + assert(!_transitionCompleter.isCompleted, + 'Cannot remove overlay from a disposed snackbar'); + _controller.dispose(); + _overlayEntries.clear(); + _transitionCompleter.complete(); + } + + Future _show() { + _configureOverlay(); + return future; + } + + static void cancelAllSnackbars() { + _snackBarQueue._cancelAllJobs(); + } + + static Future closeCurrentSnackbar() async { + await _snackBarQueue._closeCurrentJob(); + } +} + +class _SnackBarQueue { + final _queue = _Queue(); + final _snackbarList = []; + + StackedSnackbarController? get _currentSnackbar { + if (_snackbarList.isEmpty) return null; + return _snackbarList.first; + } + + bool get _isJobInProgress => _snackbarList.isNotEmpty; + + Future _addJob(StackedSnackbarController job) async { + _snackbarList.add(job); + final data = await _queue.add(job._show); + _snackbarList.remove(job); + return data; + } + + Future _cancelAllJobs() async { + await _currentSnackbar?.close(); + _queue.cancelAllJobs(); + _snackbarList.clear(); + } + + Future _closeCurrentJob() async { + if (_currentSnackbar == null) return; + await _currentSnackbar!.close(); + } +} + +/// Minimal serial job queue, vendored from `get`'s GetQueue. +class _Queue { + final List<_Item> _queue = []; + bool _active = false; + + Future add(Function job) { + var completer = Completer(); + _queue.add(_Item(completer, job)); + _check(); + return completer.future; + } + + void cancelAllJobs() { + _queue.clear(); + } + + void _check() async { + if (!_active && _queue.isNotEmpty) { + _active = true; + var item = _queue.removeAt(0); + try { + item.completer.complete(await item.job()); + } on Exception catch (e) { + item.completer.completeError(e); + } + _active = false; + _check(); + } + } +} + +class _Item { + final dynamic completer; + final dynamic job; + + _Item(this.completer, this.job); +} diff --git a/lib/src/stacked_service.dart b/lib/src/stacked_service.dart index 3e45532..687e8fb 100644 --- a/lib/src/stacked_service.dart +++ b/lib/src/stacked_service.dart @@ -1,19 +1,33 @@ import 'package:flutter/material.dart'; -import 'package:get/get.dart'; import 'navigation/route_observer.dart'; +import 'navigation/stacked_routing.dart'; /// This service exposes properties that is required to be set before any of the services can be used class StackedService { const StackedService._(); - /// Returns the [Get.key] value to be set in the applications navigation - static GlobalKey? get navigatorKey => Get.key; + static final GlobalKey _navigatorKey = + GlobalKey(); - /// Creates and/or returns a new navigator key based on the index passed in + /// The navigation key used by the application's [MaterialApp]. + static GlobalKey? get navigatorKey => _navigatorKey; + + static final Map> _nestedNavigationKeys = {}; + + /// Creates and/or returns a navigator key based on the index passed in static GlobalKey? nestedNavigationKey(int index) => - Get.nestedKey(index); + _nestedNavigationKeys.putIfAbsent(index, () => GlobalKey()); + + /// The routing state maintained by [routeObserver]. Native replacement for + /// `Get.routing`. + static final StackedRouting routing = StackedRouting(); + + /// Default navigation behaviour, configured through `NavigationService.config`. + static final StackedNavigationConfig navigationConfig = + StackedNavigationConfig(); - /// Returns the [GetObserver] to be passed through navigatorObservers in MaterialApp to use all the functionalities - static NavigatorObserver get routeObserver => StackObserver(); + /// Returns the [NavigatorObserver] to be passed through navigatorObservers in + /// MaterialApp to keep the routing state up to date. + static NavigatorObserver get routeObserver => StackObserver(routing: routing); } diff --git a/lib/stacked_services.dart b/lib/stacked_services.dart index 9d6c617..02f38e4 100644 --- a/lib/stacked_services.dart +++ b/lib/stacked_services.dart @@ -6,6 +6,7 @@ export 'src/models/overlay_request.dart'; export 'src/models/overlay_response.dart'; export 'src/navigation/navigation_service.dart'; export 'src/navigation/route_transition.dart'; +export 'src/navigation/stacked_routing.dart'; export 'src/navigation/router_service.dart'; export 'src/snackbar/snackbar_config.dart'; export 'src/snackbar/snackbar_service.dart'; diff --git a/pubspec.yaml b/pubspec.yaml index fcf4fa8..c3a12ad 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -13,9 +13,6 @@ dependencies: stacked_shared: ^1.3.2 stacked: ^3.4.0 - # navigation - get: ^4.6.6 - crypto: ^3.0.1 dev_dependencies: diff --git a/test/bottom_sheet/bottom_sheet_service_test.dart b/test/bottom_sheet/bottom_sheet_service_test.dart new file mode 100644 index 0000000..639c6ab --- /dev/null +++ b/test/bottom_sheet/bottom_sheet_service_test.dart @@ -0,0 +1,120 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../helpers/test_app.dart'; + +void main() { + group('BottomSheetService -', () { + late BottomSheetService service; + + setUp(() { + service = BottomSheetService(); + }); + + Future pumpApp(WidgetTester tester) async { + await tester.pumpWidget(buildTestApp()); + await tester.pumpAndSettle(); + } + + testWidgets('showBottomSheet renders title, description and buttons', + (tester) async { + await pumpApp(tester); + + final future = service.showBottomSheet( + title: 'Sheet title', + description: 'Sheet description', + confirmButtonTitle: 'Confirm', + cancelButtonTitle: 'Cancel', + ); + await tester.pumpAndSettle(); + + expect(find.text('Sheet title'), findsOneWidget); + expect(find.text('Sheet description'), findsOneWidget); + expect(find.text('Confirm'), findsOneWidget); + expect(find.text('Cancel'), findsOneWidget); + + await tester.tap(find.text('Confirm')); + await tester.pumpAndSettle(); + await future; + }); + + testWidgets('confirm completes with confirmed = true', (tester) async { + await pumpApp(tester); + + final future = service.showBottomSheet( + title: 'Sheet title', + confirmButtonTitle: 'Confirm', + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Confirm')); + await tester.pumpAndSettle(); + + final response = await future; + expect(response?.confirmed, isTrue); + }); + + testWidgets('cancel completes with confirmed = false', (tester) async { + await pumpApp(tester); + + final future = service.showBottomSheet( + title: 'Sheet title', + confirmButtonTitle: 'Confirm', + cancelButtonTitle: 'Cancel', + ); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Cancel')); + await tester.pumpAndSettle(); + + final response = await future; + expect(response?.confirmed, isFalse); + }); + + testWidgets('isBottomSheetOpen toggles while a sheet is visible', + (tester) async { + await pumpApp(tester); + expect(service.isBottomSheetOpen, isFalse); + + final future = service.showBottomSheet( + title: 'Sheet title', + confirmButtonTitle: 'Confirm', + ); + await tester.pumpAndSettle(); + expect(service.isBottomSheetOpen, isTrue); + + await tester.tap(find.text('Confirm')); + await tester.pumpAndSettle(); + await future; + expect(service.isBottomSheetOpen, isFalse); + }); + + testWidgets('showCustomSheet uses the registered builder and completes', + (tester) async { + service.setCustomSheetBuilders({ + 'basic': (context, request, completer) => ElevatedButton( + key: const Key('custom_sheet_button'), + onPressed: () => completer(SheetResponse(confirmed: true)), + child: Text(request.title ?? ''), + ), + }); + + await pumpApp(tester); + + final future = service.showCustomSheet( + variant: 'basic', + title: 'Custom sheet', + ); + await tester.pumpAndSettle(); + + expect(find.text('Custom sheet'), findsOneWidget); + + await tester.tap(find.byKey(const Key('custom_sheet_button'))); + await tester.pumpAndSettle(); + + final response = await future; + expect(response?.confirmed, isTrue); + }); + }); +} diff --git a/test/dialog/dialog_service_test.dart b/test/dialog/dialog_service_test.dart new file mode 100644 index 0000000..7e5dbfd --- /dev/null +++ b/test/dialog/dialog_service_test.dart @@ -0,0 +1,125 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../helpers/test_app.dart'; + +void main() { + group('DialogService -', () { + late DialogService service; + + setUp(() { + service = DialogService(); + }); + + Future pumpApp(WidgetTester tester) async { + await tester.pumpWidget(buildTestApp()); + await tester.pumpAndSettle(); + } + + testWidgets('showDialog renders title, description and button', + (tester) async { + await pumpApp(tester); + + final future = service.showDialog( + title: 'Title', + description: 'Description', + buttonTitle: 'Confirm', + dialogPlatform: DialogPlatform.Material, + ); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('dialog_view')), findsOneWidget); + expect(find.text('Title'), findsOneWidget); + expect(find.text('Description'), findsOneWidget); + expect(find.text('Confirm'), findsOneWidget); + + await tester.tap(find.byKey(const Key('dialog_touchable_confirm'))); + await tester.pumpAndSettle(); + await future; + }); + + testWidgets('confirm button completes with confirmed = true', + (tester) async { + await pumpApp(tester); + + final future = service.showDialog( + title: 'Title', + buttonTitle: 'Confirm', + dialogPlatform: DialogPlatform.Material, + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key('dialog_touchable_confirm'))); + await tester.pumpAndSettle(); + + final response = await future; + expect(response?.confirmed, isTrue); + }); + + testWidgets('cancel button completes with confirmed = false', + (tester) async { + await pumpApp(tester); + + final future = service.showConfirmationDialog( + title: 'Title', + cancelTitle: 'Cancel', + confirmationTitle: 'Confirm', + dialogPlatform: DialogPlatform.Material, + ); + await tester.pumpAndSettle(); + + await tester.tap(find.byKey(const Key('dialog_touchable_cancel'))); + await tester.pumpAndSettle(); + + final response = await future; + expect(response?.confirmed, isFalse); + }); + + testWidgets('isDialogOpen toggles while a dialog is visible', + (tester) async { + await pumpApp(tester); + expect(service.isDialogOpen, isFalse); + + final future = service.showDialog( + title: 'Title', + buttonTitle: 'Confirm', + dialogPlatform: DialogPlatform.Material, + ); + await tester.pumpAndSettle(); + expect(service.isDialogOpen, isTrue); + + await tester.tap(find.byKey(const Key('dialog_touchable_confirm'))); + await tester.pumpAndSettle(); + await future; + expect(service.isDialogOpen, isFalse); + }); + + testWidgets('showCustomDialog uses the registered builder and completes', + (tester) async { + service.registerCustomDialogBuilders({ + 'basic': (context, request, completer) => ElevatedButton( + key: const Key('custom_dialog_button'), + onPressed: () => completer(DialogResponse(confirmed: true)), + child: Text(request.title ?? ''), + ), + }); + + await pumpApp(tester); + + final future = service.showCustomDialog( + variant: 'basic', + title: 'Custom', + ); + await tester.pumpAndSettle(); + + expect(find.text('Custom'), findsOneWidget); + + await tester.tap(find.byKey(const Key('custom_dialog_button'))); + await tester.pumpAndSettle(); + + final response = await future; + expect(response?.confirmed, isTrue); + }); + }); +} diff --git a/test/helpers/test_app.dart b/test/helpers/test_app.dart new file mode 100644 index 0000000..00130d9 --- /dev/null +++ b/test/helpers/test_app.dart @@ -0,0 +1,68 @@ +import 'package:flutter/material.dart'; +import 'package:stacked_services/stacked_services.dart'; + +/// Named routes used across the navigation tests. +class TestRoutes { + static const String home = '/'; + static const String second = '/second'; + static const String third = '/third'; +} + +/// A simple screen that is uniquely identifiable both by [Key] and by text. +class TestScreen extends StatelessWidget { + final String name; + const TestScreen(this.name, {Key? key}) : super(key: key); + + static Key keyFor(String name) => Key('screen_$name'); + + @override + Widget build(BuildContext context) { + return Scaffold( + key: keyFor(name), + body: Center(child: Text(name, key: Key('text_$name'))), + ); + } +} + +/// [onGenerateRoute] used by the test [MaterialApp] so that named navigation +/// (toNamed/offNamed/offAllNamed) resolves to deterministic screens. +Route? testOnGenerateRoute(RouteSettings settings) { + final name = settings.name ?? TestRoutes.home; + String screenName; + switch (name) { + case TestRoutes.home: + screenName = 'home'; + break; + case TestRoutes.second: + screenName = 'second'; + break; + case TestRoutes.third: + screenName = 'third'; + break; + default: + screenName = 'unknown'; + } + return MaterialPageRoute( + builder: (_) => TestScreen(screenName), + settings: settings, + ); +} + +/// Builds a [MaterialApp] wired exactly like a consumer wires `stacked_services`: +/// using [StackedService.navigatorKey], [StackedService.routeObserver] and an +/// [onGenerateRoute] for named navigation. +/// +/// This intentionally references the public `stacked_services` surface only, so +/// the same tests keep working before and after the `get` removal. +Widget buildTestApp({ + List? observers, + String initialRoute = TestRoutes.home, + Route? Function(RouteSettings)? onGenerateRoute, +}) { + return MaterialApp( + navigatorKey: StackedService.navigatorKey, + navigatorObservers: observers ?? [StackedService.routeObserver], + onGenerateRoute: onGenerateRoute ?? testOnGenerateRoute, + initialRoute: initialRoute, + ); +} diff --git a/test/navigation/navigation_service_test.dart b/test/navigation/navigation_service_test.dart new file mode 100644 index 0000000..bf0db72 --- /dev/null +++ b/test/navigation/navigation_service_test.dart @@ -0,0 +1,145 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../helpers/test_app.dart'; + +void main() { + group('NavigationService -', () { + late NavigationService service; + + setUp(() { + service = NavigationService(); + }); + + Future pumpApp(WidgetTester tester) async { + await tester.pumpWidget(buildTestApp()); + await tester.pumpAndSettle(); + } + + testWidgets('starts on the initial route', (tester) async { + await pumpApp(tester); + expect(find.byKey(TestScreen.keyFor('home')), findsOneWidget); + }); + + testWidgets('navigateTo pushes the named route', (tester) async { + await pumpApp(tester); + + unawaited(service.navigateTo(TestRoutes.second)); + await tester.pumpAndSettle(); + + expect(find.byKey(TestScreen.keyFor('second')), findsOneWidget); + expect(find.byKey(TestScreen.keyFor('home')), findsNothing); + }); + + testWidgets('currentRoute and previousRoute reflect the stack', + (tester) async { + await pumpApp(tester); + expect(service.currentRoute, TestRoutes.home); + + unawaited(service.navigateTo(TestRoutes.second)); + await tester.pumpAndSettle(); + + expect(service.currentRoute, TestRoutes.second); + expect(service.previousRoute, TestRoutes.home); + }); + + testWidgets('currentArguments returns the arguments passed in', + (tester) async { + await pumpApp(tester); + + unawaited(service.navigateTo(TestRoutes.second, arguments: 'payload')); + await tester.pumpAndSettle(); + + expect(service.currentArguments, 'payload'); + }); + + testWidgets('back pops the current route', (tester) async { + await pumpApp(tester); + unawaited(service.navigateTo(TestRoutes.second)); + await tester.pumpAndSettle(); + + service.back(); + await tester.pumpAndSettle(); + + expect(find.byKey(TestScreen.keyFor('home')), findsOneWidget); + }); + + testWidgets('preventDuplicates blocks navigating to the current route', + (tester) async { + await pumpApp(tester); + unawaited(service.navigateTo(TestRoutes.second)); + await tester.pumpAndSettle(); + + final result = service.navigateTo(TestRoutes.second); + await tester.pumpAndSettle(); + + expect(result, isNull); + }); + + testWidgets('replaceWith swaps the current route', (tester) async { + await pumpApp(tester); + unawaited(service.navigateTo(TestRoutes.second)); + await tester.pumpAndSettle(); + + unawaited(service.replaceWith(TestRoutes.third)); + await tester.pumpAndSettle(); + + expect(find.byKey(TestScreen.keyFor('third')), findsOneWidget); + expect(find.byKey(TestScreen.keyFor('second')), findsNothing); + + // Popping should return to home, proving second was replaced not pushed. + service.back(); + await tester.pumpAndSettle(); + expect(find.byKey(TestScreen.keyFor('home')), findsOneWidget); + }); + + testWidgets('clearStackAndShow clears the backstack', (tester) async { + await pumpApp(tester); + unawaited(service.navigateTo(TestRoutes.second)); + await tester.pumpAndSettle(); + unawaited(service.navigateTo(TestRoutes.third)); + await tester.pumpAndSettle(); + + unawaited(service.clearStackAndShow(TestRoutes.home)); + await tester.pumpAndSettle(); + + expect(find.byKey(TestScreen.keyFor('home')), findsOneWidget); + // Nothing left to pop. + expect( + StackedService.navigatorKey?.currentState?.canPop(), + isFalse, + ); + }); + + testWidgets('navigateToView pushes a widget directly', (tester) async { + await pumpApp(tester); + + unawaited(service.navigateToView(const TestScreen('inline'))); + await tester.pumpAndSettle(); + + expect(find.byKey(TestScreen.keyFor('inline')), findsOneWidget); + }); + + testWidgets('navigateTo folds parameters into the route name', + (tester) async { + String? capturedName; + await tester.pumpWidget(buildTestApp( + onGenerateRoute: (settings) { + capturedName = settings.name; + return testOnGenerateRoute(settings); + }, + )); + await tester.pumpAndSettle(); + + unawaited(service.navigateTo( + TestRoutes.second, + parameters: {'id': '42'}, + )); + await tester.pumpAndSettle(); + + expect(capturedName, contains('id=42')); + }); + }); +} diff --git a/test/navigation/route_observer_test.dart b/test/navigation/route_observer_test.dart new file mode 100644 index 0000000..d83a672 --- /dev/null +++ b/test/navigation/route_observer_test.dart @@ -0,0 +1,102 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../helpers/test_app.dart'; + +/// These tests exercise the [StackObserver] indirectly through the public +/// routing state it feeds (currentRoute / previousRoute / isDialogOpen / +/// isBottomSheetOpen). They are written against the public surface so they +/// survive the `get` removal unchanged. +void main() { + group('StackObserver (routing state) -', () { + late NavigationService navigation; + late DialogService dialog; + late BottomSheetService bottomSheet; + + setUp(() { + navigation = NavigationService(); + dialog = DialogService(); + bottomSheet = BottomSheetService(); + }); + + Future pumpApp(WidgetTester tester) async { + await tester.pumpWidget(buildTestApp()); + await tester.pumpAndSettle(); + } + + testWidgets('tracks current and previous across multiple pushes', + (tester) async { + await pumpApp(tester); + + unawaited(navigation.navigateTo(TestRoutes.second)); + await tester.pumpAndSettle(); + unawaited(navigation.navigateTo(TestRoutes.third)); + await tester.pumpAndSettle(); + + expect(navigation.currentRoute, TestRoutes.third); + expect(navigation.previousRoute, TestRoutes.second); + }); + + testWidgets('restores current after popping', (tester) async { + await pumpApp(tester); + + unawaited(navigation.navigateTo(TestRoutes.second)); + await tester.pumpAndSettle(); + unawaited(navigation.navigateTo(TestRoutes.third)); + await tester.pumpAndSettle(); + + navigation.back(); + await tester.pumpAndSettle(); + + expect(navigation.currentRoute, TestRoutes.second); + }); + + testWidgets('opening a dialog does not change the current page route', + (tester) async { + await pumpApp(tester); + unawaited(navigation.navigateTo(TestRoutes.second)); + await tester.pumpAndSettle(); + + final future = dialog.showDialog( + title: 'Title', + buttonTitle: 'Confirm', + dialogPlatform: DialogPlatform.Material, + ); + await tester.pumpAndSettle(); + + expect(navigation.currentRoute, TestRoutes.second); + expect(dialog.isDialogOpen, isTrue); + + await tester.tap(find.byKey(const Key('dialog_touchable_confirm'))); + await tester.pumpAndSettle(); + await future; + + expect(dialog.isDialogOpen, isFalse); + }); + + testWidgets('opening a bottom sheet does not change the current page route', + (tester) async { + await pumpApp(tester); + unawaited(navigation.navigateTo(TestRoutes.second)); + await tester.pumpAndSettle(); + + final future = bottomSheet.showBottomSheet( + title: 'Sheet', + confirmButtonTitle: 'Confirm', + ); + await tester.pumpAndSettle(); + + expect(navigation.currentRoute, TestRoutes.second); + expect(bottomSheet.isBottomSheetOpen, isTrue); + + await tester.tap(find.text('Confirm')); + await tester.pumpAndSettle(); + await future; + + expect(bottomSheet.isBottomSheetOpen, isFalse); + }); + }); +} diff --git a/test/navigation/stacked_service_test.dart b/test/navigation/stacked_service_test.dart new file mode 100644 index 0000000..4d0c730 --- /dev/null +++ b/test/navigation/stacked_service_test.dart @@ -0,0 +1,29 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stacked_services/stacked_services.dart'; + +void main() { + group('StackedService -', () { + test('navigatorKey is a non-null GlobalKey', () { + expect(StackedService.navigatorKey, isA>()); + }); + + test('navigatorKey is stable across calls', () { + expect( + identical(StackedService.navigatorKey, StackedService.navigatorKey), + isTrue, + ); + }); + + test('nestedNavigationKey returns a non-null key', () { + expect( + StackedService.nestedNavigationKey(1), + isA>(), + ); + }); + + test('routeObserver returns a NavigatorObserver', () { + expect(StackedService.routeObserver, isA()); + }); + }); +} diff --git a/test/navigation/transition_test.dart b/test/navigation/transition_test.dart new file mode 100644 index 0000000..78b3213 --- /dev/null +++ b/test/navigation/transition_test.dart @@ -0,0 +1,57 @@ +import 'dart:async'; + +import 'package:flutter_test/flutter_test.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../helpers/test_app.dart'; + +/// Ensures every [Transition] value can drive a navigation that renders the +/// target view. After the `get` removal this guards the native +/// `buildTransitionRoute` implementation for all enum cases. +void main() { + group('Transitions -', () { + late NavigationService navigation; + + setUp(() { + navigation = NavigationService(); + }); + + Future pumpApp(WidgetTester tester) async { + await tester.pumpWidget(buildTestApp()); + await tester.pumpAndSettle(); + } + + for (final transition in Transition.values) { + testWidgets('navigateWithTransition (${transition.name}) shows the view', + (tester) async { + await pumpApp(tester); + + unawaited(navigation.navigateWithTransition( + const TestScreen('inline'), + transitionStyle: transition, + duration: const Duration(milliseconds: 100), + )); + await tester.pumpAndSettle(); + + expect(find.byKey(TestScreen.keyFor('inline')), findsOneWidget); + + navigation.back(); + await tester.pumpAndSettle(); + expect(find.byKey(TestScreen.keyFor('home')), findsOneWidget); + }); + } + + testWidgets('navigateToView with transitionStyle shows the view', + (tester) async { + await pumpApp(tester); + + unawaited(navigation.navigateToView( + const TestScreen('inline'), + transitionStyle: Transition.fade, + )); + await tester.pumpAndSettle(); + + expect(find.byKey(TestScreen.keyFor('inline')), findsOneWidget); + }); + }); +} diff --git a/test/snackbar/snackbar_service_test.dart b/test/snackbar/snackbar_service_test.dart new file mode 100644 index 0000000..d24eb5a --- /dev/null +++ b/test/snackbar/snackbar_service_test.dart @@ -0,0 +1,93 @@ +import 'dart:async'; + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:stacked_services/stacked_services.dart'; + +import '../helpers/test_app.dart'; + +void main() { + group('SnackbarService -', () { + late SnackbarService service; + + setUp(() { + service = SnackbarService(); + }); + + Future pumpApp(WidgetTester tester) async { + await tester.pumpWidget(buildTestApp()); + await tester.pumpAndSettle(); + } + + // Fully dismisses any open snackbar and lets its exit animation finish so + // the overlay disposes its tickers before the test tree is torn down. + Future dismiss(WidgetTester tester, SnackbarService service) async { + unawaited(service.closeSnackbar()); + await tester.pumpAndSettle(const Duration(seconds: 1)); + } + + testWidgets('showSnackbar displays the message', (tester) async { + await pumpApp(tester); + + service.showSnackbar( + title: 'Hello', + message: 'World', + duration: const Duration(seconds: 30), + ); + await tester.pumpAndSettle(); + + expect(find.text('World'), findsOneWidget); + expect(service.isSnackbarOpen, isTrue); + + await dismiss(tester, service); + }); + + testWidgets('closeSnackbar dismisses the snackbar', (tester) async { + await pumpApp(tester); + + service.showSnackbar( + message: 'Dismiss me', + duration: const Duration(seconds: 30), + ); + await tester.pumpAndSettle(); + expect(service.isSnackbarOpen, isTrue); + + await dismiss(tester, service); + + expect(service.isSnackbarOpen, isFalse); + }); + + testWidgets('showCustomSnackBar renders the custom snackbar view', + (tester) async { + service.registerCustomSnackbarConfig( + variant: 'basic', + config: SnackbarConfig(instantInit: true), + ); + + await pumpApp(tester); + + service.showCustomSnackBar( + message: 'Custom message', + variant: 'basic', + duration: const Duration(seconds: 30), + ); + await tester.pumpAndSettle(); + + expect(find.byKey(const Key('snackbar_view')), findsOneWidget); + expect(find.text('Custom message'), findsOneWidget); + + await dismiss(tester, service); + }); + + testWidgets( + 'showCustomSnackBar throws when no config registered for variant', + (tester) async { + await pumpApp(tester); + + await expectLater( + service.showCustomSnackBar(message: 'No config', variant: 'missing'), + throwsA(isA()), + ); + }); + }); +} From a469ea68a10c015454026d6fc6b9f9cfd515ec5d Mon Sep 17 00:00:00 2001 From: Fernando Ferrara Date: Mon, 22 Jun 2026 10:11:24 -0300 Subject: [PATCH 2/4] test(example): add migration smoke suite for get-free services Adds an integration_test suite that drives the example app end-to-end to verify the get-removal refactor's behaviour through the public API: - DialogService -> showGeneralDialog - SnackbarService -> vendored GetSnackBar on the navigator overlay - BottomSheetService -> showModalBottomSheet - NavigationService -> native Navigator + PageRouteBuilder transitions Each test dismisses its transient UI and settles before ending so the shared StackedService overlay/navigator tickers are disposed cleanly. Also adds the integration_test dev dependency + web driver entrypoint, and replaces deprecated MaterialStateProperty with WidgetStateProperty in setup_bottom_sheet_ui.dart so the example analyzes clean. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01QX2hw8ST4QPV8DpockczFm --- .../migration_smoke_test.dart | 122 ++++++++++++++++++ example/lib/ui/setup_bottom_sheet_ui.dart | 4 +- example/pubspec.yaml | 2 + example/test_driver/integration_test.dart | 3 + 4 files changed, 129 insertions(+), 2 deletions(-) create mode 100644 example/integration_test/migration_smoke_test.dart create mode 100644 example/test_driver/integration_test.dart diff --git a/example/integration_test/migration_smoke_test.dart b/example/integration_test/migration_smoke_test.dart new file mode 100644 index 0000000..a555a6e --- /dev/null +++ b/example/integration_test/migration_smoke_test.dart @@ -0,0 +1,122 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; +import 'package:stacked_services/stacked_services.dart'; +import 'package:stacked_services_example/app/app.locator.dart'; +import 'package:stacked_services_example/app/app.router.dart'; +import 'package:stacked_services_example/ui/setup_bottom_sheet_ui.dart'; +import 'package:stacked_services_example/ui/setup_dialog_ui.dart'; +import 'package:stacked_services_example/ui/setup_snackbar_ui.dart'; + +/// End-to-end smoke test of the get-free refactor, driving the real example +/// app. Exercises every migrated service: +/// - DialogService -> showGeneralDialog +/// - SnackbarService -> vendored GetSnackBar on the navigator overlay +/// - BottomSheetService -> showModalBottomSheet +/// - NavigationService -> native Navigator + PageRouteBuilder transitions +/// +/// The snackbar/sheet/transition each run an AnimationController on the shared +/// [StackedService] overlay/navigator, so every test fully dismisses its +/// transient UI and settles before ending — otherwise an active Ticker would +/// outlive the widget tree and trip the framework's leak assertion. +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + Widget buildApp() => MaterialApp( + title: 'Stacked Services Demo', + navigatorObservers: [StackedService.routeObserver], + onGenerateRoute: StackedRouter().onGenerateRoute, + navigatorKey: StackedService.navigatorKey, + ); + + setUp(() async { + // Reset between tests so re-running setupLocator() doesn't throw on the + // already-registered get_it singletons. + await locator.reset(); + setupLocator(); + setupDialogUi(); + setupSnackbarUi(); + setupBottomSheetUi(); + }); + + testWidgets('DialogService.showDialog renders via showGeneralDialog', + (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + + // Dialogs tab is the default landing view. + await tester.tap(find.text('Show Material Dialog').first); + await tester.pumpAndSettle(); + + expect(find.text('Test Dialog Title'), findsOneWidget); + expect(find.text('Test Dialog Description'), findsOneWidget); + + // Dismiss the dialog so the route's transition ticker is disposed. + tester.state(find.byType(Navigator).first).pop(); + await tester.pumpAndSettle(); + }); + + testWidgets('SnackbarService.showSnackbar renders the vendored snackbar', + (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Snackbar')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Show Snackbar')); + await tester.pump(); // schedule the entry + await tester.pump(const Duration(seconds: 1)); // finish the entry animation + + expect(find.text('This is a snack bar'), findsOneWidget); + + // Let the 3s auto-dismiss timer fire, then settle the exit animation so the + // overlay entry (and its ticker) is fully removed before teardown. + await tester.pump(const Duration(seconds: 4)); + await tester.pumpAndSettle(); + expect(find.text('This is a snack bar'), findsNothing); + }); + + testWidgets('BottomSheetService.showBottomSheet renders via modal sheet', + (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + + await tester.tap(find.text('BottomSheet')); + await tester.pumpAndSettle(); + + await tester.tap(find.text('Show Basic Bottom Sheet Alert')); + await tester.pumpAndSettle(); + + expect(find.text('This is my Sheets Title'), findsOneWidget); + + // Dismiss the modal sheet and settle its exit animation. + tester.state(find.byType(Navigator).first).pop(); + await tester.pumpAndSettle(); + expect(find.text('This is my Sheets Title'), findsNothing); + }); + + testWidgets('NavigationService transition pushes via native Navigator', + (tester) async { + await tester.pumpWidget(buildApp()); + await tester.pumpAndSettle(); + + // FAB -> FirstScreen (named route). + await tester.tap(find.byType(FloatingActionButton)); + await tester.pumpAndSettle(); + expect(find.text('First Screen'), findsOneWidget); + + // Fade transition -> SecondScreen (widget push + PageRouteBuilder). + await tester.tap(find.text('Use Fade Transition')); + await tester.pumpAndSettle(); + expect(find.text('Second Screen'), findsOneWidget); + + // Unwind the stack so transition tickers are disposed before teardown. + final navigator = tester.state(find.byType(Navigator).first); + navigator.pop(); + await tester.pumpAndSettle(); + navigator.pop(); + await tester.pumpAndSettle(); + expect(find.text('Home Screen'), findsOneWidget); + }); +} diff --git a/example/lib/ui/setup_bottom_sheet_ui.dart b/example/lib/ui/setup_bottom_sheet_ui.dart index 49cd5d5..8215976 100644 --- a/example/lib/ui/setup_bottom_sheet_ui.dart +++ b/example/lib/ui/setup_bottom_sheet_ui.dart @@ -79,7 +79,7 @@ class _FloatingBoxBottomSheet extends StatelessWidget { style: TextStyle(color: Colors.white), ), style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( + backgroundColor: WidgetStateProperty.all( Theme.of(context).primaryColor, ), ), @@ -167,7 +167,7 @@ class GenericBottomSheet extends StatelessWidget { style: TextStyle(color: Colors.white), ), style: ButtonStyle( - backgroundColor: MaterialStateProperty.all( + backgroundColor: WidgetStateProperty.all( Theme.of(context).primaryColor, ), ), diff --git a/example/pubspec.yaml b/example/pubspec.yaml index 6795890..6a256df 100644 --- a/example/pubspec.yaml +++ b/example/pubspec.yaml @@ -30,6 +30,8 @@ dependencies: dev_dependencies: flutter_test: sdk: flutter + integration_test: + sdk: flutter stacked_generator: build_runner: ^2.1.11 diff --git a/example/test_driver/integration_test.dart b/example/test_driver/integration_test.dart new file mode 100644 index 0000000..b38629c --- /dev/null +++ b/example/test_driver/integration_test.dart @@ -0,0 +1,3 @@ +import 'package:integration_test/integration_test_driver.dart'; + +Future main() => integrationDriver(); From d4eb4477c3649fbdabb6f9900ff4afed1e40aa08 Mon Sep 17 00:00:00 2001 From: Fernando Ferrara Date: Mon, 22 Jun 2026 10:34:58 -0300 Subject: [PATCH 3/4] chore(example): modernize android/ios so integration tests run on device The example's Android scaffolding (Gradle 5.6.2, AGP 7, jcenter, legacy plugin loader) couldn't build under JDK 21, and iOS lacked the RunnerTests XCTest target that on-device integration tests require. Regenerated both platforms via `flutter create` and finished the migration by hand: Android: - Kotlin-DSL build files (settings/app/root build.gradle.kts), AGP 9.0.1, Gradle wrapper 9.1.0, mavenCentral. - Removed stale Groovy build files and the old com.example.example MainActivity package. - gradle.properties aligned to the current template (dropped obsolete enableR8/enableJetifier). - Manifests: dropped the AGP-incompatible package= attribute and added android:exported on the launcher activity. iOS: - Regenerated the Runner project with the RunnerTests target wired into project.pbxproj (flutter create won't inject it into an existing project), plus the modern UIScene embedding (SceneDelegate) and Podfile. App/bundle id is now com.example.stacked_services_example. Verified: migration_smoke_test passes on the Android emulator and the iOS simulator (DialogService, SnackbarService, BottomSheetService, NavigationService), and `flutter analyze` is clean. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01QX2hw8ST4QPV8DpockczFm --- example/.metadata | 24 +- example/android/app/build.gradle | 65 ---- example/android/app/build.gradle.kts | 45 +++ .../android/app/src/debug/AndroidManifest.xml | 6 +- .../android/app/src/main/AndroidManifest.xml | 45 ++- .../com/example/example/MainActivity.kt | 12 - .../stacked_services_example/MainActivity.kt | 5 + .../res/drawable-v21/launch_background.xml | 12 + .../app/src/main/res/values-night/styles.xml | 18 + .../app/src/profile/AndroidManifest.xml | 6 +- example/android/build.gradle | 31 -- example/android/build.gradle.kts | 24 ++ example/android/gradle.properties | 8 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- example/android/settings.gradle | 15 - example/android/settings.gradle.kts | 26 ++ example/ios/.gitignore | 2 + example/ios/Flutter/AppFrameworkInfo.plist | 4 +- example/ios/Flutter/Debug.xcconfig | 1 + example/ios/Flutter/Release.xcconfig | 1 + example/ios/Podfile | 43 +++ example/ios/Runner.xcodeproj/project.pbxproj | 312 +++++++++++++++--- .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + .../xcshareddata/xcschemes/Runner.xcscheme | 24 +- .../contents.xcworkspacedata | 3 + .../xcshareddata/IDEWorkspaceChecks.plist | 8 + .../xcshareddata/WorkspaceSettings.xcsettings | 8 + example/ios/Runner/AppDelegate.swift | 11 +- .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin 564 -> 295 bytes .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin 1283 -> 406 bytes .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin 1588 -> 450 bytes .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin 1025 -> 282 bytes .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin 1716 -> 462 bytes .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin 1920 -> 704 bytes .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin 1283 -> 406 bytes .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin 1895 -> 586 bytes .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin 2665 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin 2665 -> 862 bytes .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin 3831 -> 1674 bytes .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin 1888 -> 762 bytes .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin 3294 -> 1226 bytes .../Icon-App-83.5x83.5@2x.png | Bin 3612 -> 1418 bytes example/ios/Runner/Info.plist | 33 +- example/ios/Runner/Runner-Bridging-Header.h | 2 +- example/ios/Runner/SceneDelegate.swift | 6 + example/ios/RunnerTests/RunnerTests.swift | 12 + 47 files changed, 626 insertions(+), 204 deletions(-) delete mode 100644 example/android/app/build.gradle create mode 100644 example/android/app/build.gradle.kts delete mode 100644 example/android/app/src/main/kotlin/com/example/example/MainActivity.kt create mode 100644 example/android/app/src/main/kotlin/com/example/stacked_services_example/MainActivity.kt create mode 100644 example/android/app/src/main/res/drawable-v21/launch_background.xml create mode 100644 example/android/app/src/main/res/values-night/styles.xml delete mode 100644 example/android/build.gradle create mode 100644 example/android/build.gradle.kts delete mode 100644 example/android/settings.gradle create mode 100644 example/android/settings.gradle.kts create mode 100644 example/ios/Podfile create mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist create mode 100644 example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings create mode 100644 example/ios/Runner/SceneDelegate.swift create mode 100644 example/ios/RunnerTests/RunnerTests.swift diff --git a/example/.metadata b/example/.metadata index 4adf4bf..98236da 100644 --- a/example/.metadata +++ b/example/.metadata @@ -4,7 +4,27 @@ # This file should be version controlled and should not be manually edited. version: - revision: f139b11009aeb8ed2a3a3aa8b0066e482709dde3 - channel: stable + revision: "c9a6c484230f8b5e408ec57be1ef71dee1e77020" + channel: "stable" project_type: app + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + - platform: ios + create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020 + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle deleted file mode 100644 index 922cf73..0000000 --- a/example/android/app/build.gradle +++ /dev/null @@ -1,65 +0,0 @@ -def localProperties = new Properties() -def localPropertiesFile = rootProject.file('local.properties') -if (localPropertiesFile.exists()) { - localPropertiesFile.withReader('UTF-8') { reader -> - localProperties.load(reader) - } -} - -def flutterRoot = localProperties.getProperty('flutter.sdk') -if (flutterRoot == null) { - throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") -} - -def flutterVersionCode = localProperties.getProperty('flutter.versionCode') -if (flutterVersionCode == null) { - flutterVersionCode = '1' -} - -def flutterVersionName = localProperties.getProperty('flutter.versionName') -if (flutterVersionName == null) { - flutterVersionName = '1.0' -} - -apply plugin: 'com.android.application' -apply plugin: 'kotlin-android' -apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" - -android { - compileSdkVersion 34 - - sourceSets { - main.java.srcDirs += 'src/main/kotlin' - } - - lintOptions { - disable 'InvalidPackage' - } - - defaultConfig { - applicationId "com.example.example" - minSdkVersion 16 - targetSdkVersion 28 - versionCode flutterVersionCode.toInteger() - versionName flutterVersionName - testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" - } - - buildTypes { - release { - // Signing with the debug keys for now, so `flutter run --release` works. - signingConfig signingConfigs.debug - } - } -} - -flutter { - source '../..' -} - -dependencies { - implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" - testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test:runner:1.1.1' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' -} diff --git a/example/android/app/build.gradle.kts b/example/android/app/build.gradle.kts new file mode 100644 index 0000000..24e36ee --- /dev/null +++ b/example/android/app/build.gradle.kts @@ -0,0 +1,45 @@ +plugins { + id("com.android.application") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.stacked_services_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.stacked_services_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +kotlin { + compilerOptions { + jvmTarget = org.jetbrains.kotlin.gradle.dsl.JvmTarget.JVM_17 + } +} + +flutter { + source = "../.." +} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml index c208884..399f698 100644 --- a/example/android/app/src/debug/AndroidManifest.xml +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -1,6 +1,6 @@ - - diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml index d4e5497..74a78b9 100644 --- a/example/android/app/src/main/AndroidManifest.xml +++ b/example/android/app/src/main/AndroidManifest.xml @@ -1,11 +1,25 @@ - - - - + + + + + @@ -13,6 +27,19 @@ - + + + + + + + + diff --git a/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt deleted file mode 100644 index 1656503..0000000 --- a/example/android/app/src/main/kotlin/com/example/example/MainActivity.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.example.example - -import androidx.annotation.NonNull; -import io.flutter.embedding.android.FlutterActivity -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.plugins.GeneratedPluginRegistrant - -class MainActivity: FlutterActivity() { - override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) { - GeneratedPluginRegistrant.registerWith(flutterEngine); - } -} diff --git a/example/android/app/src/main/kotlin/com/example/stacked_services_example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/stacked_services_example/MainActivity.kt new file mode 100644 index 0000000..e182ee6 --- /dev/null +++ b/example/android/app/src/main/kotlin/com/example/stacked_services_example/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.stacked_services_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml index c208884..399f698 100644 --- a/example/android/app/src/profile/AndroidManifest.xml +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -1,6 +1,6 @@ - - diff --git a/example/android/build.gradle b/example/android/build.gradle deleted file mode 100644 index c81ce64..0000000 --- a/example/android/build.gradle +++ /dev/null @@ -1,31 +0,0 @@ -buildscript { - ext.kotlin_version = '1.7.0' - repositories { - google() - jcenter() - } - - dependencies { - classpath 'com.android.tools.build:gradle:7.0.0' - classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" - } -} - -allprojects { - repositories { - google() - jcenter() - } -} - -rootProject.buildDir = '../build' -subprojects { - project.buildDir = "${rootProject.buildDir}/${project.name}" -} -subprojects { - project.evaluationDependsOn(':app') -} - -task clean(type: Delete) { - delete rootProject.buildDir -} diff --git a/example/android/build.gradle.kts b/example/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/example/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties index 38c8d45..e96108c 100644 --- a/example/android/gradle.properties +++ b/example/android/gradle.properties @@ -1,4 +1,6 @@ -org.gradle.jvmargs=-Xmx1536M -android.enableR8=true +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError android.useAndroidX=true -android.enableJetifier=true +# This newDsl flag was added by the Flutter template +android.newDsl=false +# This builtInKotlin flag was added by the Flutter template +android.builtInKotlin=false diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties index 296b146..a97e89c 100644 --- a/example/android/gradle/wrapper/gradle-wrapper.properties +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -3,4 +3,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.1.0-all.zip diff --git a/example/android/settings.gradle b/example/android/settings.gradle deleted file mode 100644 index 5a2f14f..0000000 --- a/example/android/settings.gradle +++ /dev/null @@ -1,15 +0,0 @@ -include ':app' - -def flutterProjectRoot = rootProject.projectDir.parentFile.toPath() - -def plugins = new Properties() -def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins') -if (pluginsFile.exists()) { - pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) } -} - -plugins.each { name, path -> - def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile() - include ":$name" - project(":$name").projectDir = pluginDirectory -} diff --git a/example/android/settings.gradle.kts b/example/android/settings.gradle.kts new file mode 100644 index 0000000..c21f0c5 --- /dev/null +++ b/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "9.0.1" apply false + id("org.jetbrains.kotlin.android") version "2.3.20" apply false +} + +include(":app") diff --git a/example/ios/.gitignore b/example/ios/.gitignore index e96ef60..7a7f987 100644 --- a/example/ios/.gitignore +++ b/example/ios/.gitignore @@ -1,3 +1,4 @@ +**/dgph *.mode1v3 *.mode2v3 *.moved-aside @@ -18,6 +19,7 @@ Flutter/App.framework Flutter/Flutter.framework Flutter/Flutter.podspec Flutter/Generated.xcconfig +Flutter/ephemeral/ Flutter/app.flx Flutter/app.zip Flutter/flutter_assets/ diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/example/ios/Flutter/AppFrameworkInfo.plist index f2872cf..391a902 100644 --- a/example/ios/Flutter/AppFrameworkInfo.plist +++ b/example/ios/Flutter/AppFrameworkInfo.plist @@ -3,7 +3,7 @@ CFBundleDevelopmentRegion - $(DEVELOPMENT_LANGUAGE) + en CFBundleExecutable App CFBundleIdentifier @@ -20,7 +20,5 @@ ???? CFBundleVersion 1.0 - MinimumOSVersion - 9.0 diff --git a/example/ios/Flutter/Debug.xcconfig b/example/ios/Flutter/Debug.xcconfig index 592ceee..ec97fc6 100644 --- a/example/ios/Flutter/Debug.xcconfig +++ b/example/ios/Flutter/Debug.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Flutter/Release.xcconfig b/example/ios/Flutter/Release.xcconfig index 592ceee..c4855bf 100644 --- a/example/ios/Flutter/Release.xcconfig +++ b/example/ios/Flutter/Release.xcconfig @@ -1 +1,2 @@ +#include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" #include "Generated.xcconfig" diff --git a/example/ios/Podfile b/example/ios/Podfile new file mode 100644 index 0000000..620e46e --- /dev/null +++ b/example/ios/Podfile @@ -0,0 +1,43 @@ +# Uncomment this line to define a global platform for your project +# platform :ios, '13.0' + +# CocoaPods analytics sends network stats synchronously affecting flutter build latency. +ENV['COCOAPODS_DISABLE_STATS'] = 'true' + +project 'Runner', { + 'Debug' => :debug, + 'Profile' => :release, + 'Release' => :release, +} + +def flutter_root + generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) + unless File.exist?(generated_xcode_build_settings_path) + raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" + end + + File.foreach(generated_xcode_build_settings_path) do |line| + matches = line.match(/FLUTTER_ROOT\=(.*)/) + return matches[1].strip if matches + end + raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" +end + +require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) + +flutter_ios_podfile_setup + +target 'Runner' do + use_frameworks! + + flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) + target 'RunnerTests' do + inherit! :search_paths + end +end + +post_install do |installer| + installer.pods_project.targets.each do |target| + flutter_additional_ios_build_settings(target) + end +end diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/example/ios/Runner.xcodeproj/project.pbxproj index 695aeab..8bd500a 100644 --- a/example/ios/Runner.xcodeproj/project.pbxproj +++ b/example/ios/Runner.xcodeproj/project.pbxproj @@ -3,18 +3,32 @@ archiveVersion = 1; classes = { }; - objectVersion = 50; + objectVersion = 54; objects = { /* Begin PBXBuildFile section */ 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */ = {isa = PBXBuildFile; fileRef = 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */; }; + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 331C807B294A618700263BE5 /* RunnerTests.swift */; }; 3B3967161E833CAA004F5970 /* AppFrameworkInfo.plist in Resources */ = {isa = PBXBuildFile; fileRef = 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */; }; + 615FC61079D9D80DF70856CE /* Pods_RunnerTests.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4D9B237DB827BF2999B70691 /* Pods_RunnerTests.framework */; }; + 6751612432783A7BC869B3A4 /* Pods_Runner.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 038EC5779A60919FDDBC403A /* Pods_Runner.framework */; }; 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 74858FAE1ED2DC5600515810 /* AppDelegate.swift */; }; + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */; }; 97C146FC1CF9000F007C117D /* Main.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FA1CF9000F007C117D /* Main.storyboard */; }; 97C146FE1CF9000F007C117D /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FD1CF9000F007C117D /* Assets.xcassets */; }; 97C147011CF9000F007C117D /* LaunchScreen.storyboard in Resources */ = {isa = PBXBuildFile; fileRef = 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */; }; /* End PBXBuildFile section */ +/* Begin PBXContainerItemProxy section */ + 331C8085294A63A400263BE5 /* PBXContainerItemProxy */ = { + isa = PBXContainerItemProxy; + containerPortal = 97C146E61CF9000F007C117D /* Project object */; + proxyType = 1; + remoteGlobalIDString = 97C146ED1CF9000F007C117D; + remoteInfo = Runner; + }; +/* End PBXContainerItemProxy section */ + /* Begin PBXCopyFilesBuildPhase section */ 9705A1C41CF9048500538489 /* Embed Frameworks */ = { isa = PBXCopyFilesBuildPhase; @@ -29,11 +43,20 @@ /* End PBXCopyFilesBuildPhase section */ /* Begin PBXFileReference section */ + 038EC5779A60919FDDBC403A /* Pods_Runner.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_Runner.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 1458129257EAA3DA8E74DB50 /* Pods-Runner.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.release.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"; sourceTree = ""; }; 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = GeneratedPluginRegistrant.h; sourceTree = ""; }; 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = GeneratedPluginRegistrant.m; sourceTree = ""; }; + 1EBC50D08B3CB16DE08C0A40 /* Pods-RunnerTests.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.profile.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.profile.xcconfig"; sourceTree = ""; }; + 24BDC3C8B4EB2B5A3A90A86A /* Pods-Runner.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.debug.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"; sourceTree = ""; }; + 331C807B294A618700263BE5 /* RunnerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RunnerTests.swift; sourceTree = ""; }; + 331C8081294A63A400263BE5 /* RunnerTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = RunnerTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 3B3967151E833CAA004F5970 /* AppFrameworkInfo.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = AppFrameworkInfo.plist; path = Flutter/AppFrameworkInfo.plist; sourceTree = ""; }; + 4D9B237DB827BF2999B70691 /* Pods_RunnerTests.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = Pods_RunnerTests.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 501113851EC095753BECB59A /* Pods-Runner.profile.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Runner.profile.xcconfig"; path = "Target Support Files/Pods-Runner/Pods-Runner.profile.xcconfig"; sourceTree = ""; }; 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = "Runner-Bridging-Header.h"; sourceTree = ""; }; 74858FAE1ED2DC5600515810 /* AppDelegate.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = AppDelegate.swift; sourceTree = ""; }; + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SceneDelegate.swift; sourceTree = ""; }; 7AFA3C8E1D35360C0083082E /* Release.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; name = Release.xcconfig; path = Flutter/Release.xcconfig; sourceTree = ""; }; 9740EEB21CF90195004384FC /* Debug.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Debug.xcconfig; path = Flutter/Debug.xcconfig; sourceTree = ""; }; 9740EEB31CF90195004384FC /* Generated.xcconfig */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.xcconfig; name = Generated.xcconfig; path = Flutter/Generated.xcconfig; sourceTree = ""; }; @@ -42,19 +65,61 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = ""; }; 97C147001CF9000F007C117D /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.storyboard; name = Base; path = Base.lproj/LaunchScreen.storyboard; sourceTree = ""; }; 97C147021CF9000F007C117D /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; + E6BB5A1AB6DF2CE6BC92357B /* Pods-RunnerTests.release.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.release.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.release.xcconfig"; sourceTree = ""; }; + EA439FB3A533E85200C306D0 /* Pods-RunnerTests.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-RunnerTests.debug.xcconfig"; path = "Target Support Files/Pods-RunnerTests/Pods-RunnerTests.debug.xcconfig"; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ + 57B92D0B7BF8374483A54795 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + 615FC61079D9D80DF70856CE /* Pods_RunnerTests.framework in Frameworks */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EB1CF9000F007C117D /* Frameworks */ = { isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( + 6751612432783A7BC869B3A4 /* Pods_Runner.framework in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + 0D0F5205865D3D2A2F479A23 /* Pods */ = { + isa = PBXGroup; + children = ( + 24BDC3C8B4EB2B5A3A90A86A /* Pods-Runner.debug.xcconfig */, + 1458129257EAA3DA8E74DB50 /* Pods-Runner.release.xcconfig */, + 501113851EC095753BECB59A /* Pods-Runner.profile.xcconfig */, + EA439FB3A533E85200C306D0 /* Pods-RunnerTests.debug.xcconfig */, + E6BB5A1AB6DF2CE6BC92357B /* Pods-RunnerTests.release.xcconfig */, + 1EBC50D08B3CB16DE08C0A40 /* Pods-RunnerTests.profile.xcconfig */, + ); + name = Pods; + path = Pods; + sourceTree = ""; + }; + 331C8082294A63A400263BE5 /* RunnerTests */ = { + isa = PBXGroup; + children = ( + 331C807B294A618700263BE5 /* RunnerTests.swift */, + ); + path = RunnerTests; + sourceTree = ""; + }; + 68F5683A412866B0E8BBED70 /* Frameworks */ = { + isa = PBXGroup; + children = ( + 038EC5779A60919FDDBC403A /* Pods_Runner.framework */, + 4D9B237DB827BF2999B70691 /* Pods_RunnerTests.framework */, + ); + name = Frameworks; + sourceTree = ""; + }; 9740EEB11CF90186004384FC /* Flutter */ = { isa = PBXGroup; children = ( @@ -72,6 +137,9 @@ 9740EEB11CF90186004384FC /* Flutter */, 97C146F01CF9000F007C117D /* Runner */, 97C146EF1CF9000F007C117D /* Products */, + 331C8082294A63A400263BE5 /* RunnerTests */, + 0D0F5205865D3D2A2F479A23 /* Pods */, + 68F5683A412866B0E8BBED70 /* Frameworks */, ); sourceTree = ""; }; @@ -79,6 +147,7 @@ isa = PBXGroup; children = ( 97C146EE1CF9000F007C117D /* Runner.app */, + 331C8081294A63A400263BE5 /* RunnerTests.xctest */, ); name = Products; sourceTree = ""; @@ -90,35 +159,49 @@ 97C146FD1CF9000F007C117D /* Assets.xcassets */, 97C146FF1CF9000F007C117D /* LaunchScreen.storyboard */, 97C147021CF9000F007C117D /* Info.plist */, - 97C146F11CF9000F007C117D /* Supporting Files */, 1498D2321E8E86230040F4C2 /* GeneratedPluginRegistrant.h */, 1498D2331E8E89220040F4C2 /* GeneratedPluginRegistrant.m */, 74858FAE1ED2DC5600515810 /* AppDelegate.swift */, + 7884E8672EC3CC0400C636F2 /* SceneDelegate.swift */, 74858FAD1ED2DC5600515810 /* Runner-Bridging-Header.h */, ); path = Runner; sourceTree = ""; }; - 97C146F11CF9000F007C117D /* Supporting Files */ = { - isa = PBXGroup; - children = ( - ); - name = "Supporting Files"; - sourceTree = ""; - }; /* End PBXGroup section */ /* Begin PBXNativeTarget section */ + 331C8080294A63A400263BE5 /* RunnerTests */ = { + isa = PBXNativeTarget; + buildConfigurationList = 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */; + buildPhases = ( + 641FB02BDB807261EBD0E55C /* [CP] Check Pods Manifest.lock */, + 331C807D294A63A400263BE5 /* Sources */, + 331C807F294A63A400263BE5 /* Resources */, + 57B92D0B7BF8374483A54795 /* Frameworks */, + ); + buildRules = ( + ); + dependencies = ( + 331C8086294A63A400263BE5 /* PBXTargetDependency */, + ); + name = RunnerTests; + productName = RunnerTests; + productReference = 331C8081294A63A400263BE5 /* RunnerTests.xctest */; + productType = "com.apple.product-type.bundle.unit-test"; + }; 97C146ED1CF9000F007C117D /* Runner */ = { isa = PBXNativeTarget; buildConfigurationList = 97C147051CF9000F007C117D /* Build configuration list for PBXNativeTarget "Runner" */; buildPhases = ( + E43778D590445DD4E939C05C /* [CP] Check Pods Manifest.lock */, 9740EEB61CF901F6004384FC /* Run Script */, 97C146EA1CF9000F007C117D /* Sources */, 97C146EB1CF9000F007C117D /* Frameworks */, 97C146EC1CF9000F007C117D /* Resources */, 9705A1C41CF9048500538489 /* Embed Frameworks */, 3B06AD1E1E4923F5004D2608 /* Thin Binary */, + F3669D5042756E8DF97F7938 /* [CP] Embed Pods Frameworks */, ); buildRules = ( ); @@ -135,9 +218,14 @@ 97C146E61CF9000F007C117D /* Project object */ = { isa = PBXProject; attributes = { - LastUpgradeCheck = 1300; - ORGANIZATIONNAME = "The Chromium Authors"; + BuildIndependentTargetsInParallel = YES; + LastUpgradeCheck = 1510; + ORGANIZATIONNAME = ""; TargetAttributes = { + 331C8080294A63A400263BE5 = { + CreatedOnToolsVersion = 14.0; + TestTargetID = 97C146ED1CF9000F007C117D; + }; 97C146ED1CF9000F007C117D = { CreatedOnToolsVersion = 7.3.1; LastSwiftMigration = 1100; @@ -145,7 +233,7 @@ }; }; buildConfigurationList = 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */; - compatibilityVersion = "Xcode 3.2"; + compatibilityVersion = "Xcode 9.3"; developmentRegion = en; hasScannedForEncodings = 0; knownRegions = ( @@ -158,11 +246,19 @@ projectRoot = ""; targets = ( 97C146ED1CF9000F007C117D /* Runner */, + 331C8080294A63A400263BE5 /* RunnerTests */, ); }; /* End PBXProject section */ /* Begin PBXResourcesBuildPhase section */ + 331C807F294A63A400263BE5 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EC1CF9000F007C117D /* Resources */ = { isa = PBXResourcesBuildPhase; buildActionMask = 2147483647; @@ -179,10 +275,12 @@ /* Begin PBXShellScriptBuildPhase section */ 3B06AD1E1E4923F5004D2608 /* Thin Binary */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); inputPaths = ( + "${TARGET_BUILD_DIR}/${INFOPLIST_PATH}", ); name = "Thin Binary"; outputPaths = ( @@ -191,8 +289,31 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" embed_and_thin"; }; + 641FB02BDB807261EBD0E55C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-RunnerTests-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; 9740EEB61CF901F6004384FC /* Run Script */ = { isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; buildActionMask = 2147483647; files = ( ); @@ -205,20 +326,76 @@ shellPath = /bin/sh; shellScript = "/bin/sh \"$FLUTTER_ROOT/packages/flutter_tools/bin/xcode_backend.sh\" build"; }; + E43778D590445DD4E939C05C /* [CP] Check Pods Manifest.lock */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + "${PODS_PODFILE_DIR_PATH}/Podfile.lock", + "${PODS_ROOT}/Manifest.lock", + ); + name = "[CP] Check Pods Manifest.lock"; + outputFileListPaths = ( + ); + outputPaths = ( + "$(DERIVED_FILE_DIR)/Pods-Runner-checkManifestLockResult.txt", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "diff \"${PODS_PODFILE_DIR_PATH}/Podfile.lock\" \"${PODS_ROOT}/Manifest.lock\" > /dev/null\nif [ $? != 0 ] ; then\n # print error to STDERR\n echo \"error: The sandbox is not in sync with the Podfile.lock. Run 'pod install' or update your CocoaPods installation.\" >&2\n exit 1\nfi\n# This output is used by Xcode 'outputs' to avoid re-running this script phase.\necho \"SUCCESS\" > \"${SCRIPT_OUTPUT_FILE_0}\"\n"; + showEnvVarsInLog = 0; + }; + F3669D5042756E8DF97F7938 /* [CP] Embed Pods Frameworks */ = { + isa = PBXShellScriptBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-input-files.xcfilelist", + ); + name = "[CP] Embed Pods Frameworks"; + outputFileListPaths = ( + "${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks-${CONFIGURATION}-output-files.xcfilelist", + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "\"${PODS_ROOT}/Target Support Files/Pods-Runner/Pods-Runner-frameworks.sh\"\n"; + showEnvVarsInLog = 0; + }; /* End PBXShellScriptBuildPhase section */ /* Begin PBXSourcesBuildPhase section */ + 331C807D294A63A400263BE5 /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + 331C808B294A63AB00263BE5 /* RunnerTests.swift in Sources */, + ); + runOnlyForDeploymentPostprocessing = 0; + }; 97C146EA1CF9000F007C117D /* Sources */ = { isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( 74858FAF1ED2DC5600515810 /* AppDelegate.swift in Sources */, 1498D2341E8E89220040F4C2 /* GeneratedPluginRegistrant.m in Sources */, + 7884E8682EC3CC0700C636F2 /* SceneDelegate.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; /* End PBXSourcesBuildPhase section */ +/* Begin PBXTargetDependency section */ + 331C8086294A63A400263BE5 /* PBXTargetDependency */ = { + isa = PBXTargetDependency; + target = 97C146ED1CF9000F007C117D /* Runner */; + targetProxy = 331C8085294A63A400263BE5 /* PBXContainerItemProxy */; + }; +/* End PBXTargetDependency section */ + /* Begin PBXVariantGroup section */ 97C146FA1CF9000F007C117D /* Main.storyboard */ = { isa = PBXVariantGroup; @@ -243,6 +420,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -272,6 +450,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -280,7 +459,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; @@ -296,18 +475,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 63688GT3UZ; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_BUNDLE_IDENTIFIER = com.example.stackedServicesExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -315,10 +490,61 @@ }; name = Profile; }; + 331C8088294A63A400263BE5 /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = EA439FB3A533E85200C306D0 /* Pods-RunnerTests.debug.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.stackedServicesExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = DEBUG; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Debug; + }; + 331C8089294A63A400263BE5 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = E6BB5A1AB6DF2CE6BC92357B /* Pods-RunnerTests.release.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.stackedServicesExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Release; + }; + 331C808A294A63A400263BE5 /* Profile */ = { + isa = XCBuildConfiguration; + baseConfigurationReference = 1EBC50D08B3CB16DE08C0A40 /* Pods-RunnerTests.profile.xcconfig */; + buildSettings = { + BUNDLE_LOADER = "$(TEST_HOST)"; + CODE_SIGN_STYLE = Automatic; + CURRENT_PROJECT_VERSION = 1; + GENERATE_INFOPLIST_FILE = YES; + MARKETING_VERSION = 1.0; + PRODUCT_BUNDLE_IDENTIFIER = com.example.stackedServicesExample.RunnerTests; + PRODUCT_NAME = "$(TARGET_NAME)"; + SWIFT_VERSION = 5.0; + TEST_HOST = "$(BUILT_PRODUCTS_DIR)/Runner.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/Runner"; + }; + name = Profile; + }; 97C147031CF9000F007C117D /* Debug */ = { isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -348,6 +574,7 @@ DEBUG_INFORMATION_FORMAT = dwarf; ENABLE_STRICT_OBJC_MSGSEND = YES; ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_DYNAMIC_NO_PIC = NO; GCC_NO_COMMON_BLOCKS = YES; @@ -362,7 +589,7 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = YES; ONLY_ACTIVE_ARCH = YES; SDKROOT = iphoneos; @@ -374,6 +601,7 @@ isa = XCBuildConfiguration; buildSettings = { ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; CLANG_ANALYZER_NONNULL = YES; CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; CLANG_CXX_LIBRARY = "libc++"; @@ -403,6 +631,7 @@ DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; ENABLE_NS_ASSERTIONS = NO; ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; GCC_C_LANGUAGE_STANDARD = gnu99; GCC_NO_COMMON_BLOCKS = YES; GCC_WARN_64_TO_32_BIT_CONVERSION = YES; @@ -411,11 +640,12 @@ GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; GCC_WARN_UNUSED_FUNCTION = YES; GCC_WARN_UNUSED_VARIABLE = YES; - IPHONEOS_DEPLOYMENT_TARGET = 9.0; + IPHONEOS_DEPLOYMENT_TARGET = 13.0; MTL_ENABLE_DEBUG_INFO = NO; SDKROOT = iphoneos; SUPPORTED_PLATFORMS = iphoneos; - SWIFT_OPTIMIZATION_LEVEL = "-Owholemodule"; + SWIFT_COMPILATION_MODE = wholemodule; + SWIFT_OPTIMIZATION_LEVEL = "-O"; TARGETED_DEVICE_FAMILY = "1,2"; VALIDATE_PRODUCT = YES; }; @@ -428,18 +658,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 63688GT3UZ; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_BUNDLE_IDENTIFIER = com.example.stackedServicesExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_OPTIMIZATION_LEVEL = "-Onone"; @@ -455,18 +681,14 @@ ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; CLANG_ENABLE_MODULES = YES; CURRENT_PROJECT_VERSION = "$(FLUTTER_BUILD_NUMBER)"; + DEVELOPMENT_TEAM = 63688GT3UZ; ENABLE_BITCODE = NO; - FRAMEWORK_SEARCH_PATHS = ( - "$(inherited)", - "$(PROJECT_DIR)/Flutter", - ); INFOPLIST_FILE = Runner/Info.plist; - LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; - LIBRARY_SEARCH_PATHS = ( + LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", - "$(PROJECT_DIR)/Flutter", + "@executable_path/Frameworks", ); - PRODUCT_BUNDLE_IDENTIFIER = com.example.example; + PRODUCT_BUNDLE_IDENTIFIER = com.example.stackedServicesExample; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_OBJC_BRIDGING_HEADER = "Runner/Runner-Bridging-Header.h"; SWIFT_VERSION = 5.0; @@ -477,6 +699,16 @@ /* End XCBuildConfiguration section */ /* Begin XCConfigurationList section */ + 331C8087294A63A400263BE5 /* Build configuration list for PBXNativeTarget "RunnerTests" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 331C8088294A63A400263BE5 /* Debug */, + 331C8089294A63A400263BE5 /* Release */, + 331C808A294A63A400263BE5 /* Profile */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; 97C146E91CF9000F007C117D /* Build configuration list for PBXProject "Runner" */ = { isa = XCConfigurationList; buildConfigurations = ( diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme index 3db53b6..e3773d4 100644 --- a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme +++ b/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme @@ -1,6 +1,6 @@ - - - - + + + + + + @@ -61,8 +73,6 @@ ReferencedContainer = "container:Runner.xcodeproj"> - - + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist new file mode 100644 index 0000000..18d9810 --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist @@ -0,0 +1,8 @@ + + + + + IDEDidComputeMac32BitWarning + + + diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings new file mode 100644 index 0000000..f9b0d7c --- /dev/null +++ b/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings @@ -0,0 +1,8 @@ + + + + + PreviewsEnabled + + + diff --git a/example/ios/Runner/AppDelegate.swift b/example/ios/Runner/AppDelegate.swift index 70693e4..c30b367 100644 --- a/example/ios/Runner/AppDelegate.swift +++ b/example/ios/Runner/AppDelegate.swift @@ -1,13 +1,16 @@ -import UIKit import Flutter +import UIKit -@UIApplicationMain -@objc class AppDelegate: FlutterAppDelegate { +@main +@objc class AppDelegate: FlutterAppDelegate, FlutterImplicitEngineDelegate { override func application( _ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? ) -> Bool { - GeneratedPluginRegistrant.register(with: self) return super.application(application, didFinishLaunchingWithOptions: launchOptions) } + + func didInitializeImplicitFlutterEngine(_ engineBridge: FlutterImplicitEngineBridge) { + GeneratedPluginRegistrant.register(with: engineBridge.pluginRegistry) + } } diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png index 28c6bf03016f6c994b70f38d1b7346e5831b531f..7353c41ecf9ca08017312dc233d9830079b50717 100644 GIT binary patch delta 279 zcmV+y0qFj;1g8R!8Gi!+006pI?LPnj0Blf9R7L;)|5U~J`u_j-{Qm)0oAmqtj@kOz z^8J|I`-|B6ht~R5kG+%I`zf~eztraM`u^bc{`dO)zUlmg)%x%C`E}6wSI77~z4s`y z^XT{f(eM4n?EUff`e@AgO~UxV*5*r_%Uhbj5N)LaQj!wdIe!-b004GLL_t&-)18pX z4udcZ1u-#g(~z+5JN*AY5?>Gw7hsN~k)CYt4dQDFxbs5*_&e@Hj)wtt(&JE<3Eq*D z;_gQLvqXoKv=I*gWqM9C(Tvu0>=?hTbOp9!6k6AF;>f6|S5%jGEE}TA9h)e`Yuiu8 d7)l?o1NFcJg%EAfM$P~L002ovPDHLkV1g^Dnv?(l delta 550 zcmV+>0@?ki0<;8>8Gi-<0051N9Sr~g00DDSM?wIu&K&6g00HhvL_t(I5v`QFOB_)Y z#?QI;j_a;jjf#Z$YJ7mH(xecJU?W)A`9CN~KrBV85C}GDQ=|;GDFPNjtWty!L{u=? zh>8yo%^GE+J9o~_IZFoiamQVQXP7%LzTbT3F@uf+9x&7cvVV%GdjTaC;zf>@mq<=3 z!c<%*UT)@yJ|0BK6~d4Jx-*KV`ZQ(@VyUPupum=XhInNG#Z_k-X|hK{B}~9IfiWx} zLD5QY6Vm)p0NrWymdkrHPN5Vgwd>5>4HI1=@PA+e^rq~CEj|n2X`??)0mUI*D{KBn zjv{V=y5X9|X@3grkpcXC6oou4ML~ezCc2EtnsQTB4tWNg?4bkf;hG7IMfhgNI(FV5 zGs4|*GyMTIY0$B=_*mso9+>eB z?J{?+FLkYu+4_Uk`r_>LHF~flZm0oBf#vr8%vJ>#p~!KNvqGG3)|f1T_)ydeh8$vDceZ>oNbH^|*hJ*t?Yc*1`WB&W>VYVEzu) zq#7;;VjO)t*nbgf(!`OXJBr45rP>>AQr$6c7slJWvbpNW@KTwna6d?PP>hvXCcp=4 zF;=GR@R4E7{4VU^0p4F>v^#A|>07*qoM6N<$f<+$JJpcdz delta 1274 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 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png index 4cde12118dda48d71e01fcb589a74d069c5d7cb5..4cd7b0099ca80c806f8fe495613e8d6c69460d76 100644 GIT binary patch delta 266 zcmV+l0rmcY2$}+r8Gi!+003c4mpuRg09{Z_R7L;)|5U~JDYo_jSDX9(|7FYh`2GLd z^Zv2r{H^2sT*&w!Y^SB+`<>qVZqE6)=lqo0`vF#&*75!I`TIh@_d&k*HoEtQyV-iD z%Xz2D9EQRbeYh5Nr~y=#0ZD;^+vz0$004MNL_t(2&&|%+4u6C&2tZM$Wf&dzefR%A z(^3-?6X>hnCz2Ba@RH&`m!pgy?n@#@AuLYB&}Q)FGY`?vcft0!vht0Z@M&ZeNCWXh75gzRTXR8EE3oN&6 Q00000NkvXXt^-0~g5kS`djJ3c delta 1014 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 Zxpx7IC@PekH?;r&002ovPDHLkV1i)CYaajr delta 1916 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 delta 1274 zcmV@pi1MCNO0zH7s z{8#}P0)7Ba8DqYf&QgSne>X__O83t$NZM4&R0{XJq|x}oAU?tcfC@|eNz$04T}34& z8DJf78R&>*Zz`k$q{`#gfGHnx7nlH^G{y`jfER)1<_fNi<9aM%_zrm1C`yPkKma(+ ztQ;y*CR2bbBYz>zG*SVsfpkGU(q>uHZf3iogk_%#9E|5SWeHrmAo>P;ejX7mwq#*} zW25m^ZI+{(Z8fI?4jM_fffY0nok=+88^|*_DwcW>mR#e+X$F_KMdb6sRz!~7K zkyN0G(3XQ+;z3X%PZ4gh;n-%62U<*VUKNv(D&IDi_4_D!s#MVXp|-XhH;H z#&@_;oApJVd}}5O@b=X_gJboD^-fM@6|#V@sA%X)Rlkd}3MLH0dGXGG&-HX|aD~|M zC)W#H7=H?AbtdaV#dGpubj_O^J-SlWpVNv-5(;wR%mvE9`Qaqo>03b&##eNNf=m#B z9@^lsd8tJ;BvI86kNV zc~0CY(7V{s+h%cWG|y=gt|q`z$l<(@qU=i?9q#uz`G?PgDMK!VMGidHZt*N+1L0ZI zFkH=mFtywc6rJ}C_?)=m)18V!ZQ`*-j(D`gCFK|nt#{bk*%%zuQ7o7kvJgA^=(^7b zzkm5GZ;jxRn{Wup8IOUx8D4uh&(=Ox-7$a;U><*5L^!% zxRlw)vAbh;sdlR||&e}8_8%)c2Fwy=F& zH|LM+p{pZB5DKTx>Y?F1N%BlZkXf!}Jb#viX>Oi;kBKp1x_fc0#UIbIeSJ^EkWFox zijdim{ojmn@#7EC*aY;fC0W*WN+DmQtE06pNK3SfZ^#@2K`6RgEuU_KwJTQ>E?Yar zc_9e#I$F8%>kuy-JI6ocSsYvQGbsxUCx04(w1z-pMRz9`kH5smmF@WHEG?dcYkv){ zV?kn3XB$_3zr*h1Uow)(<5)w5;3Wh1jHI)`ZlXp&!yEV{Y_~@;?CLwq;4eeaGOe6( zEsSSbwSGD0-`dUUl014$1_O8Gi!+006nq0-pc?0H{z*R7L;)|5U~JDYo_jSDXF*|5nEMy6F5^ z$M}8I`uzU?*Yf=uXr;5|{0m;6_Wb|A>ik^D_|)+I$?g3CSDK^3+eX0mD!2CP`2NN0 z{dLg!a?km&%iyTt`yiax0acdp`~T(l{$a`ZF1YpsRg(cvjDG_-U$Er-fz#Bw>2W$eUI#iU z)Wdgs8Y3U+A$Gd&{+j)d)BmGKx+43U_!tik_YlN)>$7G!hkE!s;%oku3;IwG3U^2k zw?z+HM)jB{@zFhK8P#KMSytSthr+4!c(5c%+^UBn_j%}l|2+O?a>_7qq7W zmx(qtA2nV^tZlLpy_#$U%ZNx5;$`0L&dZ!@e7rFXPGAOup%q`|03hpdtXsPP0000< KMNUMnLSTZ1N;Pr- delta 1891 zcmV-p2b}oI1m_Nr8Gi-<0052=@~r>>2QEoOK~#9!?VW3E6jc<*XLh$yKNt;)Mial3 z7z%<>zxaV5DhMs*(b6YIW1=KP6Jj(m21QYbiJ}su&;o5EN=$%gptMj6p|(7#AOTUJ zlt8fsX(iGq?ZQ50=XmbU+~w|cmz~|6$KBbz$-g^IcV>Hk`+q<8%-p?uMi3G-0B~!5 ze-yPCwFPw?HGmpMc~K)7BCq;C528+>zC*o^8h^XKC)IFgkv#xzm!ewK7j|kRa9dFo zC>MoDSR@P2#cWSU{i1oH5K2-Xb3jRz>|h7VOh0K` zhq^--L3H}A0r)nr z;Tr|-kPjB1s=ItpnS`oT%|U=a4oK-ZFIE^YBLH{u2#~@%%D^K)$`9*Tg(~9M-B+Zj z;~H?4LVsEt0eFtN4&>H(DZ@KpI6RhBKLL21CxC`J&m4Gc^9wwMZU#7SR1+KtuhSZM z+yLY}Vekzw6T_ApfEkuB_yU;e&a)L@rX~z70A_N+upOXN!qygmPDmKG0d%7CECcAI zgkd>ArzH$a0XjKsO$X@IgkcH5Y;m3`0G*yNOn(KK4GF_EfL4aB5i1j9o&Z{vFk~k> z&?@K2jQcJO%W!cddG(_DyfSoO55bUMHtbDF8DPkwF^~Ql#Eq4w15k{h%ML5Ar&pzi zl-D7v8kQXQ!&RRgKCW#5DZB$$6?mjWm50rRw*ukK>P-GkA|k69h{NARc>e}uLx+U4 z0DqE>7pa}9Fez+Vc-3jb`%i^uulglFoMzAVR|2%rf= zf#;74FXF^Ku_4+G&-4$KVy%YP>%2rxu2VG_cdm?XRjEhF&wPXJ># z_Q2+jGs=l~Fyx#MmGn+PZ0`@kBfGp|fO;Vov<$;z`(+sSZ7;Y=zXaF(8rb@CuQDV^ zq3i(2LfqO%AS!Ss>V%j7%>{6mtbYQrtQK5V4InPq0NZSaXv+f2U=&2}Z6OvkBfNHi z{LSaVJ!d5dC2K*ft_L^DRk;boQhOoVw!~Kt#0b2vd%!(&DF|~u1F@nG#LA5zR&7Fv z4GKgXooMSKb1g)6Obo-rgpuEP20T;W0Aa>55KC4gtQrKkAq-Hgs@FigV1GG8+rQ=z z6Jm=Bui-SfpDYLA=|vzGE(dYm=OC8XM&MDo7ux4UF1~0J1+i%aCUpRet3L_uNyQ*c zE(38Uy03H%I*)*Bh=Lb^Xj3?I^Hnbeq72(EOK^Y93CNp*uAA{5Lc=kyx=~RKa4{iT zm{_>_vSCm?$Ej=i6@=m%@PE9t1zZaoM}@2|h!#1K02~31S_I<0ZV=|K0}n!RRX6Ac zXmMf*5P-dLW}WPVsCKq)-x(0*txpZ2xrv3cxJ%l=7lpoNCyG< zK92ySAcmb-3m&}s@VwXv9(0#p<>B-5$bMxT;rk;OmENa6eM4D&LVo~01soUL39?R{ zyFLt3m|v?rCK7#KNu9E9Q4KV-pEUv^{rrClE&X&9I4-e7%pu_31#zGTOfC=ab%w20R*zBP+uT#l2{a~~~0wuG%6 zco*tVxK&e>%SJj*K!2tq*_h&ES5S9@TKb8WzpK;`&b9dNdxh4S)z%Q)o`aYWUh}9L z(`p!#WO5IxI|nf?yz{90R93Ed6@2qim*}Zjj$H&Esd`?JsFJUnDfiAgF_eYiWR3GC z>M9SHDylEWrA(%mfm~;u7OU9!Wz^!7Z%jZF zi@JR;>Mhi7S>V7wQ176|FdW2m?&`qa(ScO^CFPR80HucLHOTy%5s*HR0^8)i0WYBP d*#0Ks^FNSabJA*5${_#%002ovPDHLkV1gB0Vle;! diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png index a6d6b8609df07bf62e5100a53a01510388bd2b22..0ec303439225b78712f49115768196d8d76f6790 100644 GIT binary patch delta 850 zcmV-Y1Fih&6y64q8Gi!+000iU#^3+|0OwFlR7L;)|5U~J09TtSw)Xt~|5(QO`~Ck( z!T0|D|3<*~RmJ%E{r+;#`2ba!klFf7!uJMSo%Q?vP{jByxcAZE>;OrUCbaZYjJo^$ z{nGILmD~Da$@upC{`C6(Ey4dPw)Pyc^>5DkHoEo!QcuK-Jwl-l}t(fQKv z{dds$V#@dygS`PvhX6is7Z+@*x-d;$ zb=6f@U3Jw}_s+W3%*+b9H_vS)-R#9?zrXogeLVI2We2RFTTAL}&3C8PS~<5D&v@UI z+`s*$wqQ=yd$laNUY-|ovcS9~n_90tFUdl#qq0tEUXle|k{Op|DHpSrbxEeZ5~$>o%>OSe z^=41qvh3LlC2xXzu+-2eQoqs1^L>7ylB$bCP);(%(xYZL1 cY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f@rA97ytkO literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png index a6d6b8609df07bf62e5100a53a01510388bd2b22..0ec303439225b78712f49115768196d8d76f6790 100644 GIT binary patch delta 850 zcmV-Y1Fih&6y64q8Gi!+000iU#^3+|0OwFlR7L;)|5U~J09TtSw)Xt~|5(QO`~Ck( z!T0|D|3<*~RmJ%E{r+;#`2ba!klFf7!uJMSo%Q?vP{jByxcAZE>;OrUCbaZYjJo^$ z{nGILmD~Da$@upC{`C6(Ey4dPw)Pyc^>5DkHoEo!QcuK-Jwl-l}t(fQKv z{dds$V#@dygS`PvhX6is7Z+@*x-d;$ zb=6f@U3Jw}_s+W3%*+b9H_vS)-R#9?zrXogeLVI2We2RFTTAL}&3C8PS~<5D&v@UI z+`s*$wqQ=yd$laNUY-|ovcS9~n_90tFUdl#qq0tEUXle|k{Op|DHpSrbxEeZ5~$>o%>OSe z^=41qvh3LlC2xXzu+-2eQoqs1^L>7ylB$bCP);(%(xYZL1 cY5!B-0ft0f?Lgb>C;$Ke07*qoM6N<$f@rA97ytkO literal 2665 zcmV-v3YPVWP)oFh3q0MFesq&64WThn3$;G69TfjsAv=f2G9}p zgSx99+!YV6qME!>9MD13x)k(+XE7W?_O4LoLb5ND8 zaV{9+P@>42xDfRiYBMSgD$0!vssptcb;&?u9u(LLBKmkZ>RMD=kvD3h`sk6!QYtBa ztlZI#nu$8lJ^q2Z79UTgZe>BU73(Aospiq+?SdMt8lDZ;*?@tyWVZVS_Q7S&*tJaiRlJ z+aSMOmbg3@h5}v;A*c8SbqM3icg-`Cnwl;7Ts%A1RkNIp+Txl-Ckkvg4oxrqGA5ewEgYqwtECD<_3Egu)xGllKt&J8g&+=ac@Jq4-?w6M3b*>w5 z69N3O%=I^6&UL5gZ!}trC7bUj*12xLdkNs~Bz4QdJJ*UDZox2UGR}SNg@lmOvhCc~ z*f_UeXv(=#I#*7>VZx2ObEN~UoGUTl=-@)E;YtCRZ>SVp$p9yG5hEFZ!`wI!spd)n zSk+vK0Vin7FL{7f&6OB%f;SH22dtbcF<|9fi2Fp%q4kxL!b1#l^)8dUwJ zwEf{(wJj@8iYDVnKB`eSU+;ml-t2`@%_)0jDM`+a46xhDbBj2+&Ih>1A>6aky#(-SYyE{R3f#y57wfLs z6w1p~$bp;6!9DX$M+J~S@D6vJAaElETnsX4h9a5tvPhC3L@qB~bOzkL@^z0k_hS{T4PF*TDrgdXp+dzsE? z>V|VR035Pl9n5&-RePFdS{7KAr2vPOqR9=M$vXA1Yy5>w;EsF`;OK{2pkn-kpp9Pw z)r;5JfJKKaT$4qCb{TaXHjb$QA{y0EYy*+b1XI;6Ah- zw13P)xT`>~eFoJC!>{2XL(a_#upp3gaR1#5+L(Jmzp4TBnx{~WHedpJ1ch8JFk~Sw z>F+gN+i+VD?gMXwcIhn8rz`>e>J^TI3E-MW>f}6R-pL}>WMOa0k#jN+`RyUVUC;#D zg|~oS^$6%wpF{^Qr+}X>0PKcr3Fc&>Z>uv@C);pwDs@2bZWhYP!rvGx?_|q{d`t<*XEb#=aOb=N+L@CVBGqImZf&+a zCQEa3$~@#kC);pasdG=f6tuIi0PO-y&tvX%>Mv=oY3U$nD zJ#gMegnQ46pq+3r=;zmgcG+zRc9D~c>z+jo9&D+`E6$LmyFqlmCYw;-Zooma{sR@~ z)_^|YL1&&@|GXo*pivH7k!msl+$Sew3%XJnxajt0K%3M6Bd&YFNy9}tWG^aovK2eX z1aL1%7;KRDrA@eG-Wr6w+;*H_VD~qLiVI`{_;>o)k`{8xa3EJT1O_>#iy_?va0eR? zDV=N%;Zjb%Z2s$@O>w@iqt!I}tLjGk!=p`D23I}N4Be@$(|iSA zf3Ih7b<{zqpDB4WF_5X1(peKe+rASze%u8eKLn#KKXt;UZ+Adf$_TO+vTqshLLJ5c z52HucO=lrNVae5XWOLm!V@n-ObU11!b+DN<$RuU+YsrBq*lYT;?AwJpmNKniF0Q1< zJCo>Q$=v$@&y=sj6{r!Y&y&`0$-I}S!H_~pI&2H8Z1C|BX4VgZ^-! zje3-;x0PBD!M`v*J_)rL^+$<1VJhH*2Fi~aA7s&@_rUHYJ9zD=M%4AFQ`}k8OC$9s XsPq=LnkwKG00000NkvXXu0mjfhAk5^ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png index 75b2d164a5a98e212cca15ea7bf2ab5de5108680..e9f5fea27c705180eb716271f41b582e76dcbd90 100644 GIT binary patch delta 1668 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 diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png index c4df70d39da7941ef3f6dcb7f06a192d8dcb308d..84ac32ae7d989f82d5e46a60405adcc8279e8001 100644 GIT binary patch delta 749 zcmVg;Ps8|O$@u8^{Z_{KM!@$5TAfS6_e#O{MZfpz`2O`0$7~@NRr(1{THzH08y3x{{PYM{eL;T_A9^tcF_4Sxb`8l z_9V3RD6;a(-0A^Pjsi!1?)d#Ap4Tk3^CP0(07;VpJ7@tgQ}z4)*zx@&yZwC9`DV-b z0ZobH_5IB4{KxD3;p_6%|f=bdFhu+F!zMZ2UFj;GUKX7tI;hv3{q~!*pMj75WP_c}> z6)IWvg5_yyg<9Op()eD1hWC19M@?_9_MHec{Z8n3FMs~w_u?Av_yNBmRxVYrpi(M% zFMP21g+hmocQp3ay*Su=qM6He)*HaaTg$E^sym`(t%s3A)x!M+vfjXUBEpK6X9%iU zU!u9jj3(-$dM~sJ%Liy#?|+!6IY#MTau#O6vVj`yh_7%Ni!?!VS+MPTO(_fG+1<#p zqu;A#i+_(N%CmVnYvb>#nA{>Q%3E`Ds7<~jZMywn@h2t>G-LrYy7?Dj{aZqhQd6tzX%(Trn+ z)HNF}%-F{rr=m*0{=a;s#YDL00000NkvXXu0mjfaGjYE delta 1884 zcmV-i2c!7<1>g>l8Gi-<0076AQ7Zrd2Pa8HK~#9!?VNjT6h$1z_m0EFf5bmb1dTDK zp;kdKV1h(V(8Sc1M<37!RE>znAk{x4#zX@eOeE1j3~!+nB5IL z<xS}u?#DBMB>w^b($1Z)`9G?eP95EKi& z$eOy@K%h;ryrR3la%;>|o*>CgB(s>dDcNOXg}CK9SPmD?Uu$P4(=PGA0ShFasNfcIHTL?9WjB9#(2xSLC z`0%$#9DW9F;B4mbU{BlaYx!SjF!QSeF~(msQRxwboh5B_O$BWOQja)GboJz$&!?mgB&3$ytsA zvns&b3Cl5Hx#%p%faR*Q906u&fbXy$maV`n?S>A)vJIH!F-vxCrY+rq5_JA(GcOgu7(Ky4X3ATR9z8*%k&<5qYeV&4Y`~}XYmK(j{)!g8d2UgHXIINM!Rvn zKtEq~Foe0s!U{kux~F6Y7Sp+2f|*Cc${S{@oh8D0=XhB8Ec-w9CflfL+te4ium2cU zoPTCj_m<3d#gjK=<*8R`HP^C$lOPM5d~UhKhRRmvv{LI za^|oavk1$QiEApSrP@~Jjbg`<*dW4TO@DPEEX$Tg$xh?Y>Qd}y@kaH~IT8!lLpS^J zR7(&wZSI6+>Eb)tX>9Z?GX#q$u z4I>7e#b7ojyJ1grOh!^}s8S#ubi^Jkd1?UK)3mp6rI^_zxRY zrx6_QmhoWoDR`fp4R7gu6@OBFGu7IDVR6~nJsB{^f5jHn<{WJ&&f^X?3f8TIk3#U& zu1*Q-e@;snJxNx8-PBnpI|uFTKN!+Lp;fPfZ+eqqU^Y1|#DJY~126?zOx-+d>%4*? z&o`TbrXSNXZW^!P0t2>@$6&aiBtUDh2wLXLD9&a(1J=k_FK|iGbAQ@x4Qmx}Ms+*; zze&q6bH(=wYuXHfz0H6<05!LkE4rl~v^!bj=^9d+vI5fN<;GP>*Pas=q2l9RxDkk` zPRk&EQI+t_0$Y%nKE)Ma)W?jaA@4Z{h zTk*7;;#lG?hvTN_On=Jaxp%bdE;mDq(q#dgdYF|-?wrMeI4h`$idZ6^VyXZVlaCd0 z;i)OYR3npf@9>00Gqn##Zb4HRurgaWFCzL9u6@J@sse>Z1XznxWvSy%Td32I3!#YN zXt9v0)RQtDDZRd?#WY?~KF7A0UcR{jt9 W+;fr}hV%pg0000&=UXv0SHh`R7L;)|5U~JDYo_jSDRDC`1<|-SjPDL z{{Q{{{{H{}09Kk-#rR9Y_viNgVafPO!S|ls`uzR=MZfp^{QU=8od8La1X`Tr_Wmff z_5e$ivgQ1@=KMy$_g9a+`TPAle6cOJ_Fc#L7qIpvwDkd1mw$fK`6IOUD75rX!}mad zv(fMTE4=(Nx%L54lL1hVF1YpqNrC`FddBPg#_Ietx%Lrkq5wX00X1L{S%Cm9QY*av z#_Rh5PKy9KYTWbvz3BX9%J>0Hi1+#X{rLA{m%$Kamk?i!03AC38#Yrxs)5QTeTVRiEmz~MKK1WAjCw(c-JK6eox;2O)?`?TG`AHia671e^vgmp!llK zp|=5sVHk#C7=~epA~VAf-~%aPC=%Qw01h8mnSZ|p?tc*y?iZ$PR7_ceEIapF3KB14K0Pog?7wtd+^xgUCa_GVmlD z<^nU>AU_Yn-JU?NFdu|wf^bTCNf-wSBYVZltDdvGBln-YrbeGvJ!|s{#`gjN@yAMb zM6cjFz0eFECCsc|_8hTa3*9-JQGehksdoVP^K4m?&wpA~+|b%{EP5D-+7h)6CE; z*{>BP=GRR3Ea}xyV*bqry{l^J=0#DaC4ej;1qs8_by?H6Tr@7hl>UKNZt)^B&yl;)&oqzLg zcfZxpE?3k%_iTOVywh%`XVN-E#COl+($9{v(pqSQcrz=)>G!!3HeNxbXGM@})1|9g zG4*@(OBaMvY0P0_TfMFPh fVHk#CZX3S=^^2mI>Ux-D00000NkvXXu0mjfzK(<8 literal 3294 zcmV<43?cK0P)1^@s67{VYS000c7NklQEG_j zup^)eW&WUIApqy$=APz8jE@awGp)!bsTjDbrJO`$x^ZR^dr;>)LW>{ zs70vpsD38v)19rI=GNk1b(0?Js9~rjsQsu*K;@SD40RB-3^gKU-MYC7G!Bw{fZsqp zih4iIi;Hr_xZ033Iu{sQxLS=}yBXgLMn40d++>aQ0#%8D1EbGZp7+ z5=mK?t31BkVYbGOxE9`i748x`YgCMwL$qMsChbSGSE1`p{nSmadR zcQ#R)(?!~dmtD0+D2!K zR9%!Xp1oOJzm(vbLvT^$IKp@+W2=-}qTzTgVtQ!#Y7Gxz}stUIm<1;oBQ^Sh2X{F4ibaOOx;5ZGSNK z0maF^@(UtV$=p6DXLgRURwF95C=|U8?osGhgOED*b z7woJ_PWXBD>V-NjQAm{~T%sjyJ{5tn2f{G%?J!KRSrrGvQ1(^`YLA5B!~eycY(e5_ z*%aa{at13SxC(=7JT7$IQF~R3sy`Nn%EMv!$-8ZEAryB*yB1k&stni)=)8-ODo41g zkJu~roIgAih94tb=YsL%iH5@^b~kU9M-=aqgXIrbtxMpFy5mekFm#edF9z7RQ6V}R zBIhbXs~pMzt0VWy1Fi$^fh+1xxLDoK09&5&MJl(q#THjPm(0=z2H2Yfm^a&E)V+a5 zbi>08u;bJsDRUKR9(INSc7XyuWv(JsD+BB*0hS)FO&l&7MdViuur@-<-EHw>kHRGY zqoT}3fDv2-m{NhBG8X}+rgOEZ;amh*DqN?jEfQdqxdj08`Sr=C-KmT)qU1 z+9Cl)a1mgXxhQiHVB}l`m;-RpmKy?0*|yl?FXvJkFxuu!fKlcmz$kN(a}i*saM3nr z0!;a~_%Xqy24IxA2rz<+08=B-Q|2PT)O4;EaxP^6qixOv7-cRh?*T?zZU`{nIM-at zTKYWr9rJ=tppQ9I#Z#mLgINVB!pO-^FOcvFw6NhV0gztuO?g ztoA*C-52Q-Z-P#xB4HAY3KQVd%dz1S4PA3vHp0aa=zAO?FCt zC_GaTyVBg2F!bBr3U@Zy2iJgIAt>1sf$JWA9kh{;L+P*HfUBX1Zy{4MgNbDfBV_ly z!y#+753arsZUt@366jIC0klaC@ckuk!qu=pAyf7&QmiBUT^L1&tOHzsK)4n|pmrVT zs2($4=?s~VejTFHbFdDOwG;_58LkIj1Fh@{glkO#F1>a==ymJS$z;gdedT1zPx4Kj ztjS`y_C}%af-RtpehdQDt3a<=W5C4$)9W@QAse;WUry$WYmr51ml9lkeunUrE`-3e zmq1SgSOPNEE-Mf+AGJ$g0M;3@w!$Ej;hMh=v=I+Lpz^n%Pg^MgwyqOkNyu2c^of)C z1~ALor3}}+RiF*K4+4{(1%1j3pif1>sv0r^mTZ?5Jd-It!tfPfiG_p$AY*Vfak%FG z4z#;wLtw&E&?}w+eKG^=#jF7HQzr8rV0mY<1YAJ_uGz~$E13p?F^fPSzXSn$8UcI$ z8er9{5w5iv0qf8%70zV71T1IBB1N}R5Kp%NO0=5wJalZt8;xYp;b{1K) zHY>2wW-`Sl{=NpR%iu3(u6l&)rc%%cSA#aV7WCowfbFR4wcc{LQZv~o1u_`}EJA3>ki`?9CKYTA!rhO)if*zRdd}Kn zEPfYbhoVE~!FI_2YbC5qAj1kq;xP6%J8+?2PAs?`V3}nyFVD#sV3+uP`pi}{$l9U^ zSz}_M9f7RgnnRhaoIJgT8us!1aB&4!*vYF07Hp&}L zCRlop0oK4DL@ISz{2_BPlezc;xj2|I z23RlDNpi9LgTG_#(w%cMaS)%N`e>~1&a3<{Xy}>?WbF>OOLuO+j&hc^YohQ$4F&ze z+hwnro1puQjnKm;vFG~o>`kCeUIlkA-2tI?WBKCFLMBY=J{hpSsQ=PDtU$=duS_hq zHpymHt^uuV1q@uc4bFb{MdG*|VoW@15Osrqt2@8ll0qO=j*uOXn{M0UJX#SUztui9FN4)K3{9!y8PC-AHHvpVTU;x|-7P+taAtyglk#rjlH2 z5Gq8ik}BPaGiM{#Woyg;*&N9R2{J0V+WGB69cEtH7F?U~Kbi6ksi*`CFXsi931q7Y zGO82?whBhN%w1iDetv%~wM*Y;E^)@Vl?VDj-f*RX>{;o_=$fU!&KAXbuadYZ46Zbg z&6jMF=49$uL^73y;;N5jaHYv)BTyfh&`qVLYn?`o6BCA_z-0niZz=qPG!vonK3MW_ zo$V96zM!+kJRs{P-5-rQVse0VBH*n6A58)4uc&gfHMa{gIhV2fGf{st>E8sKyP-$8zp~wJX^A*@DI&-;8>gANXZj zU)R+Y)PB?=)a|Kj>8NXEu^S_h^7R`~Q&7*Kn!xyvzVv&^>?^iu;S~R2e-2fJx-oUb cX)(b1KSk$MOV07*qoM6N<$f&{Qds= z{r_0T`1}6fwc-8!#-TGX}_?g)CZq4{k!uZ_g@DrQdoW0kI zu+W69&uN^)W`CK&06mMNcYMVF00dG=L_t(|+U?wHQxh>12H+Dm+1+fh+IF>G0SjJM zkQQre1x4|G*Z==(Ot&kCnUrL4I(rf(ucITwmuHf^hXiJTkdTm&kdTm&kdTm&kdP`e zsgWG0BcWCVkVZ&2dUwN`cgM8QJb`Z7Z~e<&Yj2(}>VI$fQI%^ugM`#6By?GeadWcu z0gy9!D`m!H>Bd!JW(@avE8`|5XX(0PN}!8K>`dkavs;rHL+wy96QGNT=S@#7%xtlm zIW!++@*2zm-Py#Zr`DzqsLm!b{iskFNULSqE9A>SqHem>o31A%XL>S_5?=;V_i_y+ z(xxXhnt#r-l1Y8_*h`r?8Tr|)(RAiO)4jQR`13X0mx07C&p@KBP_2s``KEhv^|*8c z$$_T(v6^1Ig=#R}sE{vjA?ErGDZGUsyoJuWdJMc7Nb1^KF)-u<7q zPy$=;)0>vuWuK2hQhswLf!9yg`88u&eBbR8uhod?Nw09AXH}-#qOLLxeT2%C;R)QQ$Za#qp~cM&YVmS4i-*Fpd!cC zBXc?(4wcg>sHmXGd^VdE<5QX{Kyz$;$sCPl(_*-P2Iw?p^C6J2ZC!+UppiK6&y3Kmbv&O!oYF34$0Z;QO!J zOY#!`qyGH<3Pd}Pt@q*A0V=3SVtWKRR8d8Z&@)3qLPA19LPA19LPEUCUoZo%k(yku QW&i*H07*qoM6N<$g47z!?*IS* literal 3612 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 iW8 + CADisableMinimumFrameDurationOnPhone + CFBundleDevelopmentRegion $(DEVELOPMENT_LANGUAGE) + CFBundleDisplayName + Stacked Services Example CFBundleExecutable $(EXECUTABLE_NAME) CFBundleIdentifier @@ -11,7 +15,7 @@ CFBundleInfoDictionaryVersion 6.0 CFBundleName - example + stacked_services_example CFBundlePackageType APPL CFBundleShortVersionString @@ -22,6 +26,29 @@ $(FLUTTER_BUILD_NUMBER) LSRequiresIPhoneOS + UIApplicationSceneManifest + + UIApplicationSupportsMultipleScenes + + UISceneConfigurations + + UIWindowSceneSessionRoleApplication + + + UISceneClassName + UIWindowScene + UISceneConfigurationName + flutter + UISceneDelegateClassName + $(PRODUCT_MODULE_NAME).SceneDelegate + UISceneStoryboardFile + Main + + + + + UIApplicationSupportsIndirectInputEvents + UILaunchStoryboardName LaunchScreen UIMainStoryboardFile @@ -39,9 +66,5 @@ UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight - UIViewControllerBasedStatusBarAppearance - - CADisableMinimumFrameDurationOnPhone - diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/example/ios/Runner/Runner-Bridging-Header.h index 7335fdf..308a2a5 100644 --- a/example/ios/Runner/Runner-Bridging-Header.h +++ b/example/ios/Runner/Runner-Bridging-Header.h @@ -1 +1 @@ -#import "GeneratedPluginRegistrant.h" \ No newline at end of file +#import "GeneratedPluginRegistrant.h" diff --git a/example/ios/Runner/SceneDelegate.swift b/example/ios/Runner/SceneDelegate.swift new file mode 100644 index 0000000..b9ce8ea --- /dev/null +++ b/example/ios/Runner/SceneDelegate.swift @@ -0,0 +1,6 @@ +import Flutter +import UIKit + +class SceneDelegate: FlutterSceneDelegate { + +} diff --git a/example/ios/RunnerTests/RunnerTests.swift b/example/ios/RunnerTests/RunnerTests.swift new file mode 100644 index 0000000..86a7c3b --- /dev/null +++ b/example/ios/RunnerTests/RunnerTests.swift @@ -0,0 +1,12 @@ +import Flutter +import UIKit +import XCTest + +class RunnerTests: XCTestCase { + + func testExample() { + // If you add code to the Runner application, consider adding tests here. + // See https://developer.apple.com/documentation/xctest for more information about using XCTest. + } + +} From b5a659855ec67b71d97ece63aeebae873b391acd Mon Sep 17 00:00:00 2001 From: Fernando Ferrara Date: Mon, 22 Jun 2026 13:19:27 -0300 Subject: [PATCH 4/4] style: apply dart format Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01CSHtjk6H5yhz977P7YXgPM --- .../migration_smoke_test.dart | 3 +- example/lib/ui/views/dialog_view.dart | 28 +++++++++++++------ lib/src/dialog/dialog_service.dart | 14 +++++++--- lib/src/navigation/navigation_service.dart | 3 +- lib/src/snackbar/snackbar_service.dart | 3 +- .../snackbar/vendored/stacked_snackbar.dart | 7 +++-- .../vendored/stacked_snackbar_controller.dart | 8 ++++-- lib/src/stacked_service.dart | 3 +- 8 files changed, 46 insertions(+), 23 deletions(-) diff --git a/example/integration_test/migration_smoke_test.dart b/example/integration_test/migration_smoke_test.dart index a555a6e..414d7d6 100644 --- a/example/integration_test/migration_smoke_test.dart +++ b/example/integration_test/migration_smoke_test.dart @@ -112,7 +112,8 @@ void main() { expect(find.text('Second Screen'), findsOneWidget); // Unwind the stack so transition tickers are disposed before teardown. - final navigator = tester.state(find.byType(Navigator).first); + final navigator = + tester.state(find.byType(Navigator).first); navigator.pop(); await tester.pumpAndSettle(); navigator.pop(); diff --git a/example/lib/ui/views/dialog_view.dart b/example/lib/ui/views/dialog_view.dart index c89a6db..1c0d6eb 100644 --- a/example/lib/ui/views/dialog_view.dart +++ b/example/lib/ui/views/dialog_view.dart @@ -93,12 +93,16 @@ class DialogView extends StatelessWidget { await _dialogService.showCustomDialog( variant: DialogType.Basic, title: 'This is a custom UI with Text as main button', - description: 'Sheck out the builder in the dialog_ui_register.dart file', + description: + 'Sheck out the builder in the dialog_ui_register.dart file', mainButtonTitle: 'Ok', showIconInMainButton: false, barrierDismissible: true, - routeSettings: RouteSettings(name: '/customDialogWithTransition'), - transitionBuilder: (context, animation, secondaryAnimation, child) => SlideTransition( + routeSettings: + RouteSettings(name: '/customDialogWithTransition'), + transitionBuilder: + (context, animation, secondaryAnimation, child) => + SlideTransition( position: animation.drive( Tween( begin: const Offset(1.0, 0.0), @@ -122,10 +126,13 @@ class DialogView extends StatelessWidget { ), OutlinedButton( onPressed: () async { - final response = await _dialogService.showCustomDialog( + final response = await _dialogService.showCustomDialog< + GenericDialogResponse, GenericDialogRequest>( variant: DialogType.Generic, - title: 'This is a custom Generic UI with Text as main button', - description: 'Sheck out the builder in the dialog_ui_register.dart file', + title: + 'This is a custom Generic UI with Text as main button', + description: + 'Sheck out the builder in the dialog_ui_register.dart file', mainButtonTitle: 'Ok', showIconInMainButton: false, barrierDismissible: true, @@ -144,7 +151,8 @@ class DialogView extends StatelessWidget { await _dialogService.showCustomDialog( variant: DialogType.Basic, title: 'This is a custom UI with icon', - description: 'Sheck out the builder in the dialog_ui_register.dart file', + description: + 'Sheck out the builder in the dialog_ui_register.dart file', showIconInMainButton: true, routeSettings: RouteSettings(name: '/customDialog'), ); @@ -166,7 +174,8 @@ class DialogView extends StatelessWidget { title: 'Test Confirmation Dialog Title', description: 'Test Confirmation Dialog Description', barrierDismissible: true, - routeSettings: RouteSettings(name: '/materialConfirmationDialog'), + routeSettings: + RouteSettings(name: '/materialConfirmationDialog'), ); }, child: Text( @@ -210,7 +219,8 @@ class DialogView extends StatelessWidget { title: 'Test Confirmation Dialog Title', description: 'Test Confirmation Dialog Description', barrierDismissible: true, - routeSettings: RouteSettings(name: '/materialConfirmationDialog'), + routeSettings: + RouteSettings(name: '/materialConfirmationDialog'), ); }, child: Text( diff --git a/lib/src/dialog/dialog_service.dart b/lib/src/dialog/dialog_service.dart index d3c4a4e..efe20c4 100644 --- a/lib/src/dialog/dialog_service.dart +++ b/lib/src/dialog/dialog_service.dart @@ -27,9 +27,11 @@ class DialogService { _dialogBuilders = {...?_dialogBuilders, ...builders}; } - Map _customDialogBuilders = Map(); + Map _customDialogBuilders = + Map(); - @Deprecated('Prefer to use the StackedServices.navigatorKey instead of using this key. This will be removed in the next major version update for stacked.') + @Deprecated( + 'Prefer to use the StackedServices.navigatorKey instead of using this key. This will be removed in the next major version update for stacked.') get navigatorKey { return StackedService.navigatorKey; } @@ -46,7 +48,9 @@ class DialogService { ) void registerCustomDialogBuilder({ required dynamic variant, - required Widget Function(BuildContext, DialogRequest, Function(DialogResponse)) builder, + required Widget Function( + BuildContext, DialogRequest, Function(DialogResponse)) + builder, }) { _customDialogBuilders[variant] = builder; } @@ -202,7 +206,9 @@ class DialogService { RouteSettings? routeSettings, GlobalKey? navigatorKey, RouteTransitionsBuilder? transitionBuilder, - @Deprecated('Prefer to use `data` and pass in a generic type. customData doesn\'t work anymore') dynamic customData, + @Deprecated( + 'Prefer to use `data` and pass in a generic type. customData doesn\'t work anymore') + dynamic customData, R? data, }) { assert( diff --git a/lib/src/navigation/navigation_service.dart b/lib/src/navigation/navigation_service.dart index 157efa2..c5cd92f 100644 --- a/lib/src/navigation/navigation_service.dart +++ b/lib/src/navigation/navigation_service.dart @@ -165,7 +165,8 @@ class NavigationService { Transition? transitionStyle, String? routeName, }) { - return _navigator(id)?.pushReplacement(buildTransitionRoute( + return _navigator(id) + ?.pushReplacement(buildTransitionRoute( page, transition: transitionStyle ?? transitionClass ?? diff --git a/lib/src/snackbar/snackbar_service.dart b/lib/src/snackbar/snackbar_service.dart index 62c4b76..202d6bc 100644 --- a/lib/src/snackbar/snackbar_service.dart +++ b/lib/src/snackbar/snackbar_service.dart @@ -100,7 +100,8 @@ class SnackbarService { onTap: onTap, barBlur: _snackbarConfig?.barBlur ?? 7.0, isDismissible: _snackbarConfig?.isDismissible ?? true, - duration: duration ?? _snackbarConfig?.duration ?? const Duration(seconds: 3), + duration: + duration ?? _snackbarConfig?.duration ?? const Duration(seconds: 3), snackPosition: _snackbarConfig?.snackPosition ?? SnackPosition.TOP, backgroundColor: _snackbarConfig?.backgroundColor ?? Colors.grey[800]!, margin: _snackbarConfig?.margin ?? diff --git a/lib/src/snackbar/vendored/stacked_snackbar.dart b/lib/src/snackbar/vendored/stacked_snackbar.dart index e95d535..c2670d5 100644 --- a/lib/src/snackbar/vendored/stacked_snackbar.dart +++ b/lib/src/snackbar/vendored/stacked_snackbar.dart @@ -230,7 +230,8 @@ class StackedSnackbarState extends State : widget.backgroundColor, child: SafeArea( minimum: widget.snackPosition == SnackPosition.BOTTOM - ? EdgeInsets.only(bottom: MediaQuery.of(context).viewInsets.bottom) + ? EdgeInsets.only( + bottom: MediaQuery.of(context).viewInsets.bottom) : EdgeInsets.only(top: MediaQuery.of(context).padding.top), bottom: widget.snackPosition == SnackPosition.BOTTOM, top: widget.snackPosition == SnackPosition.TOP, @@ -398,8 +399,8 @@ You need to either use message[String], or messageText[Widget] or define a userI : null, ), child: Padding( - padding: - const EdgeInsets.only(left: 8.0, right: 8.0, bottom: 8.0, top: 16.0), + padding: const EdgeInsets.only( + left: 8.0, right: 8.0, bottom: 8.0, top: 16.0), child: FocusScope( node: _focusNode, autofocus: true, diff --git a/lib/src/snackbar/vendored/stacked_snackbar_controller.dart b/lib/src/snackbar/vendored/stacked_snackbar_controller.dart index b94252d..1a17631 100644 --- a/lib/src/snackbar/vendored/stacked_snackbar_controller.dart +++ b/lib/src/snackbar/vendored/stacked_snackbar_controller.dart @@ -162,7 +162,8 @@ class StackedSnackbarController { } Animation _createColorOverlayColor() { - return ColorTween(begin: const Color(0x00000000), end: snackbar.overlayColor) + return ColorTween( + begin: const Color(0x00000000), end: snackbar.overlayColor) .animate( CurvedAnimation( parent: _controller, @@ -227,8 +228,9 @@ class StackedSnackbarController { Widget _getBodyWidget() { return Builder(builder: (_) { return GestureDetector( - onTap: - snackbar.onTap != null ? () => snackbar.onTap?.call(snackbar) : null, + onTap: snackbar.onTap != null + ? () => snackbar.onTap?.call(snackbar) + : null, child: snackbar, ); }); diff --git a/lib/src/stacked_service.dart b/lib/src/stacked_service.dart index 687e8fb..efbaf87 100644 --- a/lib/src/stacked_service.dart +++ b/lib/src/stacked_service.dart @@ -17,7 +17,8 @@ class StackedService { /// Creates and/or returns a navigator key based on the index passed in static GlobalKey? nestedNavigationKey(int index) => - _nestedNavigationKeys.putIfAbsent(index, () => GlobalKey()); + _nestedNavigationKeys.putIfAbsent( + index, () => GlobalKey()); /// The routing state maintained by [routeObserver]. Native replacement for /// `Get.routing`.