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/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/integration_test/migration_smoke_test.dart b/example/integration_test/migration_smoke_test.dart new file mode 100644 index 0000000..414d7d6 --- /dev/null +++ b/example/integration_test/migration_smoke_test.dart @@ -0,0 +1,123 @@ +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/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 28c6bf0..7353c41 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png index 2ccbfd9..797d452 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png index f091b6b..6ed2d93 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png differ 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 4cde121..4cd7b00 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png index d0ef06e..fe73094 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png index dcdc230..321773c 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png index 2ccbfd9..797d452 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png index c8f9ed8..502f463 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png differ 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 a6d6b86..0ec3034 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png differ 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 a6d6b86..0ec3034 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png differ 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 75b2d16..e9f5fea 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png differ 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 c4df70d..84ac32a 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png index 6a84f41..8953cba 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png differ diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png index d0e1f58..0467bf1 100644 Binary files a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png and b/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png differ diff --git a/example/ios/Runner/Info.plist b/example/ios/Runner/Info.plist index 1579fb3..9f9ff60 100644 --- a/example/ios/Runner/Info.plist +++ b/example/ios/Runner/Info.plist @@ -2,8 +2,12 @@ + 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. + } + +} 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/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/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(); 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..efe20c4 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, @@ -26,11 +27,13 @@ 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 Get.key; + return StackedService.navigatorKey; } /// Registers a custom dialog builder. The builder function has been updated to include the function to call @@ -45,13 +48,15 @@ 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; } /// Check if dialog is open - bool? get isDialogOpen => Get.isDialogOpen; + bool? get isDialogOpen => StackedService.routing.isDialog; /// Shows a dialog to the user /// @@ -86,7 +91,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 +122,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 +171,6 @@ class DialogService { ), ], ), - barrierDismissible: barrierDismissible, - routeSettings: routeSettings, - navigatorKey: navigatorKey, ); } @@ -192,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( @@ -207,13 +223,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 +289,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..c5cd92f 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,18 @@ 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 +184,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 +218,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 +234,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 +254,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 +281,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 +302,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 +315,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 +366,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 +379,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..202d6bc 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,43 @@ 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 +178,7 @@ class SnackbarService { config: snackbarConfig, ); - final getBar = GetSnackBar( + final getBar = StackedSnackbar( key: Key('snackbar_view'), titleText: title != null ? Text( @@ -229,8 +232,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 +258,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..c2670d5 --- /dev/null +++ b/lib/src/snackbar/vendored/stacked_snackbar.dart @@ -0,0 +1,535 @@ +// 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..1a17631 --- /dev/null +++ b/lib/src/snackbar/vendored/stacked_snackbar_controller.dart @@ -0,0 +1,407 @@ +// 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..efbaf87 100644 --- a/lib/src/stacked_service.dart +++ b/lib/src/stacked_service.dart @@ -1,19 +1,34 @@ 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()), + ); + }); + }); +}