├── .fvm └── fvm_config.json ├── .github ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── actions │ └── setup │ │ └── action.yaml └── workflows │ ├── checkout.yml │ └── test-report.yml ├── .gitignore ├── .metadata ├── .vscode ├── extensions.json ├── launch.json ├── settings.json └── tasks.json ├── CHANGELOG.md ├── LICENSE ├── Makefile ├── README.md ├── analysis_options.yaml ├── dart_test.yaml ├── example ├── .gitignore ├── .metadata ├── README.md ├── analysis_options.yaml ├── android │ ├── .gitignore │ ├── app │ │ ├── build.gradle │ │ └── src │ │ │ ├── debug │ │ │ └── AndroidManifest.xml │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ ├── kotlin │ │ │ │ └── com │ │ │ │ │ └── example │ │ │ │ │ └── example │ │ │ │ │ └── MainActivity.kt │ │ │ └── res │ │ │ │ ├── drawable-v21 │ │ │ │ └── launch_background.xml │ │ │ │ ├── drawable │ │ │ │ └── launch_background.xml │ │ │ │ ├── mipmap-hdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-mdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ └── ic_launcher.png │ │ │ │ ├── values-night │ │ │ │ └── styles.xml │ │ │ │ └── values │ │ │ │ └── styles.xml │ │ │ └── profile │ │ │ └── AndroidManifest.xml │ ├── build.gradle │ ├── gradle.properties │ ├── gradle │ │ └── wrapper │ │ │ └── gradle-wrapper.properties │ └── settings.gradle ├── config │ └── development.json ├── ios │ ├── .gitignore │ ├── Flutter │ │ ├── AppFrameworkInfo.plist │ │ ├── Debug.xcconfig │ │ └── Release.xcconfig │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ ├── contents.xcworkspacedata │ │ │ └── xcshareddata │ │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ │ └── WorkspaceSettings.xcsettings │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ ├── IDEWorkspaceChecks.plist │ │ │ └── WorkspaceSettings.xcsettings │ ├── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ ├── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── Icon-App-1024x1024@1x.png │ │ │ │ ├── Icon-App-20x20@1x.png │ │ │ │ ├── Icon-App-20x20@2x.png │ │ │ │ ├── Icon-App-20x20@3x.png │ │ │ │ ├── Icon-App-29x29@1x.png │ │ │ │ ├── Icon-App-29x29@2x.png │ │ │ │ ├── Icon-App-29x29@3x.png │ │ │ │ ├── Icon-App-40x40@1x.png │ │ │ │ ├── Icon-App-40x40@2x.png │ │ │ │ ├── Icon-App-40x40@3x.png │ │ │ │ ├── Icon-App-60x60@2x.png │ │ │ │ ├── Icon-App-60x60@3x.png │ │ │ │ ├── Icon-App-76x76@1x.png │ │ │ │ ├── Icon-App-76x76@2x.png │ │ │ │ └── Icon-App-83.5x83.5@2x.png │ │ │ └── LaunchImage.imageset │ │ │ │ ├── Contents.json │ │ │ │ ├── LaunchImage.png │ │ │ │ ├── LaunchImage@2x.png │ │ │ │ ├── LaunchImage@3x.png │ │ │ │ └── README.md │ │ ├── Base.lproj │ │ │ ├── LaunchScreen.storyboard │ │ │ └── Main.storyboard │ │ ├── Info.plist │ │ └── Runner-Bridging-Header.h │ └── RunnerTests │ │ └── RunnerTests.swift ├── lib │ └── main.dart ├── linux │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter │ │ ├── CMakeLists.txt │ │ ├── generated_plugin_registrant.cc │ │ ├── generated_plugin_registrant.h │ │ └── generated_plugins.cmake │ ├── main.cc │ ├── my_application.cc │ └── my_application.h ├── macos │ ├── .gitignore │ ├── Flutter │ │ ├── Flutter-Debug.xcconfig │ │ ├── Flutter-Release.xcconfig │ │ └── GeneratedPluginRegistrant.swift │ ├── Podfile │ ├── Podfile.lock │ ├── Runner.xcodeproj │ │ ├── project.pbxproj │ │ ├── project.xcworkspace │ │ │ └── xcshareddata │ │ │ │ └── IDEWorkspaceChecks.plist │ │ └── xcshareddata │ │ │ └── xcschemes │ │ │ └── Runner.xcscheme │ ├── Runner.xcworkspace │ │ ├── contents.xcworkspacedata │ │ └── xcshareddata │ │ │ └── IDEWorkspaceChecks.plist │ ├── Runner │ │ ├── AppDelegate.swift │ │ ├── Assets.xcassets │ │ │ └── AppIcon.appiconset │ │ │ │ ├── Contents.json │ │ │ │ ├── app_icon_1024.png │ │ │ │ ├── app_icon_128.png │ │ │ │ ├── app_icon_16.png │ │ │ │ ├── app_icon_256.png │ │ │ │ ├── app_icon_32.png │ │ │ │ ├── app_icon_512.png │ │ │ │ └── app_icon_64.png │ │ ├── Base.lproj │ │ │ └── MainMenu.xib │ │ ├── Configs │ │ │ ├── AppInfo.xcconfig │ │ │ ├── Debug.xcconfig │ │ │ ├── Release.xcconfig │ │ │ └── Warnings.xcconfig │ │ ├── DebugProfile.entitlements │ │ ├── Info.plist │ │ ├── MainFlutterWindow.swift │ │ └── Release.entitlements │ └── RunnerTests │ │ └── RunnerTests.swift ├── pubspec.lock ├── pubspec.yaml ├── test │ └── widget_test.dart ├── web │ ├── favicon.png │ ├── icons │ │ ├── Icon-192.png │ │ ├── Icon-512.png │ │ ├── Icon-maskable-192.png │ │ └── Icon-maskable-512.png │ ├── index.html │ └── manifest.json └── windows │ ├── .gitignore │ ├── CMakeLists.txt │ ├── flutter │ ├── CMakeLists.txt │ ├── generated_plugin_registrant.cc │ ├── generated_plugin_registrant.h │ └── generated_plugins.cmake │ └── runner │ ├── CMakeLists.txt │ ├── Runner.rc │ ├── flutter_window.cpp │ ├── flutter_window.h │ ├── main.cpp │ ├── resource.h │ ├── resources │ └── app_icon.ico │ ├── runner.exe.manifest │ ├── utils.cpp │ ├── utils.h │ ├── win32_window.cpp │ └── win32_window.h ├── lib ├── control.dart └── src │ ├── concurrency │ ├── concurrency.dart │ ├── concurrent_controller_handler.dart │ ├── droppable_controller_handler.dart │ └── sequential_controller_handler.dart │ ├── controller.dart │ ├── controller_scope.dart │ ├── extensions.dart │ ├── handler_context.dart │ ├── registry.dart │ ├── state_consumer.dart │ └── state_controller.dart ├── pubspec.yaml └── test ├── control_test.dart ├── unit ├── handler_context_test.dart └── state_controller_test.dart ├── util └── test_util.dart └── widget ├── controller_scope_test.dart └── state_consumer_test.dart /.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "stable", 3 | "flavors": {} 4 | } -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | #github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | patreon: plugfox 5 | #open_collective: # Replace with a single Open Collective username 6 | #ko_fi: # Replace with a single Ko-fi username 7 | #tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | #community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | #liberapay: # Replace with a single Liberapay username 10 | #issuehunt: # Replace with a single IssueHunt username 11 | #otechie: # Replace with a single Otechie username 12 | #lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: ['https://www.buymeacoffee.com/plugfox', 'https://boosty.to/plugfox'] 14 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Describe the bug** 10 | A clear and concise description of what the bug is. 11 | 12 | **To Reproduce** 13 | Steps to reproduce the behavior: 14 | 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | 28 | - OS: [e.g. iOS] 29 | - Browser [e.g. chrome, safari] 30 | - Version [e.g. 22] 31 | 32 | **Smartphone (please complete the following information):** 33 | 34 | - Device: [e.g. iPhone6] 35 | - OS: [e.g. iOS8.1] 36 | - Browser [e.g. stock browser, safari] 37 | - Version [e.g. 22] 38 | 39 | **Additional context** 40 | Add any other context about the problem here. 41 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: "" 5 | labels: "" 6 | assignees: "" 7 | --- 8 | 9 | **Is your feature request related to a problem? Please describe.** 10 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 11 | 12 | **Describe the solution you'd like** 13 | A clear and concise description of what you want to happen. 14 | 15 | **Describe alternatives you've considered** 16 | A clear and concise description of any alternative solutions or features you've considered. 17 | 18 | **Additional context** 19 | Add any other context or screenshots about the feature request here. 20 | -------------------------------------------------------------------------------- /.github/actions/setup/action.yaml: -------------------------------------------------------------------------------- 1 | name: Setup 2 | description: Sets up the Flutter environment 3 | 4 | inputs: 5 | flutter-version: 6 | description: 'The version of Flutter to use' 7 | required: false 8 | default: '3.24.3' 9 | pub-cache: 10 | description: 'The name of the pub cache variable' 11 | required: false 12 | default: control 13 | 14 | runs: 15 | using: composite 16 | steps: 17 | - name: 📦 Checkout the repo 18 | uses: actions/checkout@v4 19 | 20 | - name: 🔢 Set up version from tags 21 | id: set-version 22 | if: startsWith(github.ref, 'refs/tags') 23 | shell: bash 24 | run: | 25 | BASE_VERSION="${GITHUB_REF#refs/tags/v}" 26 | UNIXTIME=$(date +%s) 27 | VERSION="${BASE_VERSION}+${UNIXTIME}" 28 | echo "VERSION=$VERSION" >> $GITHUB_ENV 29 | sed -i "s/^version: .*/version: ${VERSION}/" pubspec.yaml 30 | echo "Version set to $VERSION" 31 | 32 | - name: 🚂 Setup Flutter 33 | uses: subosito/flutter-action@v2 34 | with: 35 | flutter-version: '${{ inputs.flutter-version }}' 36 | channel: "stable" 37 | 38 | - name: 📤 Restore Pub modules 39 | id: cache-pub-restore 40 | uses: actions/cache/restore@v4 41 | with: 42 | path: | 43 | /home/runner/.pub-cache 44 | key: ${{ runner.os }}-pub-${{ inputs.pub-cache }}-${{ hashFiles('pubspec.lock') }} 45 | 46 | - name: 👷 Install Dependencies 47 | shell: bash 48 | run: | 49 | echo /home/runner/.pub-cache/bin >> $GITHUB_PATH 50 | flutter config --no-cli-animations --no-analytics 51 | flutter pub get 52 | 53 | #- name: ⏲️ Run build runner 54 | # shell: bash 55 | # run: | 56 | # dart run build_runner build --delete-conflicting-outputs --release 57 | 58 | - name: 📥 Save Pub modules 59 | id: cache-pub-save 60 | if: steps.cache-pub-restore.outputs.cache-hit != 'true' 61 | uses: actions/cache/save@v4 62 | with: 63 | path: | 64 | /home/runner/.pub-cache 65 | key: ${{ steps.cache-pub-restore.outputs.cache-primary-key }} 66 | -------------------------------------------------------------------------------- /.github/workflows/checkout.yml: -------------------------------------------------------------------------------- 1 | name: Checkout 2 | 3 | on: 4 | workflow_dispatch: 5 | push: 6 | branches: 7 | - "master" 8 | pull_request: 9 | branches: 10 | - "master" 11 | - "develop" 12 | - "feature/**" 13 | - "bugfix/**" 14 | - "hotfix/**" 15 | - "support/**" 16 | paths: 17 | - "pubspec.yaml" 18 | - "pubspec.lock" 19 | - "lib/**.dart" 20 | - "test/**.dart" 21 | - "example/**.dart" 22 | 23 | permissions: 24 | contents: read 25 | actions: read 26 | checks: write 27 | 28 | jobs: 29 | checkout: 30 | name: "🧪 Check code with analysis, format, and tests" 31 | runs-on: ubuntu-latest 32 | timeout-minutes: 10 33 | defaults: 34 | run: 35 | working-directory: ./ 36 | steps: 37 | - name: 📦 Get the .github actions 38 | uses: actions/checkout@v4 39 | with: 40 | sparse-checkout: | 41 | .github 42 | 43 | - name: 🚂 Setup Flutter and dependencies 44 | uses: ./.github/actions/setup 45 | with: 46 | flutter-version: 3.24.3 47 | 48 | - name: 👷 Install Dependencies 49 | timeout-minutes: 1 50 | run: | 51 | flutter pub get 52 | 53 | - name: 🚦 Check code format 54 | id: check-format 55 | timeout-minutes: 1 56 | run: | 57 | find lib test -name "*.dart" ! -name "*.*.dart" -print0 | xargs -0 dart format --set-exit-if-changed --line-length 80 -o none 58 | 59 | - name: 📈 Check for Warnings 60 | id: check-analyzer 61 | timeout-minutes: 1 62 | run: | 63 | flutter analyze --fatal-infos --fatal-warnings lib/ test/ 64 | 65 | - name: 🧪 Unit & Widget tests 66 | timeout-minutes: 2 67 | run: | 68 | flutter test -r github --concurrency=6 --coverage test/control_test.dart 69 | 70 | - name: 📥 Upload coverage to Codecov 71 | timeout-minutes: 1 72 | uses: codecov/codecov-action@v3 73 | with: 74 | files: ./coverage/lcov.info 75 | # token: ${{ secrets.CODECOV_TOKEN }} # not required for public repos 76 | 77 | - name: 📥 Upload test report 78 | uses: actions/upload-artifact@v4 79 | if: (success() || failure()) && ${{ github.actor != 'dependabot[bot]' }} 80 | with: 81 | name: test-results 82 | path: reports/tests.json 83 | -------------------------------------------------------------------------------- /.github/workflows/test-report.yml: -------------------------------------------------------------------------------- 1 | name: "Test Report" 2 | 3 | on: 4 | workflow_run: 5 | workflows: ["Checkout"] # runs after "Checkout" workflow 6 | types: 7 | - completed 8 | 9 | permissions: 10 | contents: read 11 | actions: read 12 | checks: write 13 | 14 | jobs: 15 | report: 16 | name: "🚛 Test report" 17 | runs-on: ubuntu-latest 18 | timeout-minutes: 10 19 | steps: 20 | - name: Test report 21 | uses: dorny/test-reporter@v1 22 | with: 23 | artifact: test-results 24 | name: Test Report 25 | path: "**/tests.json" 26 | reporter: flutter-json 27 | fail-on-error: false 28 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | .dart_tool 28 | build/ 29 | **/doc/api/ 30 | **/ios/Flutter/.last_build_id 31 | .flutter-plugins 32 | .flutter-plugins-dependencies 33 | .packages 34 | .pub-cache/ 35 | 36 | # Pana 37 | log.pana.json 38 | 39 | # Test 40 | .coverage/ 41 | coverage/ 42 | /test/**/*.json 43 | /test/.test_coverage.dart 44 | 45 | # Temp 46 | /tmp 47 | /temp 48 | 49 | # FVM 50 | .fvm/flutter_sdk 51 | 52 | # Generated files 53 | *.*.dart -------------------------------------------------------------------------------- /.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "2e9cb0aa71a386a91f73f7088d115c0d96654829" 8 | channel: "stable" 9 | 10 | project_type: package 11 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "Dart-Code.dart-code", 4 | "Dart-Code.flutter" 5 | ] 6 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "[debug] Run app (development)", 6 | "type": "dart", 7 | "request": "launch", 8 | "cwd": "${workspaceFolder}/example", 9 | "args": [ 10 | "--dart-define-from-file=config/development.json", 11 | "--dart-define=octopus.logs=true", 12 | "--dart-define=octopus.measure=false" 13 | ], 14 | "env": {} 15 | }, 16 | { 17 | "name": "Flutter Test (VM)", 18 | "request": "launch", 19 | "type": "dart", 20 | "program": "test/control_test.dart", 21 | "env": { 22 | "ENVIRONMENT": "test" 23 | }, 24 | "console": "debugConsole", 25 | "runTestsOnDevice": false, 26 | "templateFor": "test", 27 | "toolArgs": [ 28 | "--color", 29 | "--reporter=expanded", 30 | "--file-reporter=json:.coverage/tests.json", 31 | "--timeout=30s", 32 | "--concurrency=12" 33 | /* "--name=handles failed connection attempts" */ 34 | ], 35 | "args": [] 36 | } 37 | ] 38 | } -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "[dart]": { 3 | "editor.insertSpaces": true, 4 | "editor.tabSize": 2, 5 | "editor.suggest.snippetsPreventQuickSuggestions": false, 6 | "editor.suggestSelection": "first", 7 | "editor.tabCompletion": "onlySnippets", 8 | "editor.wordBasedSuggestions": "off", 9 | "editor.selectionHighlight": false, 10 | "editor.defaultFormatter": "Dart-Code.dart-code", 11 | "editor.formatOnSave": true, 12 | "editor.formatOnType": true, 13 | "editor.formatOnPaste": true, 14 | "editor.codeActionsOnSave": { 15 | "source.fixAll": "explicit", 16 | "source.organizeImports": "explicit" 17 | }, 18 | "editor.quickSuggestions": { 19 | "comments": "on", 20 | "strings": "on", 21 | "other": "on" 22 | }, 23 | "editor.links": true, 24 | "editor.rulers": [ 25 | 80 26 | ] 27 | }, 28 | "dart.lineLength": 80, 29 | "dart.doNotFormat": [ 30 | "**.g.dart", 31 | "**.gql.dart", 32 | "**.freezed.dart", 33 | "**.config.dart", 34 | "**.mocks.dart", 35 | "**.gen.dart", 36 | "**.pb.dart", 37 | "**.pbenum.dart", 38 | "**.pbjson.dart", 39 | "**/generated/**" 40 | ], 41 | // Flutter Version Manager 42 | //"dart.flutterSdkPath": ".fvm/flutter_sdk", 43 | // Remove .fvm files from search 44 | "search.exclude": { 45 | //"**/.fvm": true, 46 | ".dart_tool": true, 47 | "coverage": true, 48 | "build": true 49 | }, 50 | // Remove from file watching 51 | "files.watcherExclude": { 52 | //"**/.fvm": true, 53 | ".dart_tool": true, 54 | "coverage": true, 55 | "build": true 56 | }, 57 | // Causes the debug view to automatically appear when a breakpoint is hit. This 58 | // setting is global and not configurable per-language. 59 | "debug.openDebug": "openOnDebugBreak", 60 | "explorer.fileNesting.enabled": true, 61 | "explorer.fileNesting.expand": false, 62 | "explorer.fileNesting.patterns": { 63 | "pubspec.yaml": ".flutter-plugins, .packages, .dart_tool, .flutter-plugins-dependencies, .metadata, .packages, pubspec.lock, build.yaml, analysis_options.yaml, all_lint_rules.yaml, dart*.yaml, flutter*.yaml, icons_launcher.yaml", 64 | ".gitignore": ".gitattributes, .gitmodules, .gitmessage, .mailmap, .git-blame*", 65 | "readme.*": "authors, backers.md, changelog*, citation*, code_of_conduct.md, codeowners, contributing.md, contributors, copying, credits, governance.md, history.md, license*, maintainers, readme*, security.md, sponsors.md", 66 | "*.dart": "$(capture).g.dart, $(capture).freezed.dart, $(capture).config.dart" 67 | }, 68 | /* "files.associations": { 69 | "*.drift": "sql" 70 | }, */ 71 | "highlight.regexes": { 72 | "(\"@\\s*.+\":\\s{0,1}{},)": { 73 | "filterFileRegex": ".*\\.arb", 74 | "decorations": [ 75 | { 76 | "overviewRulerColor": "#d19a66", 77 | "backgroundColor": "#d19a66", 78 | "color": "#282c34", 79 | "fontWeight": "bold" 80 | } 81 | ] 82 | } 83 | } 84 | } -------------------------------------------------------------------------------- /.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "label": "flutter:pub:get", 6 | "detail": "Get dependencies for the project", 7 | "icon": { 8 | "color": "terminal.ansiGreen", 9 | "id": "cloud-download" 10 | }, 11 | "dependsOn": [], 12 | "type": "shell", 13 | "command": [ 14 | "flutter pub get" 15 | ], 16 | "args": [], 17 | "group": { 18 | "kind": "none", 19 | "isDefault": true 20 | }, 21 | "problemMatcher": [], 22 | "options": { 23 | "cwd": "${workspaceFolder}" 24 | }, 25 | "isBackground": false, 26 | "presentation": { 27 | "reveal": "silent", 28 | "focus": false, 29 | "panel": "shared", 30 | "showReuseMessage": false, 31 | "clear": false, 32 | "group": "dart" 33 | } 34 | }, 35 | { 36 | "label": "dart:build_runner:all", 37 | "detail": "Generate code for the project", 38 | "icon": { 39 | "color": "terminal.ansiGreen", 40 | "id": "code" 41 | }, 42 | "type": "shell", 43 | "command": [ 44 | "dart run build_runner build --delete-conflicting-outputs", 45 | "&& dart format --fix -l 80 lib test" 46 | ], 47 | "dependsOn": [ 48 | "flutter:pub:get" 49 | ], 50 | "args": [], 51 | "group": { 52 | "kind": "none", 53 | "isDefault": true 54 | }, 55 | "problemMatcher": [], 56 | "options": { 57 | "cwd": "${workspaceFolder}" 58 | }, 59 | "isBackground": false, 60 | "presentation": { 61 | "reveal": "silent", 62 | "focus": false, 63 | "panel": "shared", 64 | "showReuseMessage": false, 65 | "clear": false, 66 | "group": "dart" 67 | } 68 | }, 69 | { 70 | "label": "dart:build_runner:dir", 71 | "detail": "Generate code for the directory", 72 | "type": "shell", 73 | "icon": { 74 | "color": "terminal.ansiGreen", 75 | "id": "code" 76 | }, 77 | "command": [ 78 | "dart run build_runner build --build-filter '${fileDirname}/*.dart'", 79 | "&& dart format --fix -l 80 '${fileDirname}'" 80 | ], 81 | "group": { 82 | "kind": "none", 83 | "isDefault": true 84 | }, 85 | "problemMatcher": [], 86 | "dependsOn": [ 87 | "flutter:pub:get" 88 | ], 89 | "isBackground": false, 90 | "presentation": { 91 | "reveal": "silent", 92 | "focus": false, 93 | "panel": "shared", 94 | "showReuseMessage": false, 95 | "clear": false, 96 | "group": "dart" 97 | } 98 | }, 99 | { 100 | "label": "dart:build_runner:watch", 101 | "detail": "Watch for changes in the project", 102 | "type": "shell", 103 | "icon": { 104 | "color": "terminal.ansiGreen", 105 | "id": "code" 106 | }, 107 | "command": [ 108 | "dart run build_runner watch --build-filter \"${input:directory}/**/*.dart\"" 109 | ], 110 | "group": { 111 | "kind": "none", 112 | "isDefault": true 113 | }, 114 | "problemMatcher": [], 115 | "dependsOn": [ 116 | "flutter:pub:get" 117 | ], 118 | "isBackground": false, 119 | "presentation": { 120 | "reveal": "silent", 121 | "focus": false, 122 | "panel": "shared", 123 | "showReuseMessage": false, 124 | "clear": false, 125 | "group": "dart" 126 | } 127 | }, 128 | { 129 | "label": "dart:format", 130 | "detail": "Format all files in the project", 131 | "icon": { 132 | "color": "terminal.ansiGreen", 133 | "id": "lightbulb-autofix" 134 | }, 135 | "type": "shell", 136 | "command": [ 137 | "dart format --fix -l 80 lib test" 138 | ], 139 | "dependsOn": [], 140 | "args": [], 141 | "group": { 142 | "kind": "none", 143 | "isDefault": true 144 | }, 145 | "problemMatcher": [], 146 | "options": { 147 | "cwd": "${workspaceFolder}" 148 | }, 149 | "isBackground": false, 150 | "presentation": { 151 | "reveal": "silent", 152 | "focus": false, 153 | "panel": "shared", 154 | "showReuseMessage": false, 155 | "clear": false, 156 | "group": "dart" 157 | } 158 | }, 159 | { 160 | "label": "flutter:test:all", 161 | "detail": "Run all tests", 162 | "icon": { 163 | "color": "terminal.ansiGreen", 164 | "id": "bug", 165 | }, 166 | "dependsOn": [ 167 | "flutter:pub:get" 168 | ], 169 | "type": "shell", 170 | "command": [ 171 | "flutter test --color --coverage --concurrency=50 --platform=tester --reporter=expanded --timeout=30s test/unit_test.dart test/widget_test.dart" 172 | ], 173 | "args": [], 174 | "group": { 175 | "kind": "test", 176 | "isDefault": true 177 | }, 178 | "problemMatcher": [], 179 | "options": { 180 | "cwd": "${workspaceFolder}" 181 | }, 182 | "isBackground": false, 183 | "presentation": { 184 | "reveal": "always", 185 | "focus": false, 186 | "panel": "shared", 187 | "showReuseMessage": false, 188 | "clear": false, 189 | "group": "dart" 190 | } 191 | }, 192 | { 193 | "label": "flutter:test:feature", 194 | "detail": "Run tests for a specific feature", 195 | "icon": { 196 | "color": "terminal.ansiGreen", 197 | "id": "bug", 198 | }, 199 | "dependsOn": [ 200 | "flutter:pub:get" 201 | ], 202 | "type": "shell", 203 | "command": [ 204 | "flutter test --color --coverage --concurrency=50 --platform=tester --reporter=expanded --timeout=30s --tags=\"${input:featureName}\" test/unit_test.dart test/widget_test.dart" 205 | ], 206 | "args": [], 207 | "group": { 208 | "kind": "test", 209 | "isDefault": true 210 | }, 211 | "problemMatcher": [], 212 | "options": { 213 | "cwd": "${workspaceFolder}" 214 | }, 215 | "isBackground": false, 216 | "presentation": { 217 | "reveal": "always", 218 | "focus": false, 219 | "panel": "shared", 220 | "showReuseMessage": false, 221 | "clear": false, 222 | "group": "dart" 223 | } 224 | } 225 | ], 226 | "inputs": [ 227 | { 228 | "id": "featureName", 229 | "type": "promptString", 230 | "description": "Enter the feature name (e.g., 'my_feature_name'):", 231 | /* "default": "my_feature_name" */ 232 | } 233 | ] 234 | } -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.2.0 2 | 3 | - **ADDED**: `HandlerContext` to handlers, available at zone and observer. 4 | - **ADDED**: `name` getter for `Controller` 5 | - **ADDED**: `void onHandler(HandlerContext context)` to `IControllerObserver` 6 | - **REMOVED**: `done` getter from `Controller` 7 | 8 | ## 0.1.0 9 | 10 | - **BREAKING CHANGE**: Replace FutureOr with Future in handler 11 | 12 | ## 0.0.3-pre.1 13 | 14 | - **BREAKING CHANGE**: Change `handler` api and implementation 15 | 16 | ## 0.0.2 17 | 18 | - Add example to README.md 19 | - ControllerRegistry only for debug mode 20 | 21 | ## 0.0.1-pre.0 22 | 23 | - Initial release 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Matiunin Mikhail 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | SHELL :=/bin/bash -e -o pipefail 2 | PWD := $(shell pwd) 3 | 4 | .DEFAULT_GOAL := all 5 | .PHONY: all 6 | all: ## build pipeline 7 | all: format check test 8 | 9 | .PHONY: ci 10 | ci: ## CI build pipeline 11 | ci: all 12 | 13 | .PHONY: precommit 14 | precommit: ## validate the branch before commit 15 | precommit: all 16 | 17 | .PHONY: help 18 | help: 19 | @echo 'Usage: make ... ' 20 | @echo '' 21 | @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 22 | 23 | .PHONY: version 24 | version: ## Check flutter version 25 | @flutter --version 26 | 27 | .PHONY: doctor 28 | doctor: ## Check flutter doctor 29 | @flutter doctor 30 | 31 | .PHONY: format 32 | format: ## Format the code 33 | @dart format -l 80 --fix lib/ test/ 34 | 35 | .PHONY: fmt 36 | fmt: format 37 | 38 | .PHONY: fix 39 | fix: format ## Fix the code 40 | @dart fix --apply lib 41 | @dart fix --apply test 42 | 43 | .PHONY: get 44 | get: ## Get the dependencies 45 | @flutter pub get 46 | 47 | .PHONY: upgrade 48 | upgrade: get ## Upgrade dependencies 49 | @flutter pub upgrade 50 | 51 | .PHONY: upgrade-major 52 | upgrade-major: get ## Upgrade to major versions 53 | @flutter pub upgrade --major-versions 54 | 55 | .PHONY: outdated 56 | outdated: get ## Check for outdated dependencies 57 | @flutter pub outdated --show-all --dev-dependencies --dependency-overrides --transitive --no-prereleases 58 | 59 | .PHONY: dependencies 60 | dependencies: get ## Check outdated dependencies 61 | @flutter pub outdated --dependency-overrides \ 62 | --dev-dependencies --prereleases --show-all --transitive 63 | 64 | .PHONY: test 65 | test: get ## Run the tests 66 | @flutter test --coverage --concurrency=6 test/control_test.dart 67 | 68 | .PHONY: publish-check 69 | publish-check: ## Check the package before publishing 70 | @flutter pub publish --dry-run 71 | 72 | .PHONY: publish 73 | publish: ## Publish the package 74 | @flutter pub publish 75 | 76 | .PHONY: analyze 77 | analyze: get ## Analyze the code 78 | @dart format --set-exit-if-changed -l 80 -o none lib/ test/ 79 | @flutter analyze --fatal-infos --fatal-warnings lib/ test/ 80 | 81 | .PHONY: check 82 | check: analyze publish-check ## Check the code 83 | # @flutter pub global activate pana 84 | # @pana --json --no-warning --line-length 80 > log.pana.json 85 | 86 | .PHONY: clean 87 | clean: ## Clean the project and remove all generated files 88 | @rm -rf dist bin out build 89 | @rm -rf coverage.* coverage .dart_tool .packages pubspec.lock 90 | 91 | .PHONY: diff 92 | diff: ## git diff 93 | $(call print-target) 94 | @git diff --exit-code 95 | @RES=$$(git status --porcelain) ; if [ -n "$$RES" ]; then echo $$RES && exit 1 ; fi 96 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Control: State Management for Flutter 2 | 3 | [![Pub](https://img.shields.io/pub/v/control.svg)](https://pub.dev/packages/control) 4 | [![Actions Status](https://github.com/PlugFox/control/actions/workflows/checkout.yml/badge.svg)](https://github.com/PlugFox/control/actions) 5 | [![Coverage](https://codecov.io/gh/PlugFox/control/branch/master/graph/badge.svg)](https://codecov.io/gh/PlugFox/control) 6 | [![License: MIT](https://img.shields.io/badge/license-MIT-purple.svg)](https://opensource.org/licenses/MIT) 7 | [![Linter](https://img.shields.io/badge/style-linter-40c4ff.svg)](https://pub.dev/packages/linter) 8 | [![GitHub stars](https://img.shields.io/github/stars/plugfox/control?style=social)](https://github.com/plugfox/control/) 9 | 10 | --- 11 | 12 | ## Installation 13 | 14 | Add the following dependency to your `pubspec.yaml` file: 15 | 16 | ```yaml 17 | dependencies: 18 | control: 19 | ``` 20 | 21 | ## Example 22 | 23 | ```dart 24 | /// Counter state for [CounterController] 25 | typedef CounterState = ({int count, bool idle}); 26 | 27 | /// Counter controller 28 | final class CounterController extends StateController 29 | with SequentialControllerHandler { 30 | CounterController({CounterState? initialState}) 31 | : super(initialState: initialState ?? (idle: true, count: 0)); 32 | 33 | void add(int value) => handle(() async { 34 | setState((idle: false, count: state.count)); 35 | await Future.delayed(const Duration(milliseconds: 1500)); 36 | setState((idle: true, count: state.count + value)); 37 | }); 38 | 39 | void subtract(int value) => handle(() async { 40 | setState((idle: false, count: state.count)); 41 | await Future.delayed(const Duration(milliseconds: 1500)); 42 | setState((idle: true, count: state.count - value)); 43 | }); 44 | } 45 | ``` 46 | 47 | ## Coverage 48 | 49 | [![](https://codecov.io/gh/PlugFox/control/branch/master/graphs/sunburst.svg)](https://codecov.io/gh/PlugFox/control/branch/master) 50 | 51 | ## Changelog 52 | 53 | Refer to the [Changelog](https://github.com/PlugFox/control/blob/master/CHANGELOG.md) to get all release notes. 54 | 55 | ## Maintainers 56 | 57 | - [Matiunin Mikhail aka Plague Fox](https://plugfox.dev) 58 | 59 | ## Funding 60 | 61 | If you want to support the development of our library, there are several ways you can do it: 62 | 63 | - [Buy me a coffee](https://www.buymeacoffee.com/plugfox) 64 | - [Support on Patreon](https://www.patreon.com/plugfox) 65 | - [Subscribe through Boosty](https://boosty.to/plugfox) 66 | 67 | We appreciate any form of support, whether it's a financial donation or just a star on GitHub. It helps us to continue developing and improving our library. Thank you for your support! 68 | 69 | ## License 70 | 71 | [MIT](https://opensource.org/licenses/MIT) 72 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | exclude: 5 | # Tests 6 | - "test/**.mocks.dart" 7 | - ".test_coverage.dart" 8 | # Assets 9 | - "assets/**" 10 | 11 | # Enable the following options to enable strong mode. 12 | language: 13 | strict-casts: true 14 | strict-raw-types: true 15 | strict-inference: true 16 | 17 | errors: 18 | # Allow having TODOs in the code 19 | todo: ignore 20 | 21 | # Info 22 | directives_ordering: info 23 | always_declare_return_types: info 24 | 25 | # Warning 26 | unsafe_html: warning 27 | missing_return: warning 28 | missing_required_param: warning 29 | no_logic_in_create_state: warning 30 | empty_catches: warning 31 | 32 | # Error 33 | always_use_package_imports: error 34 | avoid_relative_lib_imports: error 35 | avoid_slow_async_io: error 36 | avoid_types_as_parameter_names: error 37 | valid_regexps: error 38 | always_require_non_null_named_parameters: error 39 | 40 | linter: 41 | rules: 42 | # Public packages 43 | public_member_api_docs: true 44 | lines_longer_than_80_chars: true 45 | 46 | # Enabling rules 47 | always_use_package_imports: true 48 | avoid_relative_lib_imports: true 49 | 50 | # Disable rules 51 | sort_pub_dependencies: false 52 | prefer_relative_imports: false 53 | prefer_final_locals: false 54 | avoid_escaping_inner_quotes: false 55 | curly_braces_in_flow_control_structures: false 56 | 57 | # Enabled 58 | use_named_constants: true 59 | unnecessary_constructor_name: true 60 | sort_constructors_first: true 61 | exhaustive_cases: true 62 | sort_unnamed_constructors_first: true 63 | type_literal_in_constant_pattern: true 64 | always_put_required_named_parameters_first: true 65 | avoid_annotating_with_dynamic: true 66 | avoid_bool_literals_in_conditional_expressions: true 67 | avoid_double_and_int_checks: true 68 | avoid_field_initializers_in_const_classes: true 69 | avoid_implementing_value_types: true 70 | avoid_js_rounded_ints: true 71 | avoid_print: true 72 | avoid_renaming_method_parameters: true 73 | avoid_returning_null_for_void: true 74 | avoid_single_cascade_in_expression_statements: true 75 | avoid_slow_async_io: true 76 | avoid_unnecessary_containers: true 77 | avoid_unused_constructor_parameters: true 78 | avoid_void_async: true 79 | await_only_futures: true 80 | cancel_subscriptions: true 81 | cascade_invocations: true 82 | close_sinks: true 83 | control_flow_in_finally: true 84 | empty_statements: true 85 | collection_methods_unrelated_type: true 86 | join_return_with_assignment: true 87 | leading_newlines_in_multiline_strings: true 88 | literal_only_boolean_expressions: true 89 | missing_whitespace_between_adjacent_strings: true 90 | no_adjacent_strings_in_list: true 91 | no_logic_in_create_state: true 92 | no_runtimeType_toString: true 93 | only_throw_errors: true 94 | overridden_fields: true 95 | package_names: true 96 | package_prefixed_library_names: true 97 | parameter_assignments: true 98 | prefer_asserts_in_initializer_lists: true 99 | prefer_asserts_with_message: true 100 | prefer_const_constructors: true 101 | prefer_const_constructors_in_immutables: true 102 | prefer_const_declarations: true 103 | prefer_const_literals_to_create_immutables: true 104 | prefer_constructors_over_static_methods: true 105 | prefer_expression_function_bodies: true 106 | prefer_final_in_for_each: true 107 | prefer_foreach: true 108 | prefer_if_elements_to_conditional_expressions: true 109 | prefer_inlined_adds: true 110 | prefer_int_literals: true 111 | prefer_is_not_operator: true 112 | prefer_null_aware_operators: true 113 | prefer_typing_uninitialized_variables: true 114 | prefer_void_to_null: true 115 | provide_deprecation_message: true 116 | sized_box_for_whitespace: true 117 | sort_child_properties_last: true 118 | test_types_in_equals: true 119 | throw_in_finally: true 120 | unnecessary_null_aware_assignments: true 121 | unnecessary_overrides: true 122 | unnecessary_parenthesis: true 123 | unnecessary_raw_strings: true 124 | unnecessary_statements: true 125 | unnecessary_string_escapes: true 126 | unnecessary_string_interpolations: true 127 | unsafe_html: true 128 | use_full_hex_values_for_flutter_colors: true 129 | use_raw_strings: true 130 | use_string_buffers: true 131 | valid_regexps: true 132 | void_checks: true 133 | 134 | # Pedantic 1.9.0 135 | always_declare_return_types: true 136 | annotate_overrides: true 137 | avoid_empty_else: true 138 | avoid_init_to_null: true 139 | avoid_null_checks_in_equality_operators: true 140 | avoid_return_types_on_setters: true 141 | avoid_shadowing_type_parameters: true 142 | avoid_types_as_parameter_names: true 143 | camel_case_extensions: true 144 | empty_catches: true 145 | empty_constructor_bodies: true 146 | library_names: true 147 | library_prefixes: true 148 | no_duplicate_case_values: true 149 | null_closures: true 150 | omit_local_variable_types: true 151 | prefer_adjacent_string_concatenation: true 152 | prefer_collection_literals: true 153 | prefer_conditional_assignment: true 154 | prefer_contains: true 155 | prefer_final_fields: true 156 | prefer_for_elements_to_map_fromIterable: true 157 | prefer_generic_function_type_aliases: true 158 | prefer_if_null_operators: true 159 | prefer_is_empty: true 160 | prefer_is_not_empty: true 161 | prefer_iterable_whereType: true 162 | prefer_single_quotes: true 163 | prefer_spread_collections: true 164 | recursive_getters: true 165 | slash_for_doc_comments: true 166 | type_init_formals: true 167 | unawaited_futures: true 168 | unnecessary_const: true 169 | unnecessary_new: true 170 | unnecessary_null_in_if_null_operators: true 171 | unnecessary_this: true 172 | unrelated_type_equality_checks: true 173 | use_function_type_syntax_for_parameters: true 174 | use_rethrow_when_possible: true 175 | 176 | # Effective_dart 1.2.0 177 | camel_case_types: true 178 | file_names: true 179 | non_constant_identifier_names: true 180 | constant_identifier_names: true 181 | directives_ordering: true 182 | package_api_docs: true 183 | implementation_imports: true 184 | prefer_interpolation_to_compose_strings: true 185 | unnecessary_brace_in_string_interps: true 186 | avoid_function_literals_in_foreach_calls: true 187 | prefer_function_declarations_over_variables: true 188 | unnecessary_lambdas: true 189 | unnecessary_getters_setters: true 190 | prefer_initializing_formals: true 191 | avoid_catches_without_on_clauses: true 192 | avoid_catching_errors: true 193 | use_to_and_as_if_applicable: true 194 | one_member_abstracts: true 195 | avoid_classes_with_only_static_members: true 196 | prefer_mixin: true 197 | use_setters_to_change_properties: true 198 | avoid_setters_without_getters: true 199 | avoid_returning_this: true 200 | type_annotate_public_apis: true 201 | avoid_types_on_closure_parameters: true 202 | avoid_private_typedef_functions: true 203 | avoid_positional_boolean_parameters: true 204 | hash_and_equals: true 205 | avoid_equals_and_hash_code_on_mutable_classes: true 206 | -------------------------------------------------------------------------------- /dart_test.yaml: -------------------------------------------------------------------------------- 1 | timeout: 1x 2 | 3 | platforms: 4 | - vm 5 | 6 | file_reporters: 7 | json: reports/tests.json 8 | 9 | tags: 10 | model: 11 | timeout: 1x 12 | data: 13 | timeout: 1x 14 | controller: 15 | timeout: 1x 16 | widget: 17 | timeout: 1x 18 | -------------------------------------------------------------------------------- /example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .build/ 9 | .buildlog/ 10 | .history 11 | .svn/ 12 | .swiftpm/ 13 | migrate_working_dir/ 14 | 15 | # IntelliJ related 16 | *.iml 17 | *.ipr 18 | *.iws 19 | .idea/ 20 | 21 | # The .vscode folder contains launch configuration and tasks you configure in 22 | # VS Code which you may wish to be included in version control, so this line 23 | # is commented out by default. 24 | #.vscode/ 25 | 26 | # Flutter/Dart/Pub related 27 | **/doc/api/ 28 | **/ios/Flutter/.last_build_id 29 | .dart_tool/ 30 | .flutter-plugins 31 | .flutter-plugins-dependencies 32 | .pub-cache/ 33 | .pub/ 34 | /build/ 35 | 36 | # Symbolication related 37 | app.*.symbols 38 | 39 | # Obfuscation related 40 | app.*.map.json 41 | 42 | # Android Studio will place build artifacts here 43 | /android/app/debug 44 | /android/app/profile 45 | /android/app/release 46 | -------------------------------------------------------------------------------- /example/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled and should not be manually edited. 5 | 6 | version: 7 | revision: "2e9cb0aa71a386a91f73f7088d115c0d96654829" 8 | channel: "stable" 9 | 10 | project_type: app 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 17 | base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 18 | - platform: android 19 | create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 20 | base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 21 | - platform: ios 22 | create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 23 | base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 24 | - platform: linux 25 | create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 26 | base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 27 | - platform: macos 28 | create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 29 | base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 30 | - platform: web 31 | create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 32 | base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 33 | - platform: windows 34 | create_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 35 | base_revision: 2e9cb0aa71a386a91f73f7088d115c0d96654829 36 | 37 | # User provided section 38 | 39 | # List of Local paths (relative to this file) that should be 40 | # ignored by the migrate tool. 41 | # 42 | # Files that are not part of the templates will be ignored by default. 43 | unmanaged_files: 44 | - 'lib/main.dart' 45 | - 'ios/Runner.xcodeproj/project.pbxproj' 46 | -------------------------------------------------------------------------------- /example/README.md: -------------------------------------------------------------------------------- 1 | # example 2 | 3 | A new Flutter project. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | include: package:flutter_lints/flutter.yaml 2 | 3 | analyzer: 4 | exclude: 5 | # Build 6 | - "build/**" 7 | # Tests 8 | - "test/**.mocks.dart" 9 | - ".test_coverage.dart" 10 | - "coverage/**" 11 | # Assets 12 | - "assets/**" 13 | # Generated 14 | - "lib/src/common/localization/generated/**" 15 | - "lib/src/common/constants/pubspec.yaml.g.dart" 16 | - "lib/src/common/model/generated/**" 17 | - "**.g.dart" 18 | - "**.gql.dart" 19 | - "**.freezed.dart" 20 | - "**.config.dart" 21 | - "**.mocks.dart" 22 | - "**.gen.dart" 23 | - "**.pb.dart" 24 | - "**.pbenum.dart" 25 | - "**.pbjson.dart" 26 | # Flutter Version Manager 27 | - ".fvm/**" 28 | # Tools 29 | #- "tool/**" 30 | - "scripts/**" 31 | - ".dart_tool/**" 32 | # Platform 33 | - "ios/**" 34 | - "android/**" 35 | - "web/**" 36 | - "macos/**" 37 | - "windows/**" 38 | - "linux/**" 39 | 40 | # Enable the following options to enable strong mode. 41 | language: 42 | strict-casts: true 43 | strict-raw-types: true 44 | strict-inference: true 45 | 46 | errors: 47 | # Allow having TODOs in the code 48 | todo: ignore 49 | 50 | # Info 51 | directives_ordering: info 52 | always_declare_return_types: info 53 | 54 | # Warning 55 | unsafe_html: warning 56 | missing_return: warning 57 | missing_required_param: warning 58 | no_logic_in_create_state: warning 59 | empty_catches: warning 60 | 61 | # Error 62 | always_use_package_imports: error 63 | avoid_relative_lib_imports: error 64 | avoid_slow_async_io: error 65 | avoid_types_as_parameter_names: error 66 | valid_regexps: error 67 | always_require_non_null_named_parameters: error 68 | 69 | linter: 70 | rules: 71 | # Public packages 72 | #public_member_api_docs: true 73 | #lines_longer_than_80_chars: true 74 | 75 | # Enabling rules 76 | always_use_package_imports: true 77 | avoid_relative_lib_imports: true 78 | 79 | # Disable rules 80 | sort_pub_dependencies: false 81 | prefer_relative_imports: false 82 | prefer_final_locals: false 83 | avoid_escaping_inner_quotes: false 84 | curly_braces_in_flow_control_structures: false 85 | 86 | # Enabled 87 | use_named_constants: true 88 | unnecessary_constructor_name: true 89 | sort_constructors_first: true 90 | exhaustive_cases: true 91 | sort_unnamed_constructors_first: true 92 | type_literal_in_constant_pattern: true 93 | always_put_required_named_parameters_first: true 94 | avoid_annotating_with_dynamic: true 95 | avoid_bool_literals_in_conditional_expressions: true 96 | avoid_double_and_int_checks: true 97 | avoid_field_initializers_in_const_classes: true 98 | avoid_implementing_value_types: true 99 | avoid_js_rounded_ints: true 100 | avoid_print: true 101 | avoid_renaming_method_parameters: true 102 | avoid_returning_null_for_void: true 103 | avoid_single_cascade_in_expression_statements: true 104 | avoid_slow_async_io: true 105 | avoid_unnecessary_containers: true 106 | avoid_unused_constructor_parameters: true 107 | avoid_void_async: true 108 | await_only_futures: true 109 | cancel_subscriptions: true 110 | cascade_invocations: true 111 | close_sinks: true 112 | control_flow_in_finally: true 113 | empty_statements: true 114 | collection_methods_unrelated_type: true 115 | join_return_with_assignment: true 116 | leading_newlines_in_multiline_strings: true 117 | literal_only_boolean_expressions: true 118 | missing_whitespace_between_adjacent_strings: true 119 | no_adjacent_strings_in_list: true 120 | no_logic_in_create_state: true 121 | no_runtimeType_toString: true 122 | only_throw_errors: true 123 | overridden_fields: true 124 | package_names: true 125 | package_prefixed_library_names: true 126 | parameter_assignments: true 127 | prefer_asserts_in_initializer_lists: true 128 | prefer_asserts_with_message: true 129 | prefer_const_constructors: true 130 | prefer_const_constructors_in_immutables: true 131 | prefer_const_declarations: true 132 | prefer_const_literals_to_create_immutables: true 133 | prefer_constructors_over_static_methods: true 134 | prefer_expression_function_bodies: true 135 | prefer_final_in_for_each: true 136 | prefer_foreach: true 137 | prefer_if_elements_to_conditional_expressions: true 138 | prefer_inlined_adds: true 139 | prefer_int_literals: true 140 | prefer_is_not_operator: true 141 | prefer_null_aware_operators: true 142 | prefer_typing_uninitialized_variables: true 143 | prefer_void_to_null: true 144 | provide_deprecation_message: true 145 | sized_box_for_whitespace: true 146 | sort_child_properties_last: true 147 | test_types_in_equals: true 148 | throw_in_finally: true 149 | unnecessary_null_aware_assignments: true 150 | unnecessary_overrides: true 151 | unnecessary_parenthesis: true 152 | unnecessary_raw_strings: true 153 | unnecessary_statements: true 154 | unnecessary_string_escapes: true 155 | unnecessary_string_interpolations: true 156 | unsafe_html: true 157 | use_full_hex_values_for_flutter_colors: true 158 | use_raw_strings: true 159 | use_string_buffers: true 160 | valid_regexps: true 161 | void_checks: true 162 | 163 | # Pedantic 1.9.0 164 | always_declare_return_types: true 165 | annotate_overrides: true 166 | avoid_empty_else: true 167 | avoid_init_to_null: true 168 | avoid_null_checks_in_equality_operators: true 169 | avoid_return_types_on_setters: true 170 | avoid_shadowing_type_parameters: true 171 | avoid_types_as_parameter_names: true 172 | camel_case_extensions: true 173 | empty_catches: true 174 | empty_constructor_bodies: true 175 | library_names: true 176 | library_prefixes: true 177 | no_duplicate_case_values: true 178 | null_closures: true 179 | omit_local_variable_types: true 180 | prefer_adjacent_string_concatenation: true 181 | prefer_collection_literals: true 182 | prefer_conditional_assignment: true 183 | prefer_contains: true 184 | prefer_final_fields: true 185 | prefer_for_elements_to_map_fromIterable: true 186 | prefer_generic_function_type_aliases: true 187 | prefer_if_null_operators: true 188 | prefer_is_empty: true 189 | prefer_is_not_empty: true 190 | prefer_iterable_whereType: true 191 | prefer_single_quotes: true 192 | prefer_spread_collections: true 193 | recursive_getters: true 194 | slash_for_doc_comments: true 195 | type_init_formals: true 196 | unawaited_futures: true 197 | unnecessary_const: true 198 | unnecessary_new: true 199 | unnecessary_null_in_if_null_operators: true 200 | unnecessary_this: true 201 | unrelated_type_equality_checks: true 202 | use_function_type_syntax_for_parameters: true 203 | use_rethrow_when_possible: true 204 | 205 | # Effective_dart 1.2.0 206 | camel_case_types: true 207 | file_names: true 208 | non_constant_identifier_names: true 209 | constant_identifier_names: true 210 | directives_ordering: true 211 | package_api_docs: true 212 | implementation_imports: true 213 | prefer_interpolation_to_compose_strings: true 214 | unnecessary_brace_in_string_interps: true 215 | avoid_function_literals_in_foreach_calls: true 216 | prefer_function_declarations_over_variables: true 217 | unnecessary_lambdas: true 218 | unnecessary_getters_setters: true 219 | prefer_initializing_formals: true 220 | avoid_catches_without_on_clauses: true 221 | avoid_catching_errors: true 222 | use_to_and_as_if_applicable: true 223 | one_member_abstracts: true 224 | avoid_classes_with_only_static_members: true 225 | prefer_mixin: true 226 | use_setters_to_change_properties: true 227 | avoid_setters_without_getters: true 228 | avoid_returning_this: true 229 | type_annotate_public_apis: true 230 | avoid_types_on_closure_parameters: true 231 | avoid_private_typedef_functions: true 232 | avoid_positional_boolean_parameters: true 233 | hash_and_equals: true 234 | avoid_equals_and_hash_code_on_mutable_classes: true 235 | -------------------------------------------------------------------------------- /example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id "com.android.application" 3 | id "kotlin-android" 4 | id "dev.flutter.flutter-gradle-plugin" 5 | } 6 | 7 | def localProperties = new Properties() 8 | def localPropertiesFile = rootProject.file('local.properties') 9 | if (localPropertiesFile.exists()) { 10 | localPropertiesFile.withReader('UTF-8') { reader -> 11 | localProperties.load(reader) 12 | } 13 | } 14 | 15 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 16 | if (flutterVersionCode == null) { 17 | flutterVersionCode = '1' 18 | } 19 | 20 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 21 | if (flutterVersionName == null) { 22 | flutterVersionName = '1.0' 23 | } 24 | 25 | android { 26 | namespace "com.example.example" 27 | compileSdkVersion flutter.compileSdkVersion 28 | ndkVersion flutter.ndkVersion 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | } 38 | 39 | sourceSets { 40 | main.java.srcDirs += 'src/main/kotlin' 41 | } 42 | 43 | defaultConfig { 44 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 45 | applicationId "com.example.example" 46 | // You can update the following values to match your application needs. 47 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-gradle-build-configuration. 48 | minSdkVersion flutter.minSdkVersion 49 | targetSdkVersion flutter.targetSdkVersion 50 | versionCode flutterVersionCode.toInteger() 51 | versionName flutterVersionName 52 | } 53 | 54 | buildTypes { 55 | release { 56 | // TODO: Add your own signing config for the release build. 57 | // Signing with the debug keys for now, so `flutter run --release` works. 58 | signingConfig signingConfigs.debug 59 | } 60 | } 61 | } 62 | 63 | flutter { 64 | source '../..' 65 | } 66 | 67 | dependencies {} 68 | -------------------------------------------------------------------------------- /example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 14 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 29 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /example/android/app/src/main/kotlin/com/example/example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package com.example.example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.7.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 10 | } 11 | } 12 | 13 | allprojects { 14 | repositories { 15 | google() 16 | mavenCentral() 17 | } 18 | } 19 | 20 | rootProject.buildDir = '../build' 21 | subprojects { 22 | project.buildDir = "${rootProject.buildDir}/${project.name}" 23 | } 24 | subprojects { 25 | project.evaluationDependsOn(':app') 26 | } 27 | 28 | tasks.register("clean", Delete) { 29 | delete rootProject.buildDir 30 | } 31 | -------------------------------------------------------------------------------- /example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx4G 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | zipStoreBase=GRADLE_USER_HOME 4 | zipStorePath=wrapper/dists 5 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-all.zip 6 | -------------------------------------------------------------------------------- /example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | def flutterSdkPath = { 3 | def properties = new Properties() 4 | file("local.properties").withInputStream { properties.load(it) } 5 | def flutterSdkPath = properties.getProperty("flutter.sdk") 6 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 7 | return flutterSdkPath 8 | } 9 | settings.ext.flutterSdkPath = flutterSdkPath() 10 | 11 | includeBuild("${settings.ext.flutterSdkPath}/packages/flutter_tools/gradle") 12 | 13 | repositories { 14 | google() 15 | mavenCentral() 16 | gradlePluginPortal() 17 | } 18 | 19 | plugins { 20 | id "dev.flutter.flutter-gradle-plugin" version "1.0.0" apply false 21 | } 22 | } 23 | 24 | plugins { 25 | id "dev.flutter.flutter-plugin-loader" version "1.0.0" 26 | id "com.android.application" version "7.3.0" apply false 27 | } 28 | 29 | include ":app" 30 | -------------------------------------------------------------------------------- /example/config/development.json: -------------------------------------------------------------------------------- 1 | { 2 | "enviroment": "development" 3 | } 4 | -------------------------------------------------------------------------------- /example/ios/.gitignore: -------------------------------------------------------------------------------- 1 | **/dgph 2 | *.mode1v3 3 | *.mode2v3 4 | *.moved-aside 5 | *.pbxuser 6 | *.perspectivev3 7 | **/*sync/ 8 | .sconsign.dblite 9 | .tags* 10 | **/.vagrant/ 11 | **/DerivedData/ 12 | Icon? 13 | **/Pods/ 14 | **/.symlinks/ 15 | profile 16 | xcuserdata 17 | **/.generated/ 18 | Flutter/App.framework 19 | Flutter/Flutter.framework 20 | Flutter/Flutter.podspec 21 | Flutter/Generated.xcconfig 22 | Flutter/ephemeral/ 23 | Flutter/app.flx 24 | Flutter/app.zip 25 | Flutter/flutter_assets/ 26 | Flutter/flutter_export_environment.sh 27 | ServiceDefinitions.json 28 | Runner/GeneratedPluginRegistrant.* 29 | 30 | # Exceptions to above rules. 31 | !default.mode1v3 32 | !default.mode2v3 33 | !default.pbxuser 34 | !default.perspectivev3 35 | -------------------------------------------------------------------------------- /example/ios/Flutter/AppFrameworkInfo.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | App 9 | CFBundleIdentifier 10 | io.flutter.flutter.app 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | App 15 | CFBundlePackageType 16 | FMWK 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1.0 23 | MinimumOSVersion 24 | 12.0 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Flutter/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Flutter/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/ios/Podfile: -------------------------------------------------------------------------------- 1 | # Uncomment this line to define a global platform for your project 2 | # platform :ios, '12.0' 3 | 4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 6 | 7 | project 'Runner', { 8 | 'Debug' => :debug, 9 | 'Profile' => :release, 10 | 'Release' => :release, 11 | } 12 | 13 | def flutter_root 14 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'Generated.xcconfig'), __FILE__) 15 | unless File.exist?(generated_xcode_build_settings_path) 16 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure flutter pub get is executed first" 17 | end 18 | 19 | File.foreach(generated_xcode_build_settings_path) do |line| 20 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 21 | return matches[1].strip if matches 22 | end 23 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Generated.xcconfig, then run flutter pub get" 24 | end 25 | 26 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 27 | 28 | flutter_ios_podfile_setup 29 | 30 | target 'Runner' do 31 | use_frameworks! 32 | use_modular_headers! 33 | 34 | flutter_install_all_ios_pods File.dirname(File.realpath(__FILE__)) 35 | target 'RunnerTests' do 36 | inherit! :search_paths 37 | end 38 | end 39 | 40 | post_install do |installer| 41 | installer.pods_project.targets.each do |target| 42 | flutter_additional_ios_build_settings(target) 43 | end 44 | end 45 | -------------------------------------------------------------------------------- /example/ios/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - Flutter (1.0.0) 3 | - integration_test (0.0.1): 4 | - Flutter 5 | - shared_preferences_foundation (0.0.1): 6 | - Flutter 7 | - FlutterMacOS 8 | 9 | DEPENDENCIES: 10 | - Flutter (from `Flutter`) 11 | - integration_test (from `.symlinks/plugins/integration_test/ios`) 12 | - shared_preferences_foundation (from `.symlinks/plugins/shared_preferences_foundation/darwin`) 13 | 14 | EXTERNAL SOURCES: 15 | Flutter: 16 | :path: Flutter 17 | integration_test: 18 | :path: ".symlinks/plugins/integration_test/ios" 19 | shared_preferences_foundation: 20 | :path: ".symlinks/plugins/shared_preferences_foundation/darwin" 21 | 22 | SPEC CHECKSUMS: 23 | Flutter: e0871f40cf51350855a761d2e70bf5af5b9b5de7 24 | integration_test: 252f60fa39af5e17c3aa9899d35d908a0721b573 25 | shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 26 | 27 | PODFILE CHECKSUM: 819463e6a0290f5a72f145ba7cde16e8b6ef0796 28 | 29 | COCOAPODS: 1.15.2 30 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 8 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PreviewsEnabled 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/ios/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import UIKit 2 | import Flutter 3 | 4 | @main 5 | @objc class AppDelegate: FlutterAppDelegate { 6 | override func application( 7 | _ application: UIApplication, 8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]? 9 | ) -> Bool { 10 | GeneratedPluginRegistrant.register(with: self) 11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "20x20", 5 | "idiom" : "iphone", 6 | "filename" : "Icon-App-20x20@2x.png", 7 | "scale" : "2x" 8 | }, 9 | { 10 | "size" : "20x20", 11 | "idiom" : "iphone", 12 | "filename" : "Icon-App-20x20@3x.png", 13 | "scale" : "3x" 14 | }, 15 | { 16 | "size" : "29x29", 17 | "idiom" : "iphone", 18 | "filename" : "Icon-App-29x29@1x.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "29x29", 23 | "idiom" : "iphone", 24 | "filename" : "Icon-App-29x29@2x.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "29x29", 29 | "idiom" : "iphone", 30 | "filename" : "Icon-App-29x29@3x.png", 31 | "scale" : "3x" 32 | }, 33 | { 34 | "size" : "40x40", 35 | "idiom" : "iphone", 36 | "filename" : "Icon-App-40x40@2x.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "40x40", 41 | "idiom" : "iphone", 42 | "filename" : "Icon-App-40x40@3x.png", 43 | "scale" : "3x" 44 | }, 45 | { 46 | "size" : "60x60", 47 | "idiom" : "iphone", 48 | "filename" : "Icon-App-60x60@2x.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "60x60", 53 | "idiom" : "iphone", 54 | "filename" : "Icon-App-60x60@3x.png", 55 | "scale" : "3x" 56 | }, 57 | { 58 | "size" : "20x20", 59 | "idiom" : "ipad", 60 | "filename" : "Icon-App-20x20@1x.png", 61 | "scale" : "1x" 62 | }, 63 | { 64 | "size" : "20x20", 65 | "idiom" : "ipad", 66 | "filename" : "Icon-App-20x20@2x.png", 67 | "scale" : "2x" 68 | }, 69 | { 70 | "size" : "29x29", 71 | "idiom" : "ipad", 72 | "filename" : "Icon-App-29x29@1x.png", 73 | "scale" : "1x" 74 | }, 75 | { 76 | "size" : "29x29", 77 | "idiom" : "ipad", 78 | "filename" : "Icon-App-29x29@2x.png", 79 | "scale" : "2x" 80 | }, 81 | { 82 | "size" : "40x40", 83 | "idiom" : "ipad", 84 | "filename" : "Icon-App-40x40@1x.png", 85 | "scale" : "1x" 86 | }, 87 | { 88 | "size" : "40x40", 89 | "idiom" : "ipad", 90 | "filename" : "Icon-App-40x40@2x.png", 91 | "scale" : "2x" 92 | }, 93 | { 94 | "size" : "76x76", 95 | "idiom" : "ipad", 96 | "filename" : "Icon-App-76x76@1x.png", 97 | "scale" : "1x" 98 | }, 99 | { 100 | "size" : "76x76", 101 | "idiom" : "ipad", 102 | "filename" : "Icon-App-76x76@2x.png", 103 | "scale" : "2x" 104 | }, 105 | { 106 | "size" : "83.5x83.5", 107 | "idiom" : "ipad", 108 | "filename" : "Icon-App-83.5x83.5@2x.png", 109 | "scale" : "2x" 110 | }, 111 | { 112 | "size" : "1024x1024", 113 | "idiom" : "ios-marketing", 114 | "filename" : "Icon-App-1024x1024@1x.png", 115 | "scale" : "1x" 116 | } 117 | ], 118 | "info" : { 119 | "version" : 1, 120 | "author" : "xcode" 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "universal", 5 | "filename" : "LaunchImage.png", 6 | "scale" : "1x" 7 | }, 8 | { 9 | "idiom" : "universal", 10 | "filename" : "LaunchImage@2x.png", 11 | "scale" : "2x" 12 | }, 13 | { 14 | "idiom" : "universal", 15 | "filename" : "LaunchImage@3x.png", 16 | "scale" : "3x" 17 | } 18 | ], 19 | "info" : { 20 | "version" : 1, 21 | "author" : "xcode" 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png -------------------------------------------------------------------------------- /example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md: -------------------------------------------------------------------------------- 1 | # Launch Screen Assets 2 | 3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory. 4 | 5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images. -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/LaunchScreen.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | -------------------------------------------------------------------------------- /example/ios/Runner/Base.lproj/Main.storyboard: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /example/ios/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleDisplayName 8 | Example 9 | CFBundleExecutable 10 | $(EXECUTABLE_NAME) 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | example 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleSignature 22 | ???? 23 | CFBundleVersion 24 | $(FLUTTER_BUILD_NUMBER) 25 | LSRequiresIPhoneOS 26 | 27 | UILaunchStoryboardName 28 | LaunchScreen 29 | UIMainStoryboardFile 30 | Main 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UISupportedInterfaceOrientations~ipad 38 | 39 | UIInterfaceOrientationPortrait 40 | UIInterfaceOrientationPortraitUpsideDown 41 | UIInterfaceOrientationLandscapeLeft 42 | UIInterfaceOrientationLandscapeRight 43 | 44 | CADisableMinimumFrameDurationOnPhone 45 | 46 | UIApplicationSupportsIndirectInputEvents 47 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /example/ios/Runner/Runner-Bridging-Header.h: -------------------------------------------------------------------------------- 1 | #import "GeneratedPluginRegistrant.h" 2 | -------------------------------------------------------------------------------- /example/ios/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import Flutter 2 | import UIKit 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:control/control.dart'; 4 | import 'package:flutter/material.dart'; 5 | import 'package:l/l.dart'; 6 | 7 | /// Observer for [Controller], react to changes in the any controller. 8 | final class ControllerObserver implements IControllerObserver { 9 | const ControllerObserver(); 10 | 11 | @override 12 | void onCreate(Controller controller) { 13 | l.v6('Controller | ${controller.name}.new'); 14 | } 15 | 16 | @override 17 | void onDispose(Controller controller) { 18 | l.v5('Controller | ${controller.name}.dispose'); 19 | } 20 | 21 | @override 22 | void onHandler(HandlerContext context) { 23 | final stopwatch = Stopwatch()..start(); 24 | l.d( 25 | 'Controller | ' '${context.controller.name}.${context.name}', 26 | context.meta, 27 | ); 28 | context.done.whenComplete(() { 29 | stopwatch.stop(); 30 | l.d( 31 | 'Controller | ' 32 | '${context.controller.name}.${context.name} | ' 33 | 'duration: ${stopwatch.elapsed}', 34 | context.meta, 35 | ); 36 | }); 37 | } 38 | 39 | @override 40 | void onStateChanged( 41 | StateController controller, 42 | S prevState, 43 | S nextState, 44 | ) { 45 | final context = Controller.context; 46 | if (context == null) { 47 | // State change occurred outside of the handler 48 | l.d( 49 | 'StateController | ' 50 | '${controller.name} | ' 51 | '$prevState -> $nextState', 52 | ); 53 | } else { 54 | // State change occurred inside the handler 55 | l.d( 56 | 'StateController | ' 57 | '${controller.name}.${context.name} | ' 58 | '$prevState -> $nextState', 59 | context.meta, 60 | ); 61 | } 62 | } 63 | 64 | @override 65 | void onError(Controller controller, Object error, StackTrace stackTrace) { 66 | final context = Controller.context; 67 | if (context == null) { 68 | // Error occurred outside of the handler 69 | l.w( 70 | 'Controller | ' 71 | '${controller.name} | ' 72 | '$error', 73 | stackTrace, 74 | ); 75 | } else { 76 | // Error occurred inside the handler 77 | l.w( 78 | 'Controller | ' 79 | '${controller.name}.${context.name} | ' 80 | '$error', 81 | stackTrace, 82 | context.meta, 83 | ); 84 | } 85 | } 86 | } 87 | 88 | void main() => runZonedGuarded>( 89 | () async { 90 | // Setup controller observer 91 | Controller.observer = const ControllerObserver(); 92 | runApp(const App()); 93 | }, 94 | (error, stackTrace) => l.e('Top level exception: $error', stackTrace), 95 | ); 96 | 97 | /// Counter state for [CounterController] 98 | typedef CounterState = ({int count, bool idle}); 99 | 100 | /// Counter controller 101 | final class CounterController extends StateController 102 | with SequentialControllerHandler { 103 | CounterController({CounterState? initialState}) 104 | : super(initialState: initialState ?? (idle: true, count: 0)); 105 | 106 | void add(int value) => handle(() async { 107 | setState((idle: false, count: state.count)); 108 | await Future.delayed(const Duration(milliseconds: 1500)); 109 | setState((idle: true, count: state.count + value)); 110 | }); 111 | 112 | void subtract(int value) => handle(() async { 113 | setState((idle: false, count: state.count)); 114 | await Future.delayed(const Duration(milliseconds: 1500)); 115 | setState((idle: true, count: state.count - value)); 116 | }); 117 | } 118 | 119 | class App extends StatelessWidget { 120 | const App({super.key}); 121 | 122 | @override 123 | Widget build(BuildContext context) => MaterialApp( 124 | title: 'StateController example', 125 | theme: ThemeData.dark(), 126 | home: const CounterScreen(), 127 | builder: (context, child) => 128 | // Create and inject the controller into the element tree. 129 | ControllerScope( 130 | CounterController.new, 131 | child: child, 132 | )); 133 | } 134 | 135 | class CounterScreen extends StatelessWidget { 136 | const CounterScreen({super.key}); 137 | 138 | @override 139 | Widget build(BuildContext context) => Scaffold( 140 | appBar: AppBar( 141 | title: const Text('Counter'), 142 | ), 143 | floatingActionButton: const CounterScreen$Buttons(), 144 | body: const SafeArea( 145 | child: Center( 146 | child: CounterScreen$Text(), 147 | ), 148 | ), 149 | ); 150 | } 151 | 152 | class CounterScreen$Text extends StatelessWidget { 153 | const CounterScreen$Text({ 154 | super.key, 155 | }); 156 | 157 | @override 158 | Widget build(BuildContext context) { 159 | final theme = Theme.of(context); 160 | final style = theme.textTheme.headlineMedium; 161 | return Row( 162 | mainAxisSize: MainAxisSize.min, 163 | crossAxisAlignment: CrossAxisAlignment.center, 164 | mainAxisAlignment: MainAxisAlignment.center, 165 | children: [ 166 | Text( 167 | 'Count: ', 168 | style: style, 169 | ), 170 | SizedBox.square( 171 | dimension: 64, 172 | child: Center( 173 | // Receive CounterController from the element tree 174 | // and rebuild the widget when the state changes. 175 | child: StateConsumer( 176 | buildWhen: (previous, current) => 177 | previous.count != current.count || 178 | previous.idle != current.idle, 179 | builder: (context, state, _) { 180 | final text = state.count.toString(); 181 | return AnimatedSwitcher( 182 | duration: const Duration(milliseconds: 500), 183 | transitionBuilder: (child, animation) => ScaleTransition( 184 | scale: animation, 185 | child: FadeTransition( 186 | opacity: animation, 187 | child: child, 188 | ), 189 | ), 190 | child: state.idle 191 | ? Text(text, style: style, overflow: TextOverflow.fade) 192 | : const CircularProgressIndicator(), 193 | ); 194 | }, 195 | ), 196 | ), 197 | ), 198 | ], 199 | ); 200 | } 201 | } 202 | 203 | class CounterScreen$Buttons extends StatelessWidget { 204 | const CounterScreen$Buttons({ 205 | super.key, 206 | }); 207 | 208 | @override 209 | Widget build(BuildContext context) => ValueListenableBuilder( 210 | // Transform [StateController] in to [ValueListenable] 211 | valueListenable: context 212 | .controllerOf() 213 | .select((state) => state.idle), 214 | builder: (context, idle, _) => IgnorePointer( 215 | ignoring: !idle, 216 | child: AnimatedOpacity( 217 | duration: const Duration(milliseconds: 350), 218 | opacity: idle ? 1 : .25, 219 | child: Column( 220 | mainAxisSize: MainAxisSize.min, 221 | children: [ 222 | FloatingActionButton( 223 | key: ValueKey('add#${idle ? 'enabled' : 'disabled'}'), 224 | onPressed: idle 225 | ? () => context.controllerOf().add(1) 226 | : null, 227 | child: const Icon(Icons.add), 228 | ), 229 | const SizedBox(height: 8), 230 | FloatingActionButton( 231 | key: ValueKey('subtract#${idle ? 'enabled' : 'disabled'}'), 232 | onPressed: idle 233 | ? () => 234 | context.controllerOf().subtract(1) 235 | : null, 236 | child: const Icon(Icons.remove), 237 | ), 238 | ], 239 | ), 240 | ), 241 | ), 242 | ); 243 | } 244 | -------------------------------------------------------------------------------- /example/linux/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral 2 | -------------------------------------------------------------------------------- /example/linux/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.10) 3 | project(runner LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "example") 8 | # The unique GTK application identifier for this application. See: 9 | # https://wiki.gnome.org/HowDoI/ChooseApplicationID 10 | set(APPLICATION_ID "com.example.example") 11 | 12 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 13 | # versions of CMake. 14 | cmake_policy(SET CMP0063 NEW) 15 | 16 | # Load bundled libraries from the lib/ directory relative to the binary. 17 | set(CMAKE_INSTALL_RPATH "$ORIGIN/lib") 18 | 19 | # Root filesystem for cross-building. 20 | if(FLUTTER_TARGET_PLATFORM_SYSROOT) 21 | set(CMAKE_SYSROOT ${FLUTTER_TARGET_PLATFORM_SYSROOT}) 22 | set(CMAKE_FIND_ROOT_PATH ${CMAKE_SYSROOT}) 23 | set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER) 24 | set(CMAKE_FIND_ROOT_PATH_MODE_PACKAGE ONLY) 25 | set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY) 26 | set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY) 27 | endif() 28 | 29 | # Define build configuration options. 30 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 31 | set(CMAKE_BUILD_TYPE "Debug" CACHE 32 | STRING "Flutter build mode" FORCE) 33 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 34 | "Debug" "Profile" "Release") 35 | endif() 36 | 37 | # Compilation settings that should be applied to most targets. 38 | # 39 | # Be cautious about adding new options here, as plugins use this function by 40 | # default. In most cases, you should add new options to specific targets instead 41 | # of modifying this function. 42 | function(APPLY_STANDARD_SETTINGS TARGET) 43 | target_compile_features(${TARGET} PUBLIC cxx_std_14) 44 | target_compile_options(${TARGET} PRIVATE -Wall -Werror) 45 | target_compile_options(${TARGET} PRIVATE "$<$>:-O3>") 46 | target_compile_definitions(${TARGET} PRIVATE "$<$>:NDEBUG>") 47 | endfunction() 48 | 49 | # Flutter library and tool build rules. 50 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 51 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 52 | 53 | # System-level dependencies. 54 | find_package(PkgConfig REQUIRED) 55 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 56 | 57 | add_definitions(-DAPPLICATION_ID="${APPLICATION_ID}") 58 | 59 | # Define the application target. To change its name, change BINARY_NAME above, 60 | # not the value here, or `flutter run` will no longer work. 61 | # 62 | # Any new source files that you add to the application should be added here. 63 | add_executable(${BINARY_NAME} 64 | "main.cc" 65 | "my_application.cc" 66 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 67 | ) 68 | 69 | # Apply the standard set of build settings. This can be removed for applications 70 | # that need different build settings. 71 | apply_standard_settings(${BINARY_NAME}) 72 | 73 | # Add dependency libraries. Add any application-specific dependencies here. 74 | target_link_libraries(${BINARY_NAME} PRIVATE flutter) 75 | target_link_libraries(${BINARY_NAME} PRIVATE PkgConfig::GTK) 76 | 77 | # Run the Flutter tool portions of the build. This must not be removed. 78 | add_dependencies(${BINARY_NAME} flutter_assemble) 79 | 80 | # Only the install-generated bundle's copy of the executable will launch 81 | # correctly, since the resources must in the right relative locations. To avoid 82 | # people trying to run the unbundled copy, put it in a subdirectory instead of 83 | # the default top-level location. 84 | set_target_properties(${BINARY_NAME} 85 | PROPERTIES 86 | RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/intermediates_do_not_run" 87 | ) 88 | 89 | 90 | # Generated plugin build rules, which manage building the plugins and adding 91 | # them to the application. 92 | include(flutter/generated_plugins.cmake) 93 | 94 | 95 | # === Installation === 96 | # By default, "installing" just makes a relocatable bundle in the build 97 | # directory. 98 | set(BUILD_BUNDLE_DIR "${PROJECT_BINARY_DIR}/bundle") 99 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 100 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 101 | endif() 102 | 103 | # Start with a clean build bundle directory every time. 104 | install(CODE " 105 | file(REMOVE_RECURSE \"${BUILD_BUNDLE_DIR}/\") 106 | " COMPONENT Runtime) 107 | 108 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 109 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}/lib") 110 | 111 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 112 | COMPONENT Runtime) 113 | 114 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 115 | COMPONENT Runtime) 116 | 117 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 118 | COMPONENT Runtime) 119 | 120 | foreach(bundled_library ${PLUGIN_BUNDLED_LIBRARIES}) 121 | install(FILES "${bundled_library}" 122 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 123 | COMPONENT Runtime) 124 | endforeach(bundled_library) 125 | 126 | # Copy the native assets provided by the build.dart from all packages. 127 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/linux/") 128 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 129 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 130 | COMPONENT Runtime) 131 | 132 | # Fully re-copy the assets directory on each build to avoid having stale files 133 | # from a previous install. 134 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 135 | install(CODE " 136 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 137 | " COMPONENT Runtime) 138 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 139 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 140 | 141 | # Install the AOT library on non-Debug builds only. 142 | if(NOT CMAKE_BUILD_TYPE MATCHES "Debug") 143 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 144 | COMPONENT Runtime) 145 | endif() 146 | -------------------------------------------------------------------------------- /example/linux/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.10) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | 12 | # Serves the same purpose as list(TRANSFORM ... PREPEND ...), 13 | # which isn't available in 3.10. 14 | function(list_prepend LIST_NAME PREFIX) 15 | set(NEW_LIST "") 16 | foreach(element ${${LIST_NAME}}) 17 | list(APPEND NEW_LIST "${PREFIX}${element}") 18 | endforeach(element) 19 | set(${LIST_NAME} "${NEW_LIST}" PARENT_SCOPE) 20 | endfunction() 21 | 22 | # === Flutter Library === 23 | # System-level dependencies. 24 | find_package(PkgConfig REQUIRED) 25 | pkg_check_modules(GTK REQUIRED IMPORTED_TARGET gtk+-3.0) 26 | pkg_check_modules(GLIB REQUIRED IMPORTED_TARGET glib-2.0) 27 | pkg_check_modules(GIO REQUIRED IMPORTED_TARGET gio-2.0) 28 | 29 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/libflutter_linux_gtk.so") 30 | 31 | # Published to parent scope for install step. 32 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 33 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 34 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 35 | set(AOT_LIBRARY "${PROJECT_DIR}/build/lib/libapp.so" PARENT_SCOPE) 36 | 37 | list(APPEND FLUTTER_LIBRARY_HEADERS 38 | "fl_basic_message_channel.h" 39 | "fl_binary_codec.h" 40 | "fl_binary_messenger.h" 41 | "fl_dart_project.h" 42 | "fl_engine.h" 43 | "fl_json_message_codec.h" 44 | "fl_json_method_codec.h" 45 | "fl_message_codec.h" 46 | "fl_method_call.h" 47 | "fl_method_channel.h" 48 | "fl_method_codec.h" 49 | "fl_method_response.h" 50 | "fl_plugin_registrar.h" 51 | "fl_plugin_registry.h" 52 | "fl_standard_message_codec.h" 53 | "fl_standard_method_codec.h" 54 | "fl_string_codec.h" 55 | "fl_value.h" 56 | "fl_view.h" 57 | "flutter_linux.h" 58 | ) 59 | list_prepend(FLUTTER_LIBRARY_HEADERS "${EPHEMERAL_DIR}/flutter_linux/") 60 | add_library(flutter INTERFACE) 61 | target_include_directories(flutter INTERFACE 62 | "${EPHEMERAL_DIR}" 63 | ) 64 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}") 65 | target_link_libraries(flutter INTERFACE 66 | PkgConfig::GTK 67 | PkgConfig::GLIB 68 | PkgConfig::GIO 69 | ) 70 | add_dependencies(flutter flutter_assemble) 71 | 72 | # === Flutter tool backend === 73 | # _phony_ is a non-existent file to force this command to run every time, 74 | # since currently there's no way to get a full input/output list from the 75 | # flutter tool. 76 | add_custom_command( 77 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 78 | ${CMAKE_CURRENT_BINARY_DIR}/_phony_ 79 | COMMAND ${CMAKE_COMMAND} -E env 80 | ${FLUTTER_TOOL_ENVIRONMENT} 81 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.sh" 82 | ${FLUTTER_TARGET_PLATFORM} ${CMAKE_BUILD_TYPE} 83 | VERBATIM 84 | ) 85 | add_custom_target(flutter_assemble DEPENDS 86 | "${FLUTTER_LIBRARY}" 87 | ${FLUTTER_LIBRARY_HEADERS} 88 | ) 89 | -------------------------------------------------------------------------------- /example/linux/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | 10 | void fl_register_plugins(FlPluginRegistry* registry) { 11 | } 12 | -------------------------------------------------------------------------------- /example/linux/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void fl_register_plugins(FlPluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /example/linux/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | ) 7 | 8 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 9 | ) 10 | 11 | set(PLUGIN_BUNDLED_LIBRARIES) 12 | 13 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 14 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) 15 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 16 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 18 | endforeach(plugin) 19 | 20 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 23 | endforeach(ffi_plugin) 24 | -------------------------------------------------------------------------------- /example/linux/main.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | int main(int argc, char** argv) { 4 | g_autoptr(MyApplication) app = my_application_new(); 5 | return g_application_run(G_APPLICATION(app), argc, argv); 6 | } 7 | -------------------------------------------------------------------------------- /example/linux/my_application.cc: -------------------------------------------------------------------------------- 1 | #include "my_application.h" 2 | 3 | #include 4 | #ifdef GDK_WINDOWING_X11 5 | #include 6 | #endif 7 | 8 | #include "flutter/generated_plugin_registrant.h" 9 | 10 | struct _MyApplication { 11 | GtkApplication parent_instance; 12 | char** dart_entrypoint_arguments; 13 | }; 14 | 15 | G_DEFINE_TYPE(MyApplication, my_application, GTK_TYPE_APPLICATION) 16 | 17 | // Implements GApplication::activate. 18 | static void my_application_activate(GApplication* application) { 19 | MyApplication* self = MY_APPLICATION(application); 20 | GtkWindow* window = 21 | GTK_WINDOW(gtk_application_window_new(GTK_APPLICATION(application))); 22 | 23 | // Use a header bar when running in GNOME as this is the common style used 24 | // by applications and is the setup most users will be using (e.g. Ubuntu 25 | // desktop). 26 | // If running on X and not using GNOME then just use a traditional title bar 27 | // in case the window manager does more exotic layout, e.g. tiling. 28 | // If running on Wayland assume the header bar will work (may need changing 29 | // if future cases occur). 30 | gboolean use_header_bar = TRUE; 31 | #ifdef GDK_WINDOWING_X11 32 | GdkScreen* screen = gtk_window_get_screen(window); 33 | if (GDK_IS_X11_SCREEN(screen)) { 34 | const gchar* wm_name = gdk_x11_screen_get_window_manager_name(screen); 35 | if (g_strcmp0(wm_name, "GNOME Shell") != 0) { 36 | use_header_bar = FALSE; 37 | } 38 | } 39 | #endif 40 | if (use_header_bar) { 41 | GtkHeaderBar* header_bar = GTK_HEADER_BAR(gtk_header_bar_new()); 42 | gtk_widget_show(GTK_WIDGET(header_bar)); 43 | gtk_header_bar_set_title(header_bar, "example"); 44 | gtk_header_bar_set_show_close_button(header_bar, TRUE); 45 | gtk_window_set_titlebar(window, GTK_WIDGET(header_bar)); 46 | } else { 47 | gtk_window_set_title(window, "example"); 48 | } 49 | 50 | gtk_window_set_default_size(window, 1280, 720); 51 | gtk_widget_show(GTK_WIDGET(window)); 52 | 53 | g_autoptr(FlDartProject) project = fl_dart_project_new(); 54 | fl_dart_project_set_dart_entrypoint_arguments(project, self->dart_entrypoint_arguments); 55 | 56 | FlView* view = fl_view_new(project); 57 | gtk_widget_show(GTK_WIDGET(view)); 58 | gtk_container_add(GTK_CONTAINER(window), GTK_WIDGET(view)); 59 | 60 | fl_register_plugins(FL_PLUGIN_REGISTRY(view)); 61 | 62 | gtk_widget_grab_focus(GTK_WIDGET(view)); 63 | } 64 | 65 | // Implements GApplication::local_command_line. 66 | static gboolean my_application_local_command_line(GApplication* application, gchar*** arguments, int* exit_status) { 67 | MyApplication* self = MY_APPLICATION(application); 68 | // Strip out the first argument as it is the binary name. 69 | self->dart_entrypoint_arguments = g_strdupv(*arguments + 1); 70 | 71 | g_autoptr(GError) error = nullptr; 72 | if (!g_application_register(application, nullptr, &error)) { 73 | g_warning("Failed to register: %s", error->message); 74 | *exit_status = 1; 75 | return TRUE; 76 | } 77 | 78 | g_application_activate(application); 79 | *exit_status = 0; 80 | 81 | return TRUE; 82 | } 83 | 84 | // Implements GObject::dispose. 85 | static void my_application_dispose(GObject* object) { 86 | MyApplication* self = MY_APPLICATION(object); 87 | g_clear_pointer(&self->dart_entrypoint_arguments, g_strfreev); 88 | G_OBJECT_CLASS(my_application_parent_class)->dispose(object); 89 | } 90 | 91 | static void my_application_class_init(MyApplicationClass* klass) { 92 | G_APPLICATION_CLASS(klass)->activate = my_application_activate; 93 | G_APPLICATION_CLASS(klass)->local_command_line = my_application_local_command_line; 94 | G_OBJECT_CLASS(klass)->dispose = my_application_dispose; 95 | } 96 | 97 | static void my_application_init(MyApplication* self) {} 98 | 99 | MyApplication* my_application_new() { 100 | return MY_APPLICATION(g_object_new(my_application_get_type(), 101 | "application-id", APPLICATION_ID, 102 | "flags", G_APPLICATION_NON_UNIQUE, 103 | nullptr)); 104 | } 105 | -------------------------------------------------------------------------------- /example/linux/my_application.h: -------------------------------------------------------------------------------- 1 | #ifndef FLUTTER_MY_APPLICATION_H_ 2 | #define FLUTTER_MY_APPLICATION_H_ 3 | 4 | #include 5 | 6 | G_DECLARE_FINAL_TYPE(MyApplication, my_application, MY, APPLICATION, 7 | GtkApplication) 8 | 9 | /** 10 | * my_application_new: 11 | * 12 | * Creates a new Flutter-based application. 13 | * 14 | * Returns: a new #MyApplication. 15 | */ 16 | MyApplication* my_application_new(); 17 | 18 | #endif // FLUTTER_MY_APPLICATION_H_ 19 | -------------------------------------------------------------------------------- /example/macos/.gitignore: -------------------------------------------------------------------------------- 1 | # Flutter-related 2 | **/Flutter/ephemeral/ 3 | **/Pods/ 4 | 5 | # Xcode-related 6 | **/dgph 7 | **/xcuserdata/ 8 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Flutter/Flutter-Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include? "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig" 2 | #include "ephemeral/Flutter-Generated.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Flutter/GeneratedPluginRegistrant.swift: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | import FlutterMacOS 6 | import Foundation 7 | 8 | import shared_preferences_foundation 9 | 10 | func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { 11 | SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) 12 | } 13 | -------------------------------------------------------------------------------- /example/macos/Podfile: -------------------------------------------------------------------------------- 1 | platform :osx, '10.14' 2 | 3 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency. 4 | ENV['COCOAPODS_DISABLE_STATS'] = 'true' 5 | 6 | project 'Runner', { 7 | 'Debug' => :debug, 8 | 'Profile' => :release, 9 | 'Release' => :release, 10 | } 11 | 12 | def flutter_root 13 | generated_xcode_build_settings_path = File.expand_path(File.join('..', 'Flutter', 'ephemeral', 'Flutter-Generated.xcconfig'), __FILE__) 14 | unless File.exist?(generated_xcode_build_settings_path) 15 | raise "#{generated_xcode_build_settings_path} must exist. If you're running pod install manually, make sure \"flutter pub get\" is executed first" 16 | end 17 | 18 | File.foreach(generated_xcode_build_settings_path) do |line| 19 | matches = line.match(/FLUTTER_ROOT\=(.*)/) 20 | return matches[1].strip if matches 21 | end 22 | raise "FLUTTER_ROOT not found in #{generated_xcode_build_settings_path}. Try deleting Flutter-Generated.xcconfig, then run \"flutter pub get\"" 23 | end 24 | 25 | require File.expand_path(File.join('packages', 'flutter_tools', 'bin', 'podhelper'), flutter_root) 26 | 27 | flutter_macos_podfile_setup 28 | 29 | target 'Runner' do 30 | use_frameworks! 31 | use_modular_headers! 32 | 33 | flutter_install_all_macos_pods File.dirname(File.realpath(__FILE__)) 34 | target 'RunnerTests' do 35 | inherit! :search_paths 36 | end 37 | end 38 | 39 | post_install do |installer| 40 | installer.pods_project.targets.each do |target| 41 | flutter_additional_macos_build_settings(target) 42 | end 43 | end 44 | -------------------------------------------------------------------------------- /example/macos/Podfile.lock: -------------------------------------------------------------------------------- 1 | PODS: 2 | - FlutterMacOS (1.0.0) 3 | - shared_preferences_foundation (0.0.1): 4 | - Flutter 5 | - FlutterMacOS 6 | 7 | DEPENDENCIES: 8 | - FlutterMacOS (from `Flutter/ephemeral`) 9 | - shared_preferences_foundation (from `Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin`) 10 | 11 | EXTERNAL SOURCES: 12 | FlutterMacOS: 13 | :path: Flutter/ephemeral 14 | shared_preferences_foundation: 15 | :path: Flutter/ephemeral/.symlinks/plugins/shared_preferences_foundation/darwin 16 | 17 | SPEC CHECKSUMS: 18 | FlutterMacOS: 8f6f14fa908a6fb3fba0cd85dbd81ec4b251fb24 19 | shared_preferences_foundation: 5b919d13b803cadd15ed2dc053125c68730e5126 20 | 21 | PODFILE CHECKSUM: 236401fc2c932af29a9fcf0e97baeeb2d750d367 22 | 23 | COCOAPODS: 1.15.2 24 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 24 | 25 | 30 | 31 | 37 | 38 | 39 | 40 | 43 | 49 | 50 | 51 | 52 | 53 | 63 | 65 | 71 | 72 | 73 | 74 | 80 | 82 | 88 | 89 | 90 | 91 | 93 | 94 | 97 | 98 | 99 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/contents.xcworkspacedata: -------------------------------------------------------------------------------- 1 | 2 | 4 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /example/macos/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | IDEDidComputeMac32BitWarning 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/Runner/AppDelegate.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | @main 5 | class AppDelegate: FlutterAppDelegate { 6 | override func applicationShouldTerminateAfterLastWindowClosed(_ sender: NSApplication) -> Bool { 7 | return true 8 | } 9 | 10 | override func applicationSupportsSecureRestorableState(_ app: NSApplication) -> Bool { 11 | return true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "size" : "16x16", 5 | "idiom" : "mac", 6 | "filename" : "app_icon_16.png", 7 | "scale" : "1x" 8 | }, 9 | { 10 | "size" : "16x16", 11 | "idiom" : "mac", 12 | "filename" : "app_icon_32.png", 13 | "scale" : "2x" 14 | }, 15 | { 16 | "size" : "32x32", 17 | "idiom" : "mac", 18 | "filename" : "app_icon_32.png", 19 | "scale" : "1x" 20 | }, 21 | { 22 | "size" : "32x32", 23 | "idiom" : "mac", 24 | "filename" : "app_icon_64.png", 25 | "scale" : "2x" 26 | }, 27 | { 28 | "size" : "128x128", 29 | "idiom" : "mac", 30 | "filename" : "app_icon_128.png", 31 | "scale" : "1x" 32 | }, 33 | { 34 | "size" : "128x128", 35 | "idiom" : "mac", 36 | "filename" : "app_icon_256.png", 37 | "scale" : "2x" 38 | }, 39 | { 40 | "size" : "256x256", 41 | "idiom" : "mac", 42 | "filename" : "app_icon_256.png", 43 | "scale" : "1x" 44 | }, 45 | { 46 | "size" : "256x256", 47 | "idiom" : "mac", 48 | "filename" : "app_icon_512.png", 49 | "scale" : "2x" 50 | }, 51 | { 52 | "size" : "512x512", 53 | "idiom" : "mac", 54 | "filename" : "app_icon_512.png", 55 | "scale" : "1x" 56 | }, 57 | { 58 | "size" : "512x512", 59 | "idiom" : "mac", 60 | "filename" : "app_icon_1024.png", 61 | "scale" : "2x" 62 | } 63 | ], 64 | "info" : { 65 | "version" : 1, 66 | "author" : "xcode" 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_1024.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_128.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_16.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_256.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_32.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_512.png -------------------------------------------------------------------------------- /example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/macos/Runner/Assets.xcassets/AppIcon.appiconset/app_icon_64.png -------------------------------------------------------------------------------- /example/macos/Runner/Configs/AppInfo.xcconfig: -------------------------------------------------------------------------------- 1 | // Application-level settings for the Runner target. 2 | // 3 | // This may be replaced with something auto-generated from metadata (e.g., pubspec.yaml) in the 4 | // future. If not, the values below would default to using the project name when this becomes a 5 | // 'flutter create' template. 6 | 7 | // The application's name. By default this is also the title of the Flutter window. 8 | PRODUCT_NAME = example 9 | 10 | // The application's bundle identifier 11 | PRODUCT_BUNDLE_IDENTIFIER = com.example.example 12 | 13 | // The copyright displayed in application information 14 | PRODUCT_COPYRIGHT = Copyright © 2023 com.example. All rights reserved. 15 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Debug.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Debug.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Release.xcconfig: -------------------------------------------------------------------------------- 1 | #include "../../Flutter/Flutter-Release.xcconfig" 2 | #include "Warnings.xcconfig" 3 | -------------------------------------------------------------------------------- /example/macos/Runner/Configs/Warnings.xcconfig: -------------------------------------------------------------------------------- 1 | WARNING_CFLAGS = -Wall -Wconditional-uninitialized -Wnullable-to-nonnull-conversion -Wmissing-method-return-type -Woverlength-strings 2 | GCC_WARN_UNDECLARED_SELECTOR = YES 3 | CLANG_UNDEFINED_BEHAVIOR_SANITIZER_NULLABILITY = YES 4 | CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE 5 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES 6 | CLANG_WARN_PRAGMA_PACK = YES 7 | CLANG_WARN_STRICT_PROTOTYPES = YES 8 | CLANG_WARN_COMMA = YES 9 | GCC_WARN_STRICT_SELECTOR_MATCH = YES 10 | CLANG_WARN_OBJC_REPEATED_USE_OF_WEAK = YES 11 | CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES 12 | GCC_WARN_SHADOW = YES 13 | CLANG_WARN_UNREACHABLE_CODE = YES 14 | -------------------------------------------------------------------------------- /example/macos/Runner/DebugProfile.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | com.apple.security.cs.allow-jit 8 | 9 | com.apple.security.network.server 10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /example/macos/Runner/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | $(DEVELOPMENT_LANGUAGE) 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIconFile 10 | 11 | CFBundleIdentifier 12 | $(PRODUCT_BUNDLE_IDENTIFIER) 13 | CFBundleInfoDictionaryVersion 14 | 6.0 15 | CFBundleName 16 | $(PRODUCT_NAME) 17 | CFBundlePackageType 18 | APPL 19 | CFBundleShortVersionString 20 | $(FLUTTER_BUILD_NAME) 21 | CFBundleVersion 22 | $(FLUTTER_BUILD_NUMBER) 23 | LSMinimumSystemVersion 24 | $(MACOSX_DEPLOYMENT_TARGET) 25 | NSHumanReadableCopyright 26 | $(PRODUCT_COPYRIGHT) 27 | NSMainNibFile 28 | MainMenu 29 | NSPrincipalClass 30 | NSApplication 31 | 32 | 33 | -------------------------------------------------------------------------------- /example/macos/Runner/MainFlutterWindow.swift: -------------------------------------------------------------------------------- 1 | import Cocoa 2 | import FlutterMacOS 3 | 4 | class MainFlutterWindow: NSWindow { 5 | override func awakeFromNib() { 6 | let flutterViewController = FlutterViewController() 7 | let windowFrame = self.frame 8 | self.contentViewController = flutterViewController 9 | self.setFrame(windowFrame, display: true) 10 | 11 | RegisterGeneratedPlugins(registry: flutterViewController) 12 | 13 | super.awakeFromNib() 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /example/macos/Runner/Release.entitlements: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | com.apple.security.app-sandbox 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /example/macos/RunnerTests/RunnerTests.swift: -------------------------------------------------------------------------------- 1 | import FlutterMacOS 2 | import Cocoa 3 | import XCTest 4 | 5 | class RunnerTests: XCTestCase { 6 | 7 | func testExample() { 8 | // If you add code to the Runner application, consider adding tests here. 9 | // See https://developer.apple.com/documentation/xctest for more information about using XCTest. 10 | } 11 | 12 | } 13 | -------------------------------------------------------------------------------- /example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: example 2 | description: "Control example" 3 | 4 | publish_to: 'none' 5 | 6 | homepage: https://github.com/PlugFox/control 7 | 8 | repository: https://github.com/PlugFox/control 9 | 10 | issue_tracker: https://github.com/PlugFox/control/issues 11 | 12 | funding: 13 | - https://www.buymeacoffee.com/plugfox 14 | - https://www.patreon.com/plugfox 15 | - https://boosty.to/plugfox 16 | 17 | topics: 18 | - architecture 19 | - state-management 20 | - state 21 | - concurrency 22 | - controller 23 | 24 | platforms: 25 | android: 26 | ios: 27 | linux: 28 | macos: 29 | web: 30 | windows: 31 | 32 | version: 1.0.0+1 33 | 34 | environment: 35 | sdk: '>=3.4.0 <4.0.0' 36 | flutter: ">=3.16.0" 37 | 38 | dependencies: 39 | # Flutter SDK 40 | flutter: 41 | sdk: flutter 42 | 43 | # Utility 44 | collection: any 45 | async: any 46 | meta: any 47 | path: any 48 | convert: any 49 | 50 | # Logger 51 | l: ^5.0.0-pre.2 52 | 53 | # Storage 54 | shared_preferences: ^2.2.2 55 | 56 | # UI and Widgets 57 | cupertino_icons: ^1.0.5 58 | 59 | # State management 60 | control: 61 | path: ../ 62 | 63 | dev_dependencies: 64 | # Unit & Widget tests for Flutter 65 | flutter_test: 66 | sdk: flutter 67 | # Integration tests for Flutter 68 | integration_test: 69 | sdk: flutter 70 | 71 | # Linting 72 | flutter_lints: ^2.0.1 73 | 74 | # Code generation 75 | build_runner: ^2.4.6 76 | 77 | flutter: 78 | generate: true 79 | uses-material-design: true -------------------------------------------------------------------------------- /example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter_test/flutter_test.dart'; 9 | 10 | void main() { 11 | testWidgets('placeholder', (tester) async { 12 | expect( 13 | true, 14 | isTrue, 15 | ); 16 | }); 17 | } 18 | -------------------------------------------------------------------------------- /example/web/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/web/favicon.png -------------------------------------------------------------------------------- /example/web/icons/Icon-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/web/icons/Icon-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/web/icons/Icon-512.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/web/icons/Icon-maskable-192.png -------------------------------------------------------------------------------- /example/web/icons/Icon-maskable-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/web/icons/Icon-maskable-512.png -------------------------------------------------------------------------------- /example/web/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | example 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 | 58 | 59 | 60 | -------------------------------------------------------------------------------- /example/web/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "example", 3 | "short_name": "example", 4 | "start_url": ".", 5 | "display": "standalone", 6 | "background_color": "#0175C2", 7 | "theme_color": "#0175C2", 8 | "description": "A new Flutter project.", 9 | "orientation": "portrait-primary", 10 | "prefer_related_applications": false, 11 | "icons": [ 12 | { 13 | "src": "icons/Icon-192.png", 14 | "sizes": "192x192", 15 | "type": "image/png" 16 | }, 17 | { 18 | "src": "icons/Icon-512.png", 19 | "sizes": "512x512", 20 | "type": "image/png" 21 | }, 22 | { 23 | "src": "icons/Icon-maskable-192.png", 24 | "sizes": "192x192", 25 | "type": "image/png", 26 | "purpose": "maskable" 27 | }, 28 | { 29 | "src": "icons/Icon-maskable-512.png", 30 | "sizes": "512x512", 31 | "type": "image/png", 32 | "purpose": "maskable" 33 | } 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /example/windows/.gitignore: -------------------------------------------------------------------------------- 1 | flutter/ephemeral/ 2 | 3 | # Visual Studio user-specific files. 4 | *.suo 5 | *.user 6 | *.userosscache 7 | *.sln.docstates 8 | 9 | # Visual Studio build-related files. 10 | x64/ 11 | x86/ 12 | 13 | # Visual Studio cache files 14 | # files ending in .cache can be ignored 15 | *.[Cc]ache 16 | # but keep track of directories ending in .cache 17 | !*.[Cc]ache/ 18 | -------------------------------------------------------------------------------- /example/windows/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # Project-level configuration. 2 | cmake_minimum_required(VERSION 3.14) 3 | project(example LANGUAGES CXX) 4 | 5 | # The name of the executable created for the application. Change this to change 6 | # the on-disk name of your application. 7 | set(BINARY_NAME "example") 8 | 9 | # Explicitly opt in to modern CMake behaviors to avoid warnings with recent 10 | # versions of CMake. 11 | cmake_policy(VERSION 3.14...3.25) 12 | 13 | # Define build configuration option. 14 | get_property(IS_MULTICONFIG GLOBAL PROPERTY GENERATOR_IS_MULTI_CONFIG) 15 | if(IS_MULTICONFIG) 16 | set(CMAKE_CONFIGURATION_TYPES "Debug;Profile;Release" 17 | CACHE STRING "" FORCE) 18 | else() 19 | if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) 20 | set(CMAKE_BUILD_TYPE "Debug" CACHE 21 | STRING "Flutter build mode" FORCE) 22 | set_property(CACHE CMAKE_BUILD_TYPE PROPERTY STRINGS 23 | "Debug" "Profile" "Release") 24 | endif() 25 | endif() 26 | # Define settings for the Profile build mode. 27 | set(CMAKE_EXE_LINKER_FLAGS_PROFILE "${CMAKE_EXE_LINKER_FLAGS_RELEASE}") 28 | set(CMAKE_SHARED_LINKER_FLAGS_PROFILE "${CMAKE_SHARED_LINKER_FLAGS_RELEASE}") 29 | set(CMAKE_C_FLAGS_PROFILE "${CMAKE_C_FLAGS_RELEASE}") 30 | set(CMAKE_CXX_FLAGS_PROFILE "${CMAKE_CXX_FLAGS_RELEASE}") 31 | 32 | # Use Unicode for all projects. 33 | add_definitions(-DUNICODE -D_UNICODE) 34 | 35 | # Compilation settings that should be applied to most targets. 36 | # 37 | # Be cautious about adding new options here, as plugins use this function by 38 | # default. In most cases, you should add new options to specific targets instead 39 | # of modifying this function. 40 | function(APPLY_STANDARD_SETTINGS TARGET) 41 | target_compile_features(${TARGET} PUBLIC cxx_std_17) 42 | target_compile_options(${TARGET} PRIVATE /W4 /WX /wd"4100") 43 | target_compile_options(${TARGET} PRIVATE /EHsc) 44 | target_compile_definitions(${TARGET} PRIVATE "_HAS_EXCEPTIONS=0") 45 | target_compile_definitions(${TARGET} PRIVATE "$<$:_DEBUG>") 46 | endfunction() 47 | 48 | # Flutter library and tool build rules. 49 | set(FLUTTER_MANAGED_DIR "${CMAKE_CURRENT_SOURCE_DIR}/flutter") 50 | add_subdirectory(${FLUTTER_MANAGED_DIR}) 51 | 52 | # Application build; see runner/CMakeLists.txt. 53 | add_subdirectory("runner") 54 | 55 | 56 | # Generated plugin build rules, which manage building the plugins and adding 57 | # them to the application. 58 | include(flutter/generated_plugins.cmake) 59 | 60 | 61 | # === Installation === 62 | # Support files are copied into place next to the executable, so that it can 63 | # run in place. This is done instead of making a separate bundle (as on Linux) 64 | # so that building and running from within Visual Studio will work. 65 | set(BUILD_BUNDLE_DIR "$") 66 | # Make the "install" step default, as it's required to run. 67 | set(CMAKE_VS_INCLUDE_INSTALL_TO_DEFAULT_BUILD 1) 68 | if(CMAKE_INSTALL_PREFIX_INITIALIZED_TO_DEFAULT) 69 | set(CMAKE_INSTALL_PREFIX "${BUILD_BUNDLE_DIR}" CACHE PATH "..." FORCE) 70 | endif() 71 | 72 | set(INSTALL_BUNDLE_DATA_DIR "${CMAKE_INSTALL_PREFIX}/data") 73 | set(INSTALL_BUNDLE_LIB_DIR "${CMAKE_INSTALL_PREFIX}") 74 | 75 | install(TARGETS ${BINARY_NAME} RUNTIME DESTINATION "${CMAKE_INSTALL_PREFIX}" 76 | COMPONENT Runtime) 77 | 78 | install(FILES "${FLUTTER_ICU_DATA_FILE}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 79 | COMPONENT Runtime) 80 | 81 | install(FILES "${FLUTTER_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 82 | COMPONENT Runtime) 83 | 84 | if(PLUGIN_BUNDLED_LIBRARIES) 85 | install(FILES "${PLUGIN_BUNDLED_LIBRARIES}" 86 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 87 | COMPONENT Runtime) 88 | endif() 89 | 90 | # Copy the native assets provided by the build.dart from all packages. 91 | set(NATIVE_ASSETS_DIR "${PROJECT_BUILD_DIR}native_assets/windows/") 92 | install(DIRECTORY "${NATIVE_ASSETS_DIR}" 93 | DESTINATION "${INSTALL_BUNDLE_LIB_DIR}" 94 | COMPONENT Runtime) 95 | 96 | # Fully re-copy the assets directory on each build to avoid having stale files 97 | # from a previous install. 98 | set(FLUTTER_ASSET_DIR_NAME "flutter_assets") 99 | install(CODE " 100 | file(REMOVE_RECURSE \"${INSTALL_BUNDLE_DATA_DIR}/${FLUTTER_ASSET_DIR_NAME}\") 101 | " COMPONENT Runtime) 102 | install(DIRECTORY "${PROJECT_BUILD_DIR}/${FLUTTER_ASSET_DIR_NAME}" 103 | DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" COMPONENT Runtime) 104 | 105 | # Install the AOT library on non-Debug builds only. 106 | install(FILES "${AOT_LIBRARY}" DESTINATION "${INSTALL_BUNDLE_DATA_DIR}" 107 | CONFIGURATIONS Profile;Release 108 | COMPONENT Runtime) 109 | -------------------------------------------------------------------------------- /example/windows/flutter/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # This file controls Flutter-level build steps. It should not be edited. 2 | cmake_minimum_required(VERSION 3.14) 3 | 4 | set(EPHEMERAL_DIR "${CMAKE_CURRENT_SOURCE_DIR}/ephemeral") 5 | 6 | # Configuration provided via flutter tool. 7 | include(${EPHEMERAL_DIR}/generated_config.cmake) 8 | 9 | # TODO: Move the rest of this into files in ephemeral. See 10 | # https://github.com/flutter/flutter/issues/57146. 11 | set(WRAPPER_ROOT "${EPHEMERAL_DIR}/cpp_client_wrapper") 12 | 13 | # Set fallback configurations for older versions of the flutter tool. 14 | if (NOT DEFINED FLUTTER_TARGET_PLATFORM) 15 | set(FLUTTER_TARGET_PLATFORM "windows-x64") 16 | endif() 17 | 18 | # === Flutter Library === 19 | set(FLUTTER_LIBRARY "${EPHEMERAL_DIR}/flutter_windows.dll") 20 | 21 | # Published to parent scope for install step. 22 | set(FLUTTER_LIBRARY ${FLUTTER_LIBRARY} PARENT_SCOPE) 23 | set(FLUTTER_ICU_DATA_FILE "${EPHEMERAL_DIR}/icudtl.dat" PARENT_SCOPE) 24 | set(PROJECT_BUILD_DIR "${PROJECT_DIR}/build/" PARENT_SCOPE) 25 | set(AOT_LIBRARY "${PROJECT_DIR}/build/windows/app.so" PARENT_SCOPE) 26 | 27 | list(APPEND FLUTTER_LIBRARY_HEADERS 28 | "flutter_export.h" 29 | "flutter_windows.h" 30 | "flutter_messenger.h" 31 | "flutter_plugin_registrar.h" 32 | "flutter_texture_registrar.h" 33 | ) 34 | list(TRANSFORM FLUTTER_LIBRARY_HEADERS PREPEND "${EPHEMERAL_DIR}/") 35 | add_library(flutter INTERFACE) 36 | target_include_directories(flutter INTERFACE 37 | "${EPHEMERAL_DIR}" 38 | ) 39 | target_link_libraries(flutter INTERFACE "${FLUTTER_LIBRARY}.lib") 40 | add_dependencies(flutter flutter_assemble) 41 | 42 | # === Wrapper === 43 | list(APPEND CPP_WRAPPER_SOURCES_CORE 44 | "core_implementations.cc" 45 | "standard_codec.cc" 46 | ) 47 | list(TRANSFORM CPP_WRAPPER_SOURCES_CORE PREPEND "${WRAPPER_ROOT}/") 48 | list(APPEND CPP_WRAPPER_SOURCES_PLUGIN 49 | "plugin_registrar.cc" 50 | ) 51 | list(TRANSFORM CPP_WRAPPER_SOURCES_PLUGIN PREPEND "${WRAPPER_ROOT}/") 52 | list(APPEND CPP_WRAPPER_SOURCES_APP 53 | "flutter_engine.cc" 54 | "flutter_view_controller.cc" 55 | ) 56 | list(TRANSFORM CPP_WRAPPER_SOURCES_APP PREPEND "${WRAPPER_ROOT}/") 57 | 58 | # Wrapper sources needed for a plugin. 59 | add_library(flutter_wrapper_plugin STATIC 60 | ${CPP_WRAPPER_SOURCES_CORE} 61 | ${CPP_WRAPPER_SOURCES_PLUGIN} 62 | ) 63 | apply_standard_settings(flutter_wrapper_plugin) 64 | set_target_properties(flutter_wrapper_plugin PROPERTIES 65 | POSITION_INDEPENDENT_CODE ON) 66 | set_target_properties(flutter_wrapper_plugin PROPERTIES 67 | CXX_VISIBILITY_PRESET hidden) 68 | target_link_libraries(flutter_wrapper_plugin PUBLIC flutter) 69 | target_include_directories(flutter_wrapper_plugin PUBLIC 70 | "${WRAPPER_ROOT}/include" 71 | ) 72 | add_dependencies(flutter_wrapper_plugin flutter_assemble) 73 | 74 | # Wrapper sources needed for the runner. 75 | add_library(flutter_wrapper_app STATIC 76 | ${CPP_WRAPPER_SOURCES_CORE} 77 | ${CPP_WRAPPER_SOURCES_APP} 78 | ) 79 | apply_standard_settings(flutter_wrapper_app) 80 | target_link_libraries(flutter_wrapper_app PUBLIC flutter) 81 | target_include_directories(flutter_wrapper_app PUBLIC 82 | "${WRAPPER_ROOT}/include" 83 | ) 84 | add_dependencies(flutter_wrapper_app flutter_assemble) 85 | 86 | # === Flutter tool backend === 87 | # _phony_ is a non-existent file to force this command to run every time, 88 | # since currently there's no way to get a full input/output list from the 89 | # flutter tool. 90 | set(PHONY_OUTPUT "${CMAKE_CURRENT_BINARY_DIR}/_phony_") 91 | set_source_files_properties("${PHONY_OUTPUT}" PROPERTIES SYMBOLIC TRUE) 92 | add_custom_command( 93 | OUTPUT ${FLUTTER_LIBRARY} ${FLUTTER_LIBRARY_HEADERS} 94 | ${CPP_WRAPPER_SOURCES_CORE} ${CPP_WRAPPER_SOURCES_PLUGIN} 95 | ${CPP_WRAPPER_SOURCES_APP} 96 | ${PHONY_OUTPUT} 97 | COMMAND ${CMAKE_COMMAND} -E env 98 | ${FLUTTER_TOOL_ENVIRONMENT} 99 | "${FLUTTER_ROOT}/packages/flutter_tools/bin/tool_backend.bat" 100 | ${FLUTTER_TARGET_PLATFORM} $ 101 | VERBATIM 102 | ) 103 | add_custom_target(flutter_assemble DEPENDS 104 | "${FLUTTER_LIBRARY}" 105 | ${FLUTTER_LIBRARY_HEADERS} 106 | ${CPP_WRAPPER_SOURCES_CORE} 107 | ${CPP_WRAPPER_SOURCES_PLUGIN} 108 | ${CPP_WRAPPER_SOURCES_APP} 109 | ) 110 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugin_registrant.cc: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #include "generated_plugin_registrant.h" 8 | 9 | 10 | void RegisterPlugins(flutter::PluginRegistry* registry) { 11 | } 12 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugin_registrant.h: -------------------------------------------------------------------------------- 1 | // 2 | // Generated file. Do not edit. 3 | // 4 | 5 | // clang-format off 6 | 7 | #ifndef GENERATED_PLUGIN_REGISTRANT_ 8 | #define GENERATED_PLUGIN_REGISTRANT_ 9 | 10 | #include 11 | 12 | // Registers Flutter plugins. 13 | void RegisterPlugins(flutter::PluginRegistry* registry); 14 | 15 | #endif // GENERATED_PLUGIN_REGISTRANT_ 16 | -------------------------------------------------------------------------------- /example/windows/flutter/generated_plugins.cmake: -------------------------------------------------------------------------------- 1 | # 2 | # Generated file, do not edit. 3 | # 4 | 5 | list(APPEND FLUTTER_PLUGIN_LIST 6 | ) 7 | 8 | list(APPEND FLUTTER_FFI_PLUGIN_LIST 9 | ) 10 | 11 | set(PLUGIN_BUNDLED_LIBRARIES) 12 | 13 | foreach(plugin ${FLUTTER_PLUGIN_LIST}) 14 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/windows plugins/${plugin}) 15 | target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) 16 | list(APPEND PLUGIN_BUNDLED_LIBRARIES $) 17 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) 18 | endforeach(plugin) 19 | 20 | foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) 21 | add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/windows plugins/${ffi_plugin}) 22 | list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) 23 | endforeach(ffi_plugin) 24 | -------------------------------------------------------------------------------- /example/windows/runner/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.14) 2 | project(runner LANGUAGES CXX) 3 | 4 | # Define the application target. To change its name, change BINARY_NAME in the 5 | # top-level CMakeLists.txt, not the value here, or `flutter run` will no longer 6 | # work. 7 | # 8 | # Any new source files that you add to the application should be added here. 9 | add_executable(${BINARY_NAME} WIN32 10 | "flutter_window.cpp" 11 | "main.cpp" 12 | "utils.cpp" 13 | "win32_window.cpp" 14 | "${FLUTTER_MANAGED_DIR}/generated_plugin_registrant.cc" 15 | "Runner.rc" 16 | "runner.exe.manifest" 17 | ) 18 | 19 | # Apply the standard set of build settings. This can be removed for applications 20 | # that need different build settings. 21 | apply_standard_settings(${BINARY_NAME}) 22 | 23 | # Add preprocessor definitions for the build version. 24 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION=\"${FLUTTER_VERSION}\"") 25 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MAJOR=${FLUTTER_VERSION_MAJOR}") 26 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_MINOR=${FLUTTER_VERSION_MINOR}") 27 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_PATCH=${FLUTTER_VERSION_PATCH}") 28 | target_compile_definitions(${BINARY_NAME} PRIVATE "FLUTTER_VERSION_BUILD=${FLUTTER_VERSION_BUILD}") 29 | 30 | # Disable Windows macros that collide with C++ standard library functions. 31 | target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") 32 | 33 | # Add dependency libraries and include directories. Add any application-specific 34 | # dependencies here. 35 | target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) 36 | target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib") 37 | target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") 38 | 39 | # Run the Flutter tool portions of the build. This must not be removed. 40 | add_dependencies(${BINARY_NAME} flutter_assemble) 41 | -------------------------------------------------------------------------------- /example/windows/runner/Runner.rc: -------------------------------------------------------------------------------- 1 | // Microsoft Visual C++ generated resource script. 2 | // 3 | #pragma code_page(65001) 4 | #include "resource.h" 5 | 6 | #define APSTUDIO_READONLY_SYMBOLS 7 | ///////////////////////////////////////////////////////////////////////////// 8 | // 9 | // Generated from the TEXTINCLUDE 2 resource. 10 | // 11 | #include "winres.h" 12 | 13 | ///////////////////////////////////////////////////////////////////////////// 14 | #undef APSTUDIO_READONLY_SYMBOLS 15 | 16 | ///////////////////////////////////////////////////////////////////////////// 17 | // English (United States) resources 18 | 19 | #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_ENU) 20 | LANGUAGE LANG_ENGLISH, SUBLANG_ENGLISH_US 21 | 22 | #ifdef APSTUDIO_INVOKED 23 | ///////////////////////////////////////////////////////////////////////////// 24 | // 25 | // TEXTINCLUDE 26 | // 27 | 28 | 1 TEXTINCLUDE 29 | BEGIN 30 | "resource.h\0" 31 | END 32 | 33 | 2 TEXTINCLUDE 34 | BEGIN 35 | "#include ""winres.h""\r\n" 36 | "\0" 37 | END 38 | 39 | 3 TEXTINCLUDE 40 | BEGIN 41 | "\r\n" 42 | "\0" 43 | END 44 | 45 | #endif // APSTUDIO_INVOKED 46 | 47 | 48 | ///////////////////////////////////////////////////////////////////////////// 49 | // 50 | // Icon 51 | // 52 | 53 | // Icon with lowest ID value placed first to ensure application icon 54 | // remains consistent on all systems. 55 | IDI_APP_ICON ICON "resources\\app_icon.ico" 56 | 57 | 58 | ///////////////////////////////////////////////////////////////////////////// 59 | // 60 | // Version 61 | // 62 | 63 | #if defined(FLUTTER_VERSION_MAJOR) && defined(FLUTTER_VERSION_MINOR) && defined(FLUTTER_VERSION_PATCH) && defined(FLUTTER_VERSION_BUILD) 64 | #define VERSION_AS_NUMBER FLUTTER_VERSION_MAJOR,FLUTTER_VERSION_MINOR,FLUTTER_VERSION_PATCH,FLUTTER_VERSION_BUILD 65 | #else 66 | #define VERSION_AS_NUMBER 1,0,0,0 67 | #endif 68 | 69 | #if defined(FLUTTER_VERSION) 70 | #define VERSION_AS_STRING FLUTTER_VERSION 71 | #else 72 | #define VERSION_AS_STRING "1.0.0" 73 | #endif 74 | 75 | VS_VERSION_INFO VERSIONINFO 76 | FILEVERSION VERSION_AS_NUMBER 77 | PRODUCTVERSION VERSION_AS_NUMBER 78 | FILEFLAGSMASK VS_FFI_FILEFLAGSMASK 79 | #ifdef _DEBUG 80 | FILEFLAGS VS_FF_DEBUG 81 | #else 82 | FILEFLAGS 0x0L 83 | #endif 84 | FILEOS VOS__WINDOWS32 85 | FILETYPE VFT_APP 86 | FILESUBTYPE 0x0L 87 | BEGIN 88 | BLOCK "StringFileInfo" 89 | BEGIN 90 | BLOCK "040904e4" 91 | BEGIN 92 | VALUE "CompanyName", "com.example" "\0" 93 | VALUE "FileDescription", "example" "\0" 94 | VALUE "FileVersion", VERSION_AS_STRING "\0" 95 | VALUE "InternalName", "example" "\0" 96 | VALUE "LegalCopyright", "Copyright (C) 2023 com.example. All rights reserved." "\0" 97 | VALUE "OriginalFilename", "example.exe" "\0" 98 | VALUE "ProductName", "example" "\0" 99 | VALUE "ProductVersion", VERSION_AS_STRING "\0" 100 | END 101 | END 102 | BLOCK "VarFileInfo" 103 | BEGIN 104 | VALUE "Translation", 0x409, 1252 105 | END 106 | END 107 | 108 | #endif // English (United States) resources 109 | ///////////////////////////////////////////////////////////////////////////// 110 | 111 | 112 | 113 | #ifndef APSTUDIO_INVOKED 114 | ///////////////////////////////////////////////////////////////////////////// 115 | // 116 | // Generated from the TEXTINCLUDE 3 resource. 117 | // 118 | 119 | 120 | ///////////////////////////////////////////////////////////////////////////// 121 | #endif // not APSTUDIO_INVOKED 122 | -------------------------------------------------------------------------------- /example/windows/runner/flutter_window.cpp: -------------------------------------------------------------------------------- 1 | #include "flutter_window.h" 2 | 3 | #include 4 | 5 | #include "flutter/generated_plugin_registrant.h" 6 | 7 | FlutterWindow::FlutterWindow(const flutter::DartProject& project) 8 | : project_(project) {} 9 | 10 | FlutterWindow::~FlutterWindow() {} 11 | 12 | bool FlutterWindow::OnCreate() { 13 | if (!Win32Window::OnCreate()) { 14 | return false; 15 | } 16 | 17 | RECT frame = GetClientArea(); 18 | 19 | // The size here must match the window dimensions to avoid unnecessary surface 20 | // creation / destruction in the startup path. 21 | flutter_controller_ = std::make_unique( 22 | frame.right - frame.left, frame.bottom - frame.top, project_); 23 | // Ensure that basic setup of the controller was successful. 24 | if (!flutter_controller_->engine() || !flutter_controller_->view()) { 25 | return false; 26 | } 27 | RegisterPlugins(flutter_controller_->engine()); 28 | SetChildContent(flutter_controller_->view()->GetNativeWindow()); 29 | 30 | flutter_controller_->engine()->SetNextFrameCallback([&]() { 31 | this->Show(); 32 | }); 33 | 34 | // Flutter can complete the first frame before the "show window" callback is 35 | // registered. The following call ensures a frame is pending to ensure the 36 | // window is shown. It is a no-op if the first frame hasn't completed yet. 37 | flutter_controller_->ForceRedraw(); 38 | 39 | return true; 40 | } 41 | 42 | void FlutterWindow::OnDestroy() { 43 | if (flutter_controller_) { 44 | flutter_controller_ = nullptr; 45 | } 46 | 47 | Win32Window::OnDestroy(); 48 | } 49 | 50 | LRESULT 51 | FlutterWindow::MessageHandler(HWND hwnd, UINT const message, 52 | WPARAM const wparam, 53 | LPARAM const lparam) noexcept { 54 | // Give Flutter, including plugins, an opportunity to handle window messages. 55 | if (flutter_controller_) { 56 | std::optional result = 57 | flutter_controller_->HandleTopLevelWindowProc(hwnd, message, wparam, 58 | lparam); 59 | if (result) { 60 | return *result; 61 | } 62 | } 63 | 64 | switch (message) { 65 | case WM_FONTCHANGE: 66 | flutter_controller_->engine()->ReloadSystemFonts(); 67 | break; 68 | } 69 | 70 | return Win32Window::MessageHandler(hwnd, message, wparam, lparam); 71 | } 72 | -------------------------------------------------------------------------------- /example/windows/runner/flutter_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_FLUTTER_WINDOW_H_ 2 | #define RUNNER_FLUTTER_WINDOW_H_ 3 | 4 | #include 5 | #include 6 | 7 | #include 8 | 9 | #include "win32_window.h" 10 | 11 | // A window that does nothing but host a Flutter view. 12 | class FlutterWindow : public Win32Window { 13 | public: 14 | // Creates a new FlutterWindow hosting a Flutter view running |project|. 15 | explicit FlutterWindow(const flutter::DartProject& project); 16 | virtual ~FlutterWindow(); 17 | 18 | protected: 19 | // Win32Window: 20 | bool OnCreate() override; 21 | void OnDestroy() override; 22 | LRESULT MessageHandler(HWND window, UINT const message, WPARAM const wparam, 23 | LPARAM const lparam) noexcept override; 24 | 25 | private: 26 | // The project to run. 27 | flutter::DartProject project_; 28 | 29 | // The Flutter instance hosted by this window. 30 | std::unique_ptr flutter_controller_; 31 | }; 32 | 33 | #endif // RUNNER_FLUTTER_WINDOW_H_ 34 | -------------------------------------------------------------------------------- /example/windows/runner/main.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | 5 | #include "flutter_window.h" 6 | #include "utils.h" 7 | 8 | int APIENTRY wWinMain(_In_ HINSTANCE instance, _In_opt_ HINSTANCE prev, 9 | _In_ wchar_t *command_line, _In_ int show_command) { 10 | // Attach to console when present (e.g., 'flutter run') or create a 11 | // new console when running with a debugger. 12 | if (!::AttachConsole(ATTACH_PARENT_PROCESS) && ::IsDebuggerPresent()) { 13 | CreateAndAttachConsole(); 14 | } 15 | 16 | // Initialize COM, so that it is available for use in the library and/or 17 | // plugins. 18 | ::CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED); 19 | 20 | flutter::DartProject project(L"data"); 21 | 22 | std::vector command_line_arguments = 23 | GetCommandLineArguments(); 24 | 25 | project.set_dart_entrypoint_arguments(std::move(command_line_arguments)); 26 | 27 | FlutterWindow window(project); 28 | Win32Window::Point origin(10, 10); 29 | Win32Window::Size size(1280, 720); 30 | if (!window.Create(L"example", origin, size)) { 31 | return EXIT_FAILURE; 32 | } 33 | window.SetQuitOnClose(true); 34 | 35 | ::MSG msg; 36 | while (::GetMessage(&msg, nullptr, 0, 0)) { 37 | ::TranslateMessage(&msg); 38 | ::DispatchMessage(&msg); 39 | } 40 | 41 | ::CoUninitialize(); 42 | return EXIT_SUCCESS; 43 | } 44 | -------------------------------------------------------------------------------- /example/windows/runner/resource.h: -------------------------------------------------------------------------------- 1 | //{{NO_DEPENDENCIES}} 2 | // Microsoft Visual C++ generated include file. 3 | // Used by Runner.rc 4 | // 5 | #define IDI_APP_ICON 101 6 | 7 | // Next default values for new objects 8 | // 9 | #ifdef APSTUDIO_INVOKED 10 | #ifndef APSTUDIO_READONLY_SYMBOLS 11 | #define _APS_NEXT_RESOURCE_VALUE 102 12 | #define _APS_NEXT_COMMAND_VALUE 40001 13 | #define _APS_NEXT_CONTROL_VALUE 1001 14 | #define _APS_NEXT_SYMED_VALUE 101 15 | #endif 16 | #endif 17 | -------------------------------------------------------------------------------- /example/windows/runner/resources/app_icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/PlugFox/control/b3b0fa5919e366aa4c18bc239b0298666acbae28/example/windows/runner/resources/app_icon.ico -------------------------------------------------------------------------------- /example/windows/runner/runner.exe.manifest: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | PerMonitorV2 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /example/windows/runner/utils.cpp: -------------------------------------------------------------------------------- 1 | #include "utils.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | 8 | #include 9 | 10 | void CreateAndAttachConsole() { 11 | if (::AllocConsole()) { 12 | FILE *unused; 13 | if (freopen_s(&unused, "CONOUT$", "w", stdout)) { 14 | _dup2(_fileno(stdout), 1); 15 | } 16 | if (freopen_s(&unused, "CONOUT$", "w", stderr)) { 17 | _dup2(_fileno(stdout), 2); 18 | } 19 | std::ios::sync_with_stdio(); 20 | FlutterDesktopResyncOutputStreams(); 21 | } 22 | } 23 | 24 | std::vector GetCommandLineArguments() { 25 | // Convert the UTF-16 command line arguments to UTF-8 for the Engine to use. 26 | int argc; 27 | wchar_t** argv = ::CommandLineToArgvW(::GetCommandLineW(), &argc); 28 | if (argv == nullptr) { 29 | return std::vector(); 30 | } 31 | 32 | std::vector command_line_arguments; 33 | 34 | // Skip the first argument as it's the binary name. 35 | for (int i = 1; i < argc; i++) { 36 | command_line_arguments.push_back(Utf8FromUtf16(argv[i])); 37 | } 38 | 39 | ::LocalFree(argv); 40 | 41 | return command_line_arguments; 42 | } 43 | 44 | std::string Utf8FromUtf16(const wchar_t* utf16_string) { 45 | if (utf16_string == nullptr) { 46 | return std::string(); 47 | } 48 | int target_length = ::WideCharToMultiByte( 49 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 50 | -1, nullptr, 0, nullptr, nullptr) 51 | -1; // remove the trailing null character 52 | int input_length = (int)wcslen(utf16_string); 53 | std::string utf8_string; 54 | if (target_length <= 0 || target_length > utf8_string.max_size()) { 55 | return utf8_string; 56 | } 57 | utf8_string.resize(target_length); 58 | int converted_length = ::WideCharToMultiByte( 59 | CP_UTF8, WC_ERR_INVALID_CHARS, utf16_string, 60 | input_length, utf8_string.data(), target_length, nullptr, nullptr); 61 | if (converted_length == 0) { 62 | return std::string(); 63 | } 64 | return utf8_string; 65 | } 66 | -------------------------------------------------------------------------------- /example/windows/runner/utils.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_UTILS_H_ 2 | #define RUNNER_UTILS_H_ 3 | 4 | #include 5 | #include 6 | 7 | // Creates a console for the process, and redirects stdout and stderr to 8 | // it for both the runner and the Flutter library. 9 | void CreateAndAttachConsole(); 10 | 11 | // Takes a null-terminated wchar_t* encoded in UTF-16 and returns a std::string 12 | // encoded in UTF-8. Returns an empty std::string on failure. 13 | std::string Utf8FromUtf16(const wchar_t* utf16_string); 14 | 15 | // Gets the command line arguments passed in as a std::vector, 16 | // encoded in UTF-8. Returns an empty std::vector on failure. 17 | std::vector GetCommandLineArguments(); 18 | 19 | #endif // RUNNER_UTILS_H_ 20 | -------------------------------------------------------------------------------- /example/windows/runner/win32_window.h: -------------------------------------------------------------------------------- 1 | #ifndef RUNNER_WIN32_WINDOW_H_ 2 | #define RUNNER_WIN32_WINDOW_H_ 3 | 4 | #include 5 | 6 | #include 7 | #include 8 | #include 9 | 10 | // A class abstraction for a high DPI-aware Win32 Window. Intended to be 11 | // inherited from by classes that wish to specialize with custom 12 | // rendering and input handling 13 | class Win32Window { 14 | public: 15 | struct Point { 16 | unsigned int x; 17 | unsigned int y; 18 | Point(unsigned int x, unsigned int y) : x(x), y(y) {} 19 | }; 20 | 21 | struct Size { 22 | unsigned int width; 23 | unsigned int height; 24 | Size(unsigned int width, unsigned int height) 25 | : width(width), height(height) {} 26 | }; 27 | 28 | Win32Window(); 29 | virtual ~Win32Window(); 30 | 31 | // Creates a win32 window with |title| that is positioned and sized using 32 | // |origin| and |size|. New windows are created on the default monitor. Window 33 | // sizes are specified to the OS in physical pixels, hence to ensure a 34 | // consistent size this function will scale the inputted width and height as 35 | // as appropriate for the default monitor. The window is invisible until 36 | // |Show| is called. Returns true if the window was created successfully. 37 | bool Create(const std::wstring& title, const Point& origin, const Size& size); 38 | 39 | // Show the current window. Returns true if the window was successfully shown. 40 | bool Show(); 41 | 42 | // Release OS resources associated with window. 43 | void Destroy(); 44 | 45 | // Inserts |content| into the window tree. 46 | void SetChildContent(HWND content); 47 | 48 | // Returns the backing Window handle to enable clients to set icon and other 49 | // window properties. Returns nullptr if the window has been destroyed. 50 | HWND GetHandle(); 51 | 52 | // If true, closing this window will quit the application. 53 | void SetQuitOnClose(bool quit_on_close); 54 | 55 | // Return a RECT representing the bounds of the current client area. 56 | RECT GetClientArea(); 57 | 58 | protected: 59 | // Processes and route salient window messages for mouse handling, 60 | // size change and DPI. Delegates handling of these to member overloads that 61 | // inheriting classes can handle. 62 | virtual LRESULT MessageHandler(HWND window, 63 | UINT const message, 64 | WPARAM const wparam, 65 | LPARAM const lparam) noexcept; 66 | 67 | // Called when CreateAndShow is called, allowing subclass window-related 68 | // setup. Subclasses should return false if setup fails. 69 | virtual bool OnCreate(); 70 | 71 | // Called when Destroy is called. 72 | virtual void OnDestroy(); 73 | 74 | private: 75 | friend class WindowClassRegistrar; 76 | 77 | // OS callback called by message pump. Handles the WM_NCCREATE message which 78 | // is passed when the non-client area is being created and enables automatic 79 | // non-client DPI scaling so that the non-client area automatically 80 | // responds to changes in DPI. All other messages are handled by 81 | // MessageHandler. 82 | static LRESULT CALLBACK WndProc(HWND const window, 83 | UINT const message, 84 | WPARAM const wparam, 85 | LPARAM const lparam) noexcept; 86 | 87 | // Retrieves a class instance pointer for |window| 88 | static Win32Window* GetThisFromHandle(HWND const window) noexcept; 89 | 90 | // Update the window frame's theme to match the system theme. 91 | static void UpdateTheme(HWND const window); 92 | 93 | bool quit_on_close_ = false; 94 | 95 | // window handle for top level window. 96 | HWND window_handle_ = nullptr; 97 | 98 | // window handle for hosted content. 99 | HWND child_content_ = nullptr; 100 | }; 101 | 102 | #endif // RUNNER_WIN32_WINDOW_H_ 103 | -------------------------------------------------------------------------------- /lib/control.dart: -------------------------------------------------------------------------------- 1 | library; 2 | 3 | export 'package:control/src/concurrency/concurrency.dart'; 4 | export 'package:control/src/controller.dart' hide IController; 5 | export 'package:control/src/controller_scope.dart' hide ControllerScope$Element; 6 | export 'package:control/src/handler_context.dart' show HandlerContext; 7 | export 'package:control/src/state_consumer.dart'; 8 | export 'package:control/src/state_controller.dart' hide IStateController; 9 | -------------------------------------------------------------------------------- /lib/src/concurrency/concurrency.dart: -------------------------------------------------------------------------------- 1 | export 'package:control/src/concurrency/concurrent_controller_handler.dart'; 2 | export 'package:control/src/concurrency/droppable_controller_handler.dart'; 3 | export 'package:control/src/concurrency/sequential_controller_handler.dart'; 4 | -------------------------------------------------------------------------------- /lib/src/concurrency/concurrent_controller_handler.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:control/src/controller.dart'; 4 | import 'package:control/src/handler_context.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | /// A mixin that provides concurrent controller concurrency handling. 8 | /// This mixin should be used on classes that extend [Controller]. 9 | base mixin ConcurrentControllerHandler on Controller { 10 | @override 11 | @nonVirtual 12 | bool get isProcessing => _$processingCalls > 0; 13 | 14 | /// Tracks the number of ongoing processing calls. 15 | int _$processingCalls = 0; 16 | 17 | /// Handles a given operation with error handling and completion tracking. 18 | /// 19 | /// [handler] is the main operation to be executed. 20 | /// [error] is an optional error handler. 21 | /// [done] is an optional callback to be executed when the operation is done. 22 | /// [name] is an optional name for the operation, used for debugging. 23 | /// [meta] is an optional HashMap of context data to be passed to the zone. 24 | @override 25 | @protected 26 | @mustCallSuper 27 | Future handle( 28 | Future Function() handler, { 29 | Future Function(Object error, StackTrace stackTrace)? error, 30 | Future Function()? done, 31 | String? name, 32 | Map? meta, 33 | }) { 34 | if (isDisposed) return Future.value(null); 35 | _$processingCalls++; 36 | final completer = Completer(); 37 | var isDone = false; // ignore error callback after done 38 | 39 | Future onError(Object e, StackTrace st) async { 40 | if (isDisposed) return; 41 | try { 42 | super.onError(e, st); 43 | if (isDone || isDisposed || completer.isCompleted) return; 44 | await error?.call(e, st); 45 | } on Object catch (error, stackTrace) { 46 | super.onError(error, stackTrace); 47 | } 48 | } 49 | 50 | Future handleZoneError(Object error, StackTrace stackTrace) async { 51 | if (isDisposed) return; 52 | super.onError(error, stackTrace); 53 | assert( 54 | false, 55 | 'A zone error occurred during controller event handling. ' 56 | 'This may be caused by an unawaited future. ' 57 | 'Make sure to await all futures in the controller ' 58 | 'event handlers.', 59 | ); 60 | } 61 | 62 | void onDone() { 63 | if (completer.isCompleted) return; 64 | _$processingCalls--; 65 | completer.complete(); 66 | } 67 | 68 | final handlerContext = HandlerContextImpl( 69 | controller: this, 70 | name: name ?? 'handler#${handler.runtimeType}', 71 | completer: completer, 72 | meta: { 73 | ...?meta, 74 | }, 75 | ); 76 | 77 | runZonedGuarded( 78 | () async { 79 | try { 80 | if (isDisposed) return; 81 | Controller.observer?.onHandler(handlerContext); 82 | await handler(); 83 | } on Object catch (error, stackTrace) { 84 | await onError(error, stackTrace); 85 | } finally { 86 | isDone = true; 87 | try { 88 | await done?.call(); 89 | } on Object catch (error, stackTrace) { 90 | super.onError(error, stackTrace); 91 | } finally { 92 | onDone(); 93 | } 94 | } 95 | }, 96 | handleZoneError, 97 | zoneValues: { 98 | HandlerContext.key: handlerContext, 99 | }, 100 | ); 101 | 102 | return completer.future; 103 | } 104 | 105 | /* @override 106 | @protected 107 | @mustCallSuper 108 | Future handle( 109 | Future Function() handler, { 110 | Future Function(Object error, StackTrace stackTrace)? error, 111 | Future Function()? done, 112 | }) => 113 | runZonedGuarded( 114 | () async { 115 | if (isDisposed) return; 116 | _$processingCalls++; 117 | _done ??= Completer.sync(); 118 | try { 119 | await handler(); 120 | } on Object catch (e, st) { 121 | onError(e, st); 122 | await Future(() async { 123 | await error?.call(e, st); 124 | }).catchError(onError); 125 | } finally { 126 | isDone = true; 127 | await Future(() async { 128 | await done?.call(); 129 | }).catchError(onError); 130 | _$processingCalls--; 131 | if (_$processingCalls == 0) { 132 | final completer = _done; 133 | if (completer != null && !completer.isCompleted) { 134 | completer.complete(); 135 | } 136 | _done = null; 137 | } 138 | } 139 | }, 140 | onError, 141 | ); */ 142 | } 143 | -------------------------------------------------------------------------------- /lib/src/concurrency/droppable_controller_handler.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:control/src/controller.dart'; 4 | import 'package:control/src/handler_context.dart'; 5 | import 'package:meta/meta.dart'; 6 | 7 | /// Droppable controller concurrency 8 | base mixin DroppableControllerHandler on Controller { 9 | @override 10 | @nonVirtual 11 | bool get isProcessing => _$processingCalls > 0; 12 | int _$processingCalls = 0; 13 | 14 | /// Handles a given operation with error handling and completion tracking. 15 | /// 16 | /// [handler] is the main operation to be executed. 17 | /// [error] is an optional error handler. 18 | /// [done] is an optional callback to be executed when the operation is done. 19 | /// [name] is an optional name for the operation, used for debugging. 20 | /// [meta] is an optional HashMap of context data to be passed to the zone. 21 | @override 22 | @protected 23 | @mustCallSuper 24 | Future handle( 25 | Future Function() handler, { 26 | Future Function(Object error, StackTrace stackTrace)? error, 27 | Future Function()? done, 28 | String? name, 29 | Map? meta, 30 | }) { 31 | if (isDisposed || isProcessing) return Future.value(null); 32 | _$processingCalls++; 33 | final completer = Completer(); 34 | var isDone = false; // ignore error callback after done 35 | 36 | Future onError(Object e, StackTrace st) async { 37 | if (isDisposed) return; 38 | try { 39 | super.onError(e, st); 40 | if (isDone || isDisposed || completer.isCompleted) return; 41 | await error?.call(e, st); 42 | } on Object catch (error, stackTrace) { 43 | super.onError(error, stackTrace); 44 | } 45 | } 46 | 47 | Future handleZoneError(Object error, StackTrace stackTrace) async { 48 | if (isDisposed) return; 49 | super.onError(error, stackTrace); 50 | assert( 51 | false, 52 | 'A zone error occurred during controller event handling. ' 53 | 'This may be caused by an unawaited future. ' 54 | 'Make sure to await all futures in the controller ' 55 | 'event handlers.', 56 | ); 57 | } 58 | 59 | void onDone() { 60 | if (completer.isCompleted) return; 61 | _$processingCalls--; 62 | completer.complete(); 63 | } 64 | 65 | final handlerContext = HandlerContextImpl( 66 | controller: this, 67 | name: name ?? 'handler#${handler.runtimeType}', 68 | completer: completer, 69 | meta: { 70 | ...?meta, 71 | }, 72 | ); 73 | 74 | runZonedGuarded( 75 | () async { 76 | try { 77 | if (isDisposed) return; 78 | Controller.observer?.onHandler(handlerContext); 79 | await handler(); 80 | } on Object catch (error, stackTrace) { 81 | await onError(error, stackTrace); 82 | } finally { 83 | isDone = true; 84 | try { 85 | await done?.call(); 86 | } on Object catch (error, stackTrace) { 87 | super.onError(error, stackTrace); 88 | } finally { 89 | onDone(); 90 | } 91 | } 92 | }, 93 | handleZoneError, 94 | zoneValues: { 95 | HandlerContext.key: handlerContext, 96 | }, 97 | ); 98 | 99 | return completer.future; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /lib/src/concurrency/sequential_controller_handler.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | import 'dart:collection'; 3 | 4 | import 'package:control/src/controller.dart'; 5 | import 'package:control/src/handler_context.dart'; 6 | import 'package:meta/meta.dart'; 7 | 8 | /// Sequential controller concurrency 9 | base mixin SequentialControllerHandler on Controller { 10 | final _ControllerEventQueue _eventQueue = _ControllerEventQueue(); 11 | 12 | @override 13 | @nonVirtual 14 | bool get isProcessing => _eventQueue.length > 0; 15 | 16 | /// Handles a given operation with error handling and completion tracking. 17 | /// 18 | /// [handler] is the main operation to be executed. 19 | /// [error] is an optional error handler. 20 | /// [done] is an optional callback to be executed when the operation is done. 21 | /// [name] is an optional name for the operation, used for debugging. 22 | /// [meta] is an optional HashMap of context data to be passed to the zone. 23 | @override 24 | @protected 25 | @mustCallSuper 26 | Future handle( 27 | Future Function() handler, { 28 | Future Function(Object error, StackTrace stackTrace)? error, 29 | Future Function()? done, 30 | String? name, 31 | Map? meta, 32 | }) => 33 | _eventQueue.push( 34 | () { 35 | final completer = Completer(); 36 | var isDone = false; // ignore error callback after done 37 | 38 | Future onError(Object e, StackTrace st) async { 39 | if (isDisposed) return; 40 | try { 41 | super.onError(e, st); 42 | if (isDone || isDisposed || completer.isCompleted) return; 43 | await error?.call(e, st); 44 | } on Object catch (error, stackTrace) { 45 | super.onError(error, stackTrace); 46 | } 47 | } 48 | 49 | Future handleZoneError( 50 | Object error, StackTrace stackTrace) async { 51 | if (isDisposed) return; 52 | super.onError(error, stackTrace); 53 | assert( 54 | false, 55 | 'A zone error occurred during controller event handling. ' 56 | 'This may be caused by an unawaited future. ' 57 | 'Make sure to await all futures in the controller ' 58 | 'event handlers.', 59 | ); 60 | } 61 | 62 | final handlerContext = HandlerContextImpl( 63 | controller: this, 64 | name: name ?? 'handler#${handler.runtimeType}', 65 | completer: completer, 66 | meta: { 67 | ...?meta, 68 | }, 69 | ); 70 | 71 | void onDone() { 72 | if (completer.isCompleted) return; 73 | completer.complete(); 74 | } 75 | 76 | runZonedGuarded( 77 | () async { 78 | try { 79 | if (isDisposed) return; 80 | Controller.observer?.onHandler(handlerContext); 81 | await handler(); 82 | } on Object catch (error, stackTrace) { 83 | await onError(error, stackTrace); 84 | } finally { 85 | isDone = true; 86 | try { 87 | await done?.call(); 88 | } on Object catch (error, stackTrace) { 89 | super.onError(error, stackTrace); 90 | } finally { 91 | onDone(); 92 | } 93 | } 94 | }, 95 | handleZoneError, 96 | zoneValues: { 97 | HandlerContext.key: handlerContext, 98 | }, 99 | ); 100 | 101 | return completer.future; 102 | }, 103 | ).catchError((_, __) => null); 104 | } 105 | 106 | final class _ControllerEventQueue { 107 | _ControllerEventQueue(); 108 | 109 | final DoubleLinkedQueue<_SequentialTask> _queue = 110 | DoubleLinkedQueue<_SequentialTask>(); 111 | Future? _processing; 112 | bool _isClosed = false; 113 | 114 | /// Event queue length. 115 | int get length => _queue.length; 116 | 117 | /// Push it at the end of the queue. 118 | Future push(Future Function() fn) { 119 | final task = _SequentialTask(fn); 120 | _queue.add(task); 121 | _exec(); 122 | return task.future; 123 | } 124 | 125 | /// Mark the queue as closed. 126 | /// The queue will be processed until it's empty. 127 | /// But all new and current events will be rejected with [WSClientClosed]. 128 | Future close() async { 129 | _isClosed = true; 130 | await _processing; 131 | } 132 | 133 | /// Execute the queue. 134 | void _exec() => _processing ??= Future.doWhile(() async { 135 | final event = _queue.first; 136 | try { 137 | if (_isClosed) { 138 | event.reject(StateError('Controller\'s event queue are disposed'), 139 | StackTrace.current); 140 | } else { 141 | await event(); 142 | } 143 | } on Object catch (error, stackTrace) { 144 | /* warning( 145 | error, 146 | stackTrace, 147 | 'Error while processing event "${event.id}"', 148 | ); */ 149 | Future.sync(() => event.reject(error, stackTrace)).ignore(); 150 | } 151 | _queue.removeFirst(); 152 | final isEmpty = _queue.isEmpty; 153 | if (isEmpty) _processing = null; 154 | return !isEmpty; 155 | }); 156 | } 157 | 158 | class _SequentialTask { 159 | _SequentialTask(Future Function() fn) 160 | : _fn = fn, 161 | _completer = Completer(); 162 | 163 | final Completer _completer; 164 | 165 | final Future Function() _fn; 166 | 167 | Future get future => _completer.future; 168 | 169 | Future call() async { 170 | final result = await _fn(); 171 | if (!_completer.isCompleted) { 172 | _completer.complete(result); 173 | } 174 | return result; 175 | } 176 | 177 | void reject(Object error, [StackTrace? stackTrace]) { 178 | if (_completer.isCompleted) return; 179 | _completer.completeError(error, stackTrace); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /lib/src/controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:control/src/handler_context.dart'; 4 | import 'package:control/src/registry.dart'; 5 | import 'package:control/src/state_controller.dart'; 6 | import 'package:flutter/foundation.dart' 7 | show ChangeNotifier, Listenable, VoidCallback; 8 | import 'package:meta/meta.dart'; 9 | 10 | /// The controller responsible for processing the logic, 11 | /// the connection of widgets and the date of the layer. 12 | /// 13 | /// Do not implement this interface directly, instead extend [Controller]. 14 | @internal 15 | abstract interface class IController implements Listenable { 16 | /// The name of the controller. 17 | /// By default, it is the runtime type of the controller. 18 | /// Override this property to provide a custom name. 19 | String get name; 20 | 21 | /// Whether the controller is permanently disposed 22 | bool get isDisposed; 23 | 24 | /// The number of subscribers to the controller 25 | int get subscribers; 26 | 27 | /// Whether any listeners are currently registered. 28 | bool get hasListeners; 29 | 30 | /// Whether the controller is currently handling a requests 31 | bool get isProcessing; 32 | 33 | /// Discards any resources used by the object. 34 | /// 35 | /// This method should only be called by the object's owner. 36 | void dispose(); 37 | 38 | /// Handles invocation in the controller. 39 | /// 40 | /// Depending on the implementation, the handler may be executed 41 | /// sequentially, concurrently, dropped and etc. 42 | /// 43 | /// The [name] parameter is used to identify the handler. 44 | /// The [meta] parameter is used to pass additional 45 | /// information to the handler's zone. 46 | /// 47 | /// See: 48 | /// - [ConcurrentControllerHandler] - handler that executes concurrently 49 | /// - [SequentialControllerHandler] - handler that executes sequentially 50 | /// - [DroppableControllerHandler] - handler that drops the request when busy 51 | void handle( 52 | Future Function() handler, { 53 | String? name, 54 | Map? meta, 55 | }); 56 | } 57 | 58 | /// Controller observer 59 | abstract interface class IControllerObserver { 60 | /// Called when the controller is created. 61 | void onCreate(Controller controller); 62 | 63 | /// Called when the controller is disposed. 64 | void onDispose(Controller controller); 65 | 66 | /// Called when the controller is handling a request. 67 | void onHandler(HandlerContext context); 68 | 69 | /// Called on any state change in the [StateController]. 70 | void onStateChanged( 71 | StateController controller, S prevState, S nextState); 72 | 73 | /// Called on any error in the controller. 74 | void onError(Controller controller, Object error, StackTrace stackTrace); 75 | } 76 | 77 | /// {@template controller} 78 | /// The controller responsible for processing the logic, 79 | /// the connection of widgets and the date of the layer. 80 | /// {@endtemplate} 81 | abstract base class Controller with ChangeNotifier implements IController { 82 | /// {@macro controller} 83 | Controller() { 84 | ControllerRegistry().insert(this); 85 | runZonedGuarded( 86 | () => Controller.observer?.onCreate(this), 87 | (error, stackTrace) {/* ignore */}, // coverage:ignore-line 88 | ); 89 | } 90 | 91 | /// Get the handler's context from the current zone. 92 | static HandlerContext? get context => HandlerContext.zoned(); 93 | 94 | /// Controller observer 95 | static IControllerObserver? observer; 96 | 97 | /// Return a [Listenable] that triggers when any of the given [Listenable]s 98 | /// themselves trigger. 99 | static Listenable merge(Iterable listenables) => 100 | Listenable.merge( 101 | List.unmodifiable(listenables.whereType()), 102 | ); 103 | 104 | @override 105 | String get name => runtimeType.toString(); 106 | 107 | @override 108 | bool get isDisposed => _$isDisposed; 109 | bool _$isDisposed = false; 110 | 111 | @override 112 | int get subscribers => _$subscribers; 113 | int _$subscribers = 0; 114 | 115 | /// Error handling callback 116 | @protected 117 | void onError(Object error, StackTrace stackTrace) => runZonedGuarded( 118 | () => Controller.observer?.onError(this, error, stackTrace), 119 | (error, stackTrace) {/* ignore */}, // coverage:ignore-line 120 | ); 121 | 122 | /// Handles a given operation with error handling and completion tracking. 123 | /// 124 | /// [handler] is the main operation to be executed. 125 | /// [name] is an optional name for the operation, used for debugging. 126 | /// [meta] is an optional HashMap of context data to be passed to the zone. 127 | @protected 128 | @override 129 | Future handle( 130 | Future Function() handler, { 131 | String? name, 132 | Map? meta, 133 | }); 134 | 135 | @protected 136 | @nonVirtual 137 | @override 138 | void notifyListeners() { 139 | if (isDisposed) { 140 | assert(false, 'A $runtimeType was already disposed.'); 141 | return; 142 | } 143 | super.notifyListeners(); 144 | } 145 | 146 | @override 147 | @mustCallSuper 148 | void addListener(VoidCallback listener) { 149 | if (isDisposed) { 150 | assert(false, 'A $runtimeType was already disposed.'); 151 | return; 152 | } 153 | super.addListener(listener); 154 | _$subscribers++; 155 | } 156 | 157 | @override 158 | @mustCallSuper 159 | void removeListener(VoidCallback listener) { 160 | super.removeListener(listener); 161 | if (isDisposed) return; 162 | _$subscribers--; 163 | } 164 | 165 | @override 166 | @mustCallSuper 167 | void dispose() { 168 | if (_$isDisposed) { 169 | assert(false, 'A $runtimeType was already disposed.'); 170 | return; 171 | } 172 | _$isDisposed = true; 173 | _$subscribers = 0; 174 | runZonedGuarded( 175 | () => Controller.observer?.onDispose(this), 176 | (error, stackTrace) {/* ignore */}, // coverage:ignore-line 177 | ); 178 | ControllerRegistry().remove(); 179 | super.dispose(); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /lib/src/extensions.dart: -------------------------------------------------------------------------------- 1 | // TODO(plugfox): Implements selectors, context, stream getters and so on. 2 | -------------------------------------------------------------------------------- /lib/src/handler_context.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:control/src/controller.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | /// Handler's context. 7 | abstract interface class HandlerContext { 8 | /// Key to access the handler's context. 9 | static const Object key = #handler; 10 | 11 | /// Get the handler's context from the current zone. 12 | static HandlerContext? zoned() => switch (Zone.current[HandlerContext.key]) { 13 | HandlerContext context => context, 14 | _ => null, 15 | }; 16 | 17 | /// Controller that the handler is attached to. 18 | abstract final Controller controller; 19 | 20 | /// Name of the handler. 21 | abstract final String name; 22 | 23 | /// Future that completes when the handler is done. 24 | Future get done; 25 | 26 | /// Whether the handler is done. 27 | bool get isDone; 28 | 29 | /// Extra meta information about the handler. 30 | abstract final Map meta; 31 | } 32 | 33 | @internal 34 | final class HandlerContextImpl implements HandlerContext { 35 | HandlerContextImpl( 36 | {required this.controller, 37 | required this.name, 38 | required this.meta, 39 | required Completer completer}) 40 | : _completer = completer; 41 | 42 | @override 43 | final Controller controller; 44 | 45 | @override 46 | final String name; 47 | 48 | @override 49 | Future get done => _completer.future; 50 | 51 | @override 52 | bool get isDone => _completer.isCompleted; 53 | 54 | final Completer _completer; 55 | 56 | @override 57 | final Map meta; 58 | } 59 | -------------------------------------------------------------------------------- /lib/src/registry.dart: -------------------------------------------------------------------------------- 1 | import 'package:control/src/controller.dart'; 2 | import 'package:control/src/state_controller.dart'; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:meta/meta.dart'; 5 | 6 | /// StateRegistry Singleton class 7 | /// Used to register and retrieve the state controllers at debug mode 8 | /// to track the state of the controllers and leaks in the application. 9 | @internal 10 | final class ControllerRegistry with ControllerRegistry$Global { 11 | factory ControllerRegistry() => _internalSingleton; 12 | ControllerRegistry._internal(); 13 | static final ControllerRegistry _internalSingleton = 14 | ControllerRegistry._internal(); 15 | } 16 | 17 | @internal 18 | base mixin ControllerRegistry$Global { 19 | late final Map>> _globalRegistry = 20 | >>{}; 21 | 22 | @internal 23 | List getAll() { 24 | if (!kDebugMode) return const []; 25 | final result = []; 26 | for (final list in _globalRegistry.values) { 27 | var j = 0; 28 | for (var i = 0; i < list.length; i++) { 29 | final wr = list[i]; 30 | final target = wr.target; 31 | if (target == null || target.isDisposed) continue; 32 | if (i != j) list[j] = wr; 33 | if (target is Controller) result.add(target); 34 | j++; 35 | } 36 | list.length = j; 37 | } 38 | return result; 39 | } 40 | 41 | /// Get the controller from the registry. 42 | @internal 43 | List get() { 44 | if (!kDebugMode) return []; 45 | final result = []; 46 | final list = _globalRegistry[C]; 47 | if (list == null) return result; 48 | var j = 0; 49 | for (var i = 0; i < list.length; i++) { 50 | final wr = list[i]; 51 | final target = wr.target; 52 | if (target == null || target.isDisposed) continue; 53 | if (i != j) list[j] = wr; 54 | if (target is C) result.add(target); 55 | j++; 56 | } 57 | list.length = j; 58 | return result; 59 | } 60 | 61 | /// Upsert the controller in the registry. 62 | @internal 63 | void insert(Controller controller) { 64 | if (!kDebugMode) return; 65 | remove(); 66 | (_globalRegistry[Controller] ??= >[]) 67 | .add(WeakReference(controller)); 68 | } 69 | 70 | /// Remove the controller from the registry. 71 | @internal 72 | void remove() { 73 | if (!kDebugMode) return; 74 | final list = _globalRegistry[Controller]; 75 | if (list == null) return; 76 | var j = 0; 77 | for (var i = 0; i < list.length; i++) { 78 | if (list[i] is WeakReference) continue; 79 | if (i != j) list[j] = list[i]; 80 | j++; 81 | } 82 | list.length = j; 83 | } 84 | } 85 | -------------------------------------------------------------------------------- /lib/src/state_consumer.dart: -------------------------------------------------------------------------------- 1 | import 'package:control/src/controller_scope.dart'; 2 | import 'package:control/src/state_controller.dart'; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/scheduler.dart'; 5 | import 'package:flutter/widgets.dart'; 6 | 7 | /// Fire when the state changes. 8 | typedef StateConsumerListener, S extends Object> 9 | = void Function(BuildContext context, C controller, S previous, S current); 10 | 11 | /// Build when the method returns true. 12 | typedef StateConsumerCondition = bool Function( 13 | S previous, S current); 14 | 15 | /// Rebuild the widget when the state changes. 16 | typedef StateConsumerBuilder = Widget Function( 17 | BuildContext context, S state, Widget? child); 18 | 19 | /// {@template state_consumer} 20 | /// StateConsumer widget. 21 | /// 22 | /// Call [listener] and rebuild with [builder] when the state changes. 23 | /// {@endtemplate} 24 | class StateConsumer, S extends Object> 25 | extends StatefulWidget { 26 | /// {@macro state_builder} 27 | const StateConsumer({ 28 | this.controller, 29 | this.listener, 30 | this.buildWhen, 31 | this.builder, 32 | this.child, 33 | super.key, 34 | }); 35 | 36 | /// The controller responsible for processing the logic. 37 | /// If omitted, the controller will be obtained 38 | /// using `ControllerScope.of(context)`. 39 | final C? controller; 40 | 41 | /// Takes the `BuildContext` along with the `state` 42 | /// and is responsible for executing in response to `state` changes. 43 | final StateConsumerListener? listener; 44 | 45 | /// Takes the previous `state` and the current `state` and is responsible for 46 | /// returning a [bool] which determines whether or not to trigger 47 | /// [builder] with the current `state`. 48 | final StateConsumerCondition? buildWhen; 49 | 50 | /// The [builder] function which will be invoked on each widget build. 51 | /// The [builder] takes the `BuildContext` and current `state` and 52 | /// must return a widget. 53 | /// This is analogous to the [builder] function in [StreamBuilder]. 54 | final StateConsumerBuilder? builder; 55 | 56 | /// The child widget which will be passed to the [builder]. 57 | final Widget? child; 58 | 59 | @override 60 | State createState() => _StateConsumerState(); 61 | } 62 | 63 | class _StateConsumerState, S extends Object> 64 | extends State> { 65 | late C _controller; 66 | late S _previousState; 67 | 68 | @override 69 | void didChangeDependencies() { 70 | _controller = 71 | widget.controller ?? ControllerScope.of(context, listen: false); 72 | _previousState = _controller.state; 73 | _subscribe(); 74 | super.didChangeDependencies(); 75 | } 76 | 77 | @override 78 | void didUpdateWidget(covariant StateConsumer oldWidget) { 79 | super.didUpdateWidget(oldWidget); 80 | final oldController = oldWidget.controller, 81 | newController = widget.controller; 82 | if (identical(oldController, newController) || 83 | oldController == newController) return; 84 | _unsubscribe(); 85 | _controller = 86 | newController ?? ControllerScope.of(context, listen: false); 87 | _previousState = _controller.state; 88 | _subscribe(); 89 | } 90 | 91 | @override 92 | void dispose() { 93 | _unsubscribe(); 94 | super.dispose(); 95 | } 96 | 97 | void _subscribe() => _controller.addListener(_valueChanged); 98 | 99 | void _unsubscribe() { 100 | if (_controller.isDisposed) return; 101 | _controller.removeListener(_valueChanged); 102 | } 103 | 104 | void _valueChanged() { 105 | final oldState = _previousState, newState = _controller.state; 106 | if (!mounted || identical(oldState, newState)) return; 107 | _previousState = newState; 108 | widget.listener?.call(context, _controller, oldState, newState); 109 | if (widget.buildWhen?.call(oldState, newState) ?? true) { 110 | // Rebuild the widget when the state changes. 111 | switch (SchedulerBinding.instance.schedulerPhase) { 112 | case SchedulerPhase.idle: 113 | case SchedulerPhase.transientCallbacks: 114 | case SchedulerPhase.postFrameCallbacks: 115 | setState(() {}); 116 | case SchedulerPhase.persistentCallbacks: 117 | case SchedulerPhase.midFrameMicrotasks: 118 | SchedulerBinding.instance.addPostFrameCallback((_) { 119 | if (!mounted) return; 120 | setState(() {}); 121 | }); 122 | } 123 | } 124 | } 125 | 126 | @override 127 | void debugFillProperties(DiagnosticPropertiesBuilder properties) => 128 | super.debugFillProperties(properties 129 | ..add( 130 | DiagnosticsProperty>('Controller', _controller)) 131 | ..add(DiagnosticsProperty('State', _controller.state)) 132 | ..add(FlagProperty('isProcessing', 133 | value: _controller.isProcessing, 134 | ifTrue: 'Processing', 135 | ifFalse: 'Idle'))); 136 | 137 | @override 138 | Widget build(BuildContext context) => 139 | widget.builder?.call(context, _controller.state, widget.child) ?? 140 | widget.child ?? 141 | const SizedBox.shrink(); 142 | } 143 | -------------------------------------------------------------------------------- /lib/src/state_controller.dart: -------------------------------------------------------------------------------- 1 | import 'dart:async'; 2 | 3 | import 'package:control/src/controller.dart'; 4 | import 'package:control/src/handler_context.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:meta/meta.dart'; 7 | 8 | /// Selector from [StateController] 9 | typedef StateControllerSelector = Value Function( 10 | S state); 11 | 12 | /// Filter for [StateController] 13 | typedef StateControllerFilter = bool Function(Value prev, Value next); 14 | 15 | /// State controller 16 | /// 17 | /// Do not implement this interface directly, instead extend [StateController]. 18 | /// 19 | @internal 20 | abstract interface class IStateController 21 | implements IController { 22 | /// The current state of the controller. 23 | S get state; 24 | } 25 | 26 | /// State controller 27 | abstract base class StateController extends Controller 28 | implements IStateController { 29 | /// State controller 30 | StateController({required S initialState}) : _$state = initialState; 31 | 32 | /// Get the handler's context from the current zone. 33 | static HandlerContext? get context => HandlerContext.zoned(); 34 | 35 | @override 36 | @nonVirtual 37 | S get state => _$state; 38 | S _$state; 39 | 40 | /// Emit a new state, usually based on [state] and some additional logic. 41 | @protected 42 | @mustCallSuper 43 | void setState(S state) { 44 | runZonedGuarded( 45 | () => Controller.observer?.onStateChanged(this, _$state, state), 46 | (error, stackTrace) {/* ignore */}, // coverage:ignore-line 47 | ); 48 | _$state = state; 49 | if (isDisposed) return; 50 | notifyListeners(); 51 | } 52 | 53 | // --- Helper --- // 54 | 55 | late final List> _$streamControllers = 56 | >[]; 57 | 58 | /// Returns a [ValueListenable] view of this controller's state. 59 | ValueListenable toValueListenable() => 60 | _StateController$ValueListenableView(this); 61 | 62 | /// Returns a [Stream] of state changes. 63 | Stream toStream() { 64 | final controller = StreamController(); 65 | _$streamControllers.add(controller); 66 | void listener() => controller.add(state); 67 | addListener(listener); 68 | controller.onCancel = () { 69 | _$streamControllers.remove(controller); 70 | if (isDisposed) return; 71 | removeListener(listener); 72 | }; 73 | return controller.stream; 74 | } 75 | 76 | /// Transform [StateController] in to [ValueListenable] 77 | /// using [selector] with optional [test] filter. 78 | /// 79 | /// The [selector] is called with the current [StateController] and 80 | /// returns a value derived from [StateController.state]. 81 | ValueListenable select( 82 | StateControllerSelector selector, [ 83 | StateControllerFilter? test, 84 | ]) => 85 | _StateController$ValueListenableSelect(this, selector, test); 86 | 87 | @override 88 | void dispose() { 89 | for (final controller in _$streamControllers) controller.close(); 90 | _$streamControllers.length = 0; 91 | scheduleMicrotask(() {}); 92 | super.dispose(); 93 | } 94 | } 95 | 96 | final class _StateController$ValueListenableView 97 | implements ValueListenable { 98 | _StateController$ValueListenableView(this._controller); 99 | 100 | final IStateController _controller; 101 | 102 | @override 103 | S get value => _controller.state; 104 | 105 | @override 106 | void addListener(VoidCallback listener) => _controller.addListener(listener); 107 | 108 | @override 109 | void removeListener(VoidCallback listener) => 110 | _controller.removeListener(listener); 111 | } 112 | 113 | final class _StateController$ValueListenableSelect 114 | with ChangeNotifier 115 | implements ValueListenable { 116 | _StateController$ValueListenableSelect( 117 | this._controller, 118 | this._selector, 119 | this._test, 120 | ); 121 | 122 | final IStateController _controller; 123 | final StateControllerSelector _selector; 124 | final StateControllerFilter? _test; 125 | bool get _isDisposed => _controller.isDisposed; 126 | bool _subscribed = false; 127 | 128 | late Value _$value = _selector(_controller.state); 129 | 130 | @override 131 | Value get value => 132 | _subscribed ? _$value : _$value = _selector(_controller.state); 133 | 134 | void _update() { 135 | final newValue = _selector(_controller.state); 136 | if (identical(_$value, newValue)) return; 137 | if (!(_test?.call(_$value, newValue) ?? true)) return; 138 | _$value = newValue; 139 | notifyListeners(); 140 | } 141 | 142 | @override 143 | void addListener(VoidCallback listener) { 144 | if (_isDisposed) return; 145 | if (!_subscribed) { 146 | _$value = _selector(_controller.state); 147 | _controller.addListener(_update); 148 | _subscribed = true; 149 | } 150 | super.addListener(listener); 151 | } 152 | 153 | @override 154 | void removeListener(VoidCallback listener) { 155 | super.removeListener(listener); 156 | if (_isDisposed) return; 157 | if (!hasListeners && _subscribed) { 158 | _controller.removeListener(_update); 159 | _subscribed = false; 160 | } 161 | } 162 | 163 | @override 164 | void dispose() { 165 | _controller.removeListener(_update); 166 | _subscribed = false; 167 | super.dispose(); 168 | } 169 | } 170 | -------------------------------------------------------------------------------- /pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: control 2 | description: "Simple state management for Flutter with concurrency support." 3 | 4 | version: 0.2.0 5 | 6 | homepage: https://github.com/PlugFox/control 7 | 8 | repository: https://github.com/PlugFox/control 9 | 10 | issue_tracker: https://github.com/PlugFox/control/issues 11 | 12 | funding: 13 | - https://www.buymeacoffee.com/plugfox 14 | - https://www.patreon.com/plugfox 15 | - https://boosty.to/plugfox 16 | 17 | topics: 18 | - architecture 19 | - state-management 20 | - state 21 | - concurrency 22 | - controller 23 | 24 | platforms: 25 | android: 26 | ios: 27 | linux: 28 | macos: 29 | web: 30 | windows: 31 | 32 | #screenshots: 33 | # - description: 'Example of using the package' 34 | # path: example.png 35 | 36 | environment: 37 | sdk: '>=3.4.0 <4.0.0' 38 | flutter: ">=3.16.0" 39 | 40 | dependencies: 41 | flutter: 42 | sdk: flutter 43 | meta: ^1.0.0 44 | 45 | dev_dependencies: 46 | flutter_test: 47 | sdk: flutter 48 | flutter_lints: ^5.0.0 49 | -------------------------------------------------------------------------------- /test/control_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: unnecessary_lambdas 2 | 3 | import 'package:flutter_test/flutter_test.dart'; 4 | 5 | import 'unit/handler_context_test.dart' as handler_context_test; 6 | import 'unit/state_controller_test.dart' as state_controller_test; 7 | import 'widget/controller_scope_test.dart' as state_scope_test; 8 | import 'widget/state_consumer_test.dart' as state_consumer_test; 9 | 10 | void main() { 11 | group('unit', () { 12 | state_controller_test.main(); 13 | handler_context_test.main(); 14 | }); 15 | 16 | group('widget', () { 17 | state_scope_test.main(); 18 | state_consumer_test.main(); 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /test/unit/handler_context_test.dart: -------------------------------------------------------------------------------- 1 | import 'dart:math' as math; 2 | 3 | import 'package:control/control.dart'; 4 | import 'package:flutter_test/flutter_test.dart'; 5 | 6 | void main() => group('HandlerContext', () { 7 | test('FakeControllers', () async { 8 | final controllers = <_FakeControllerBase>[ 9 | _FakeControllerSequential(), 10 | _FakeControllerDroppable(), 11 | _FakeControllerConcurrent(), 12 | ]; 13 | for (final controller in controllers) { 14 | final observer = Controller.observer = _FakeControllerObserver(); 15 | expect(controller.isProcessing, isFalse); 16 | expect(observer.lastContext, isNull); 17 | expect(observer.lastStateContext, isNull); 18 | expect(observer.lastErrorContext, isNull); 19 | expect(Controller.context, isNull); 20 | 21 | // After the normal event is called, the context should be available. 22 | final value = math.Random().nextDouble(); 23 | HandlerContext? lastContext; 24 | controller.event( 25 | meta: {'double': value}, 26 | out: (ctx) => lastContext = ctx, 27 | ).ignore(); 28 | expect(controller.isProcessing, isTrue); 29 | expect(lastContext, isNotNull); 30 | await expectLater(lastContext!.done, completes); 31 | // Event should be done by now. 32 | expect(lastContext!.isDone, isTrue); 33 | expect( 34 | lastContext, 35 | allOf( 36 | isNotNull, 37 | same(observer.lastContext), 38 | same(observer.lastStateContext), 39 | isA() 40 | .having( 41 | (ctx) => ctx.name, 42 | 'name', 43 | 'event', 44 | ) 45 | .having( 46 | (ctx) => ctx.meta['double'], 47 | 'meta should contain double', 48 | equals(value), 49 | ) 50 | .having( 51 | (ctx) => ctx.meta['started_at'], 52 | 'meta should contain started_at', 53 | allOf( 54 | isNotNull, 55 | isA(), 56 | ), 57 | ) 58 | .having( 59 | (ctx) => ctx.meta['duration'], 60 | 'meta should contain duration', 61 | allOf( 62 | isNotNull, 63 | isA(), 64 | isNot(Duration.zero), 65 | ), 66 | ) 67 | .having( 68 | (ctx) => ctx.controller, 69 | 'controller', 70 | same(controller), 71 | ) 72 | .having( 73 | (ctx) => ctx.isDone, 74 | 'isDone', 75 | isTrue, 76 | ), 77 | ), 78 | ); 79 | expect(observer.lastErrorContext, isNull); 80 | expect(Controller.context, isNull); 81 | 82 | controller.dispose(); 83 | } 84 | }); 85 | }); 86 | 87 | final class _FakeControllerObserver implements IControllerObserver { 88 | HandlerContext? lastContext; 89 | HandlerContext? lastStateContext; 90 | HandlerContext? lastErrorContext; 91 | 92 | @override 93 | void onCreate(Controller controller) {/* ignore */} 94 | 95 | @override 96 | void onDispose(Controller controller) {/* ignore */} 97 | 98 | @override 99 | void onHandler(HandlerContext context) { 100 | lastContext = context; 101 | } 102 | 103 | @override 104 | void onStateChanged( 105 | StateController controller, 106 | S prevState, 107 | S nextState, 108 | ) { 109 | lastStateContext = Controller.context; 110 | } 111 | 112 | @override 113 | void onError(Controller controller, Object error, StackTrace stackTrace) { 114 | lastErrorContext = Controller.context; 115 | } 116 | } 117 | 118 | abstract base class _FakeControllerBase extends StateController { 119 | _FakeControllerBase() : super(initialState: false); 120 | 121 | Future event({ 122 | Map? meta, 123 | void Function(HandlerContext context)? out, 124 | }) => 125 | handle( 126 | () async { 127 | out?.call(Controller.context!); 128 | final stopwatch = Stopwatch()..start(); 129 | try { 130 | setState(false); 131 | await Future.delayed(Duration.zero); 132 | () { 133 | out?.call(Controller.context!); 134 | }(); 135 | setState(true); 136 | Controller.context?.meta['duration'] = stopwatch.elapsed; 137 | } finally { 138 | stopwatch.stop(); 139 | } 140 | }, 141 | name: 'event', 142 | meta: { 143 | ...?meta, 144 | 'started_at': DateTime.now(), 145 | }, 146 | ); 147 | } 148 | 149 | final class _FakeControllerSequential = _FakeControllerBase 150 | with SequentialControllerHandler; 151 | 152 | final class _FakeControllerDroppable = _FakeControllerBase 153 | with DroppableControllerHandler; 154 | 155 | final class _FakeControllerConcurrent = _FakeControllerBase 156 | with ConcurrentControllerHandler; 157 | -------------------------------------------------------------------------------- /test/util/test_util.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: avoid_classes_with_only_static_members 2 | 3 | import 'package:control/control.dart'; 4 | import 'package:flutter/material.dart'; 5 | 6 | abstract final class TestUtil { 7 | /// Basic wrapper for the current widgets. 8 | static Widget appContext({required Widget child, Size? size}) => MediaQuery( 9 | data: MediaQueryData(size: size ?? const Size(800, 600)), 10 | child: Directionality( 11 | textDirection: TextDirection.ltr, 12 | child: Material( 13 | elevation: 0, 14 | child: DefaultSelectionStyle( 15 | child: ScaffoldMessenger( 16 | child: HeroControllerScope.none( 17 | child: Navigator( 18 | pages: >[ 19 | MaterialPage( 20 | child: Scaffold( 21 | body: SafeArea( 22 | child: Center( 23 | child: child, 24 | ), 25 | ), 26 | ), 27 | ), 28 | ], 29 | onDidRemovePage: (route) => route.canPop, 30 | ), 31 | ), 32 | ), 33 | ), 34 | ), 35 | ), 36 | ); 37 | } 38 | 39 | /// Base fake controller for testing. 40 | final class FakeController extends StateController 41 | with SequentialControllerHandler { 42 | FakeController({int? initialState}) : super(initialState: initialState ?? 0); 43 | 44 | void add(int value) => handle(() async { 45 | await Future.delayed(Duration.zero); 46 | setState(state + value); 47 | }); 48 | 49 | void subtract(int value) => handle(() async { 50 | await Future.delayed(Duration.zero); 51 | setState(state - value); 52 | }); 53 | } 54 | --------------------------------------------------------------------------------