From 9e86097c7f85a00e85f690461ab3979483807e86 Mon Sep 17 00:00:00 2001 From: Anton Borries Date: Sat, 20 Jul 2024 16:55:57 +0200 Subject: [PATCH] chore: Move to package structure and introduce Melos (#282) --- .github/workflows/flutter-beta.yml | 29 +- .github/workflows/main.yml | 29 +- README.md | 676 +----------------- .../HomeWidgetBackgroundReceiver.kt | 15 - .../HomeWidgetBackgroundService.kt | 91 --- .../home_widget/HomeWidgetGlanceState.kt | 32 - .../HomeWidgetGlanceWidgetReceiver.kt | 29 - .../home_widget/HomeWidgetIntent.kt | 62 -- .../home_widget/HomeWidgetPlugin.kt | 282 -------- .../home_widget/HomeWidgetProvider.kt | 16 - .../HomeWidgetExampleProvider.kt | 57 -- .../glance/HomeWidgetGlanceAppWidget.kt | 93 --- .../flutter/generated_plugin_registrant.cc | 11 - .../flutter/generated_plugin_registrant.h | 15 - example/linux/flutter/generated_plugins.cmake | 23 - melos.yaml | 27 + packages/home_widget/.gitignore | 135 ++++ .metadata => packages/home_widget/.metadata | 0 .../home_widget/CHANGELOG.md | 0 LICENSE => packages/home_widget/LICENSE | 0 packages/home_widget/README.md | 675 +++++++++++++++++ .../home_widget/analysis_options.yaml | 0 .../home_widget/android}/.gitignore | 0 .../home_widget/android}/build.gradle | 0 .../home_widget/android}/gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 .../home_widget/android}/settings.gradle | 0 .../android}/src/main/AndroidManifest.xml | 0 .../HomeWidgetBackgroundReceiver.kt | 15 + .../HomeWidgetBackgroundService.kt | 87 +++ .../home_widget/HomeWidgetGlanceState.kt | 38 + .../HomeWidgetGlanceWidgetReceiver.kt | 38 + .../home_widget/HomeWidgetIntent.kt | 66 ++ .../home_widget/HomeWidgetPlugin.kt | 314 ++++++++ .../home_widget/HomeWidgetProvider.kt | 25 + .../home_widget/dart_test.yaml | 0 .../home_widget/example}/.gitignore | 0 .../home_widget/example}/.metadata | 12 - .../home_widget/example}/README.md | 0 .../example}/analysis_options.yaml | 0 .../home_widget/example}/android/.gitignore | 0 .../example}/android/app/build.gradle | 0 .../example}/android/app/proguard-rules.pro | 0 .../android/app/src/debug/AndroidManifest.xml | 0 .../android/app/src/main/AndroidManifest.xml | 0 .../HomeWidgetExampleProvider.kt | 61 ++ .../home_widget_example/MainActivity.kt | 3 +- .../glance/HomeWidgetGlanceAppWidget.kt | 98 +++ .../glance/HomeWidgetReceiver.kt | 4 +- .../main/res/drawable/launch_background.xml | 0 .../main/res/drawable/widget_background.xml | 0 .../src/main/res/layout/example_layout.xml | 0 .../src/main/res/mipmap-hdpi/ic_launcher.png | Bin .../src/main/res/mipmap-mdpi/ic_launcher.png | Bin .../src/main/res/mipmap-xhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxhdpi/ic_launcher.png | Bin .../main/res/mipmap-xxxhdpi/ic_launcher.png | Bin .../app/src/main/res/values/styles.xml | 0 .../src/main/res/xml/home_widget_example.xml | 0 .../res/xml/home_widget_glance_example.xml | 0 .../app/src/profile/AndroidManifest.xml | 0 .../home_widget/example}/android/build.gradle | 0 .../example}/android/gradle.properties | 0 .../gradle/wrapper/gradle-wrapper.properties | 0 .../example}/android/settings.gradle | 0 .../integration_test/android_test.dart | 0 .../example}/integration_test/ios_test.dart | 0 .../home_widget/example}/ios/.gitignore | 0 .../ios/Flutter/AppFrameworkInfo.plist | 0 .../example}/ios/Flutter/Debug.xcconfig | 0 .../example}/ios/Flutter/Release.xcconfig | 0 .../AccentColor.colorset/Contents.json | 0 .../AppIcon.appiconset/Contents.json | 0 .../Assets.xcassets/Contents.json | 0 .../WidgetBackground.colorset/Contents.json | 0 .../HomeWidgetExample/HomeWidgetExample.swift | 10 +- .../example}/ios/HomeWidgetExample/Info.plist | 0 .../HomeWidgetExampleExtension.entitlements | 0 .../home_widget/example}/ios/Podfile | 0 .../ios/Runner.xcodeproj/project.pbxproj | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../xcshareddata/xcschemes/Runner.xcscheme | 0 .../xcshareddata/IDEWorkspaceChecks.plist | 0 .../xcshareddata/WorkspaceSettings.xcsettings | 0 .../example}/ios/Runner/AppDelegate.swift | 0 .../AppIcon.appiconset/Contents.json | 0 .../Icon-App-1024x1024@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@1x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@2x.png | Bin .../AppIcon.appiconset/Icon-App-20x20@3x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@1x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@2x.png | Bin .../AppIcon.appiconset/Icon-App-29x29@3x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@1x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@2x.png | Bin .../AppIcon.appiconset/Icon-App-40x40@3x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@2x.png | Bin .../AppIcon.appiconset/Icon-App-60x60@3x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@1x.png | Bin .../AppIcon.appiconset/Icon-App-76x76@2x.png | Bin .../Icon-App-83.5x83.5@2x.png | Bin .../LaunchImage.imageset/Contents.json | 0 .../LaunchImage.imageset/LaunchImage.png | Bin .../LaunchImage.imageset/LaunchImage@2x.png | Bin .../LaunchImage.imageset/LaunchImage@3x.png | Bin .../LaunchImage.imageset/README.md | 0 .../ios/Runner/BackgroundIntent.swift | 0 .../Runner/Base.lproj/LaunchScreen.storyboard | 0 .../ios/Runner/Base.lproj/Main.storyboard | 0 .../example}/ios/Runner/Info.plist | 0 .../ios/Runner/Runner-Bridging-Header.h | 0 .../example}/ios/Runner/Runner.entitlements | 0 .../ios/RunnerTests/RunnerTests.swift | 0 .../home_widget/example}/lib/main.dart | 0 .../home_widget/example}/pubspec.yaml | 0 .../test_driver/integration_test.dart | 0 {ios => packages/home_widget/ios}/.gitignore | 0 .../home_widget/ios}/Assets/.gitkeep | 0 .../Classes/HomeWidgetBackgroundWorker.swift | 0 .../ios}/Classes/HomeWidgetPlugin.h | 0 .../ios}/Classes/HomeWidgetPlugin.m | 0 .../ios}/Classes/SwiftHomeWidgetPlugin.swift | 12 +- .../home_widget/ios}/home_widget.podspec | 0 .../home_widget/lib}/home_widget.dart | 0 .../home_widget/lib}/src/home_widget.dart | 0 .../src/home_widget_callback_dispatcher.dart | 0 .../lib}/src/home_widget_info.dart | 0 packages/home_widget/pubspec.yaml | 32 + .../home_widget/test}/background_test.dart | 0 .../test}/goldens/render-flutter-widget.png | Bin .../test}/home_widget_info_test.dart | 0 .../home_widget/test}/home_widget_test.dart | 0 .../home_widget/test}/mocks.dart | 0 pubspec.yaml | 32 +- 135 files changed, 1669 insertions(+), 1475 deletions(-) mode change 100644 => 120000 README.md delete mode 100644 android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundReceiver.kt delete mode 100644 android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundService.kt delete mode 100644 android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceState.kt delete mode 100644 android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceWidgetReceiver.kt delete mode 100644 android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt delete mode 100644 android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetPlugin.kt delete mode 100644 android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetProvider.kt delete mode 100644 example/android/app/src/main/kotlin/es/antonborri/home_widget_example/HomeWidgetExampleProvider.kt delete mode 100644 example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt delete mode 100644 example/linux/flutter/generated_plugin_registrant.cc delete mode 100644 example/linux/flutter/generated_plugin_registrant.h delete mode 100644 example/linux/flutter/generated_plugins.cmake create mode 100644 melos.yaml create mode 100644 packages/home_widget/.gitignore rename .metadata => packages/home_widget/.metadata (100%) rename CHANGELOG.md => packages/home_widget/CHANGELOG.md (100%) rename LICENSE => packages/home_widget/LICENSE (100%) create mode 100644 packages/home_widget/README.md rename analysis_options.yaml => packages/home_widget/analysis_options.yaml (100%) rename {android => packages/home_widget/android}/.gitignore (100%) rename {android => packages/home_widget/android}/build.gradle (100%) rename {android => packages/home_widget/android}/gradle.properties (100%) rename {android => packages/home_widget/android}/gradle/wrapper/gradle-wrapper.properties (100%) rename {android => packages/home_widget/android}/settings.gradle (100%) rename {android => packages/home_widget/android}/src/main/AndroidManifest.xml (100%) create mode 100644 packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundReceiver.kt create mode 100644 packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundService.kt create mode 100644 packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceState.kt create mode 100644 packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceWidgetReceiver.kt create mode 100644 packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt create mode 100644 packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetPlugin.kt create mode 100644 packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetProvider.kt rename dart_test.yaml => packages/home_widget/dart_test.yaml (100%) rename {example => packages/home_widget/example}/.gitignore (100%) rename {example => packages/home_widget/example}/.metadata (65%) rename {example => packages/home_widget/example}/README.md (100%) rename {example => packages/home_widget/example}/analysis_options.yaml (100%) rename {example => packages/home_widget/example}/android/.gitignore (100%) rename {example => packages/home_widget/example}/android/app/build.gradle (100%) rename {example => packages/home_widget/example}/android/app/proguard-rules.pro (100%) rename {example => packages/home_widget/example}/android/app/src/debug/AndroidManifest.xml (100%) rename {example => packages/home_widget/example}/android/app/src/main/AndroidManifest.xml (100%) create mode 100644 packages/home_widget/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/HomeWidgetExampleProvider.kt rename {example => packages/home_widget/example}/android/app/src/main/kotlin/es/antonborri/home_widget_example/MainActivity.kt (69%) create mode 100644 packages/home_widget/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt rename {example => packages/home_widget/example}/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetReceiver.kt (73%) rename {example => packages/home_widget/example}/android/app/src/main/res/drawable/launch_background.xml (100%) rename {example => packages/home_widget/example}/android/app/src/main/res/drawable/widget_background.xml (100%) rename {example => packages/home_widget/example}/android/app/src/main/res/layout/example_layout.xml (100%) rename {example => packages/home_widget/example}/android/app/src/main/res/mipmap-hdpi/ic_launcher.png (100%) rename {example => packages/home_widget/example}/android/app/src/main/res/mipmap-mdpi/ic_launcher.png (100%) rename {example => packages/home_widget/example}/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png (100%) rename {example => packages/home_widget/example}/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png (100%) rename {example => packages/home_widget/example}/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png (100%) rename {example => packages/home_widget/example}/android/app/src/main/res/values/styles.xml (100%) rename {example => packages/home_widget/example}/android/app/src/main/res/xml/home_widget_example.xml (100%) rename {example => packages/home_widget/example}/android/app/src/main/res/xml/home_widget_glance_example.xml (100%) rename {example => packages/home_widget/example}/android/app/src/profile/AndroidManifest.xml (100%) rename {example => packages/home_widget/example}/android/build.gradle (100%) rename {example => packages/home_widget/example}/android/gradle.properties (100%) rename {example => packages/home_widget/example}/android/gradle/wrapper/gradle-wrapper.properties (100%) rename {example => packages/home_widget/example}/android/settings.gradle (100%) rename {example => packages/home_widget/example}/integration_test/android_test.dart (100%) rename {example => packages/home_widget/example}/integration_test/ios_test.dart (100%) rename {example => packages/home_widget/example}/ios/.gitignore (100%) rename {example => packages/home_widget/example}/ios/Flutter/AppFrameworkInfo.plist (100%) rename {example => packages/home_widget/example}/ios/Flutter/Debug.xcconfig (100%) rename {example => packages/home_widget/example}/ios/Flutter/Release.xcconfig (100%) rename {example => packages/home_widget/example}/ios/HomeWidgetExample/Assets.xcassets/AccentColor.colorset/Contents.json (100%) rename {example => packages/home_widget/example}/ios/HomeWidgetExample/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {example => packages/home_widget/example}/ios/HomeWidgetExample/Assets.xcassets/Contents.json (100%) rename {example => packages/home_widget/example}/ios/HomeWidgetExample/Assets.xcassets/WidgetBackground.colorset/Contents.json (100%) rename {example => packages/home_widget/example}/ios/HomeWidgetExample/HomeWidgetExample.swift (91%) rename {example => packages/home_widget/example}/ios/HomeWidgetExample/Info.plist (100%) rename {example => packages/home_widget/example}/ios/HomeWidgetExampleExtension.entitlements (100%) rename {example => packages/home_widget/example}/ios/Podfile (100%) rename {example => packages/home_widget/example}/ios/Runner.xcodeproj/project.pbxproj (100%) rename {example => packages/home_widget/example}/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {example => packages/home_widget/example}/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename {example => packages/home_widget/example}/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme (100%) rename {example => packages/home_widget/example}/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist (100%) rename {example => packages/home_widget/example}/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings (100%) rename {example => packages/home_widget/example}/ios/Runner/AppDelegate.swift (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png (100%) rename {example => packages/home_widget/example}/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md (100%) rename {example => packages/home_widget/example}/ios/Runner/BackgroundIntent.swift (100%) rename {example => packages/home_widget/example}/ios/Runner/Base.lproj/LaunchScreen.storyboard (100%) rename {example => packages/home_widget/example}/ios/Runner/Base.lproj/Main.storyboard (100%) rename {example => packages/home_widget/example}/ios/Runner/Info.plist (100%) rename {example => packages/home_widget/example}/ios/Runner/Runner-Bridging-Header.h (100%) rename {example => packages/home_widget/example}/ios/Runner/Runner.entitlements (100%) rename {example => packages/home_widget/example}/ios/RunnerTests/RunnerTests.swift (100%) rename {example => packages/home_widget/example}/lib/main.dart (100%) rename {example => packages/home_widget/example}/pubspec.yaml (100%) rename {example => packages/home_widget/example}/test_driver/integration_test.dart (100%) rename {ios => packages/home_widget/ios}/.gitignore (100%) rename {ios => packages/home_widget/ios}/Assets/.gitkeep (100%) rename {ios => packages/home_widget/ios}/Classes/HomeWidgetBackgroundWorker.swift (100%) rename {ios => packages/home_widget/ios}/Classes/HomeWidgetPlugin.h (100%) rename {ios => packages/home_widget/ios}/Classes/HomeWidgetPlugin.m (100%) rename {ios => packages/home_widget/ios}/Classes/SwiftHomeWidgetPlugin.swift (95%) rename {ios => packages/home_widget/ios}/home_widget.podspec (100%) rename {lib => packages/home_widget/lib}/home_widget.dart (100%) rename {lib => packages/home_widget/lib}/src/home_widget.dart (100%) rename {lib => packages/home_widget/lib}/src/home_widget_callback_dispatcher.dart (100%) rename {lib => packages/home_widget/lib}/src/home_widget_info.dart (100%) create mode 100644 packages/home_widget/pubspec.yaml rename {test => packages/home_widget/test}/background_test.dart (100%) rename {test => packages/home_widget/test}/goldens/render-flutter-widget.png (100%) rename {test => packages/home_widget/test}/home_widget_info_test.dart (100%) rename {test => packages/home_widget/test}/home_widget_test.dart (100%) rename {test => packages/home_widget/test}/mocks.dart (100%) diff --git a/.github/workflows/flutter-beta.yml b/.github/workflows/flutter-beta.yml index 526ad9f8..99aca360 100644 --- a/.github/workflows/flutter-beta.yml +++ b/.github/workflows/flutter-beta.yml @@ -1,6 +1,7 @@ name: Build Flutter Beta on: + workflow_dispatch: push: branches: - flutter-beta @@ -15,7 +16,10 @@ concurrency: jobs: quality: name: Quality Checks - runs-on: ubuntu-latest + runs-on: macos-14 + defaults: + run: + working-directory: packages/home_widget steps: - uses: actions/checkout@v2 @@ -23,12 +27,13 @@ jobs: - uses: subosito/flutter-action@v1 with: channel: beta - - name: Get Packages - run: flutter pub get + - uses: bluefireteam/melos-action@v3 - name: Analyze - run: flutter analyze + run: melos analyze + - name: Install Formatters + run: brew install swift-format ktfmt - name: Format - run: dart format . --set-exit-if-changed + run: melos format:all - name: Publishability run: flutter pub publish --dry-run - name: Test @@ -45,11 +50,10 @@ jobs: uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} - - uses: VeryGoodOpenSource/very_good_coverage@v1.2.0 + path: ./packages/home_widget/coverage/lcov.info android: name: Android Integration Tests - needs: quality runs-on: ubuntu-latest steps: @@ -57,6 +61,7 @@ jobs: - uses: subosito/flutter-action@v1 with: channel: beta + - uses: bluefireteam/melos-action@v3 - name: Enable KVM run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules @@ -71,20 +76,22 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: 29 - working-directory: example script: flutter test integration_test/android_test.dart -d emulator-5554 + working-directory: packages/home_widget/example # iOS Test based on https://medium.com/flutter-community/run-flutter-driver-tests-on-github-actions-13c639c7e4ab # by @kate_sheremet ios: name: iOS Integration Tests - needs: quality strategy: matrix: device: - "iPhone 14" fail-fast: false runs-on: macos-14 + defaults: + run: + working-directory: packages/home_widget/example steps: - uses: actions/checkout@v2 - uses: maxim-lobanov/setup-xcode@v1 @@ -93,10 +100,10 @@ jobs: - uses: subosito/flutter-action@v1 with: channel: beta + - uses: bluefireteam/melos-action@v3 - uses: futureware-tech/simulator-action@v1 id: simulator with: model: ${{ matrix.device }} - name: "Run iOS integration tests" - run: flutter test integration_test/ios_test.dart -d ${{steps.simulator.outputs.udid}} - working-directory: example \ No newline at end of file + run: flutter test integration_test/ios_test.dart -d ${{steps.simulator.outputs.udid}} \ No newline at end of file diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 1bbcb4e4..c71f178b 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,6 +1,7 @@ name: Build on: + workflow_dispatch: push: branches: - main @@ -17,7 +18,10 @@ concurrency: jobs: quality: name: Quality Checks - runs-on: ubuntu-latest + runs-on: macos-14 + defaults: + run: + working-directory: packages/home_widget steps: - uses: actions/checkout@v2 @@ -25,12 +29,13 @@ jobs: - uses: subosito/flutter-action@v1 with: channel: stable - - name: Get Packages - run: flutter pub get + - uses: bluefireteam/melos-action@v3 - name: Analyze - run: flutter analyze + run: melos analyze + - name: Install Formatters + run: brew install swift-format ktfmt - name: Format - run: dart format . --set-exit-if-changed + run: melos format:all - name: Publishability run: flutter pub publish --dry-run - name: Test @@ -47,11 +52,10 @@ jobs: uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} - - uses: VeryGoodOpenSource/very_good_coverage@v1.2.0 + path: ./packages/home_widget/coverage/lcov.info android: name: Android Integration Tests - needs: quality runs-on: ubuntu-latest steps: @@ -59,6 +63,7 @@ jobs: - uses: subosito/flutter-action@v1 with: channel: stable + - uses: bluefireteam/melos-action@v3 - name: Enable KVM run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules @@ -73,20 +78,22 @@ jobs: uses: reactivecircus/android-emulator-runner@v2 with: api-level: 29 - working-directory: example script: flutter test integration_test/android_test.dart -d emulator-5554 + working-directory: packages/home_widget/example # iOS Test based on https://medium.com/flutter-community/run-flutter-driver-tests-on-github-actions-13c639c7e4ab # by @kate_sheremet ios: name: iOS Integration Tests - needs: quality strategy: matrix: device: - "iPhone 14" fail-fast: false runs-on: macos-14 + defaults: + run: + working-directory: packages/home_widget/example steps: - uses: actions/checkout@v2 - uses: maxim-lobanov/setup-xcode@v1 @@ -95,10 +102,10 @@ jobs: - uses: subosito/flutter-action@v1 with: channel: stable + - uses: bluefireteam/melos-action@v3 - uses: futureware-tech/simulator-action@v1 id: simulator with: model: ${{ matrix.device }} - name: "Run iOS integration tests" - run: flutter test integration_test/ios_test.dart -d ${{steps.simulator.outputs.udid}} - working-directory: example \ No newline at end of file + run: flutter test integration_test/ios_test.dart -d ${{steps.simulator.outputs.udid}} \ No newline at end of file diff --git a/README.md b/README.md deleted file mode 100644 index c325b116..00000000 --- a/README.md +++ /dev/null @@ -1,675 +0,0 @@ -# Home Widget - -[![Pub](https://img.shields.io/pub/v/home_widget.svg)](https://pub.dartlang.org/packages/home_widget) -[![likes](https://img.shields.io/pub/likes/home_widget)](https://pub.dev/packages/home_widget/score) -[![popularity](https://img.shields.io/pub/popularity/home_widget)](https://pub.dev/packages/home_widget/score) -[![pub points](https://img.shields.io/pub/points/home_widget)](https://pub.dev/packages/home_widget/score) -[![Build](https://github.com/abausg/home_widget/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/ABausG/home_widget/actions/workflows/main.yml?query=branch%3Amain) -[![codecov](https://codecov.io/gh/ABausG/home_widget/branch/main/graph/badge.svg?token=ZXTZOL6KFO)](https://codecov.io/gh/ABausG/home_widget) - -HomeWidget is a Plugin to make it easier to create HomeScreen Widgets on Android and iOS. -HomeWidget does **not** allow writing Widgets with Flutter itself. It still requires writing the Widgets with native code. However, it provides a unified Interface for sending data, retrieving data and updating the Widgets - -| iOS |  Android | -|----------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------| -| | | - -## Platform Setup -In order to work correctly there needs to be some platform specific setup. Check below on how to add support for Android and iOS - -
iOS - -### Add a Widget to your App in Xcode -Add a widget extension by going File > New > Target > Widget Extension - -![Widget Extension](https://github.com/ABausG/home_widget/blob/main/.github/assets/widget_extension.png?raw=true) - - -### Add GroupId -You need to add a groupId to the App and the Widget Extension - -**Note: in order to add groupIds you need a paid Apple Developer Account** - -Go to your [Apple Developer Account](https://developer.apple.com/account/resources/identifiers/list/applicationGroup) and add a new group. -Add this group to your Runner and the Widget Extension inside XCode: Signing & Capabilities > App Groups > +. -(To swap between your App, and the Extension change the Target) - -![Build Targets](https://github.com/ABausG/home_widget/blob/main/.github/assets/target.png?raw=true) - -### Sync CFBundleVersion (optional) -This step is optional, this will sync the widget extension build version with your app version, so you don't get warnings of mismatch version from App Store Connect when uploading your app. - -![Build Phases](https://github.com/ABausG/home_widget/blob/main/.github/assets/build_phases.png?raw=true) - -In your Runner (app) target go to Build Phases > + > New Run Script Phase and add the following script: -```bash -generatedPath="$SRCROOT/Flutter/Generated.xcconfig" - -# Read and trim versionNumber and buildNumber -versionNumber=$(grep FLUTTER_BUILD_NAME "$generatedPath" | cut -d '=' -f2 | xargs) -buildNumber=$(grep FLUTTER_BUILD_NUMBER "$generatedPath" | cut -d '=' -f2 | xargs) - -infoPlistPath="$SRCROOT/HomeExampleWidget/Info.plist" - -# Check and add CFBundleVersion if it does not exist -/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$infoPlistPath" 2>/dev/null -if [ $? != 0 ]; then - /usr/libexec/PlistBuddy -c "Add :CFBundleVersion string $buildNumber" "$infoPlistPath" -else - /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "$infoPlistPath" -fi - -# Check and add CFBundleShortVersionString if it does not exist -/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$infoPlistPath" 2>/dev/null -if [ $? != 0 ]; then - /usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string $versionNumber" "$infoPlistPath" -else - /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $versionNumber" "$infoPlistPath" -fi - -``` - -Replace `HomeExampleWidget` with the name of the widget extension folder that you have created. - - -### Write your Widget -Check the [Example App](example/ios/HomeWidgetExample/HomeWidgetExample.swift) for an Implementation of a Widget. -A more detailed overview on how to write Widgets for iOS 14 can be found on the [Apple Developer documentation](https://developer.apple.com/documentation/swiftui/widget). -In order to access the Data send with Flutter can be access with -```swift -let data = UserDefaults.init(suiteName:"YOUR_GROUP_ID") -``` -
- -
Android (Jetpack Glance) - -### Add Jetpack Glance as a dependency to you app's Gradle File -```groovy -implementation 'androidx.glance:glance-appwidget:LATEST-VERSION' -``` - -### Create Widget Configuration into `android/app/src/main/res/xml` -```xml - - -``` - -### Add WidgetReceiver to AndroidManifest -```xml - - - - - - -``` - -### Create WidgetReceiver - -To get automatic Updates you should extend from [HomeWidgetGlanceWidgetReceiver](android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceWidgetReceiver.kt) - -Your Receiver should then look like this - -```kotlin -package es.antonborri.home_widget_example.glance - -import HomeWidgetGlanceWidgetReceiver - -class HomeWidgetReceiver : HomeWidgetGlanceWidgetReceiver() { - override val glanceAppWidget = HomeWidgetGlanceAppWidget() -} -``` - -### Build Your AppWidget - -```kotlin - -class HomeWidgetGlanceAppWidget : GlanceAppWidget() { - - /** - * Needed for Updating - */ - override val stateDefinition = HomeWidgetGlanceStateDefinition() - - override suspend fun provideGlance(context: Context, id: GlanceId) { - provideContent { - GlanceContent(context, currentState()) - } - } - - @Composable - private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) { - // Use data to access the data you save with - val data = currentState.preferences - - - // Build Your Composable Widget - Column( - ... - } - -``` - -
- -
Android (XML) - -### Create Widget Layout inside `android/app/src/main/res/layout` - -### Create Widget Configuration into `android/app/src/main/res/xml` -```xml - - - -``` - -### Add WidgetReceiver to AndroidManifest -```xml - - - - - - -``` - -### Write your WidgetProvider -For convenience, you can extend from [HomeWidgetProvider](android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetProvider.kt) which gives you access to a SharedPreferences Object with the Data in the `onUpdate` method. -In case you don't want to use the convenience Method you can access the Data using -```kotlin -import es.antonborri.home_widget.HomeWidgetPlugin -... -HomeWidgetPlugin.getData(context) -``` -which will give you access to the same SharedPreferences - -### More Information -For more Information on how to create and configure Android Widgets, check out [this guide](https://developer.android.com/develop/ui/views/appwidgets) on the Android Developers Page. - -
- -## Usage - -### Setup -
iOS - -For iOS, you need to call `HomeWidget.setAppGroupId('YOUR_GROUP_ID');` -Without this you won't be able to share data between your App and the Widget and calls to `saveWidgetData` and `getWidgetData` will return an error - -
- -### Save Data -In order to save Data call `HomeWidget.saveWidgetData('id', data)` - -### Update a Widget -In order to force a reload of the HomeScreenWidget you need to call -```dart -HomeWidget.updateWidget( - name: 'HomeWidgetExampleProvider', - androidName: 'HomeWidgetExampleProvider', - iOSName: 'HomeWidgetExample', - qualifiedAndroidName: 'com.example.app.HomeWidgetExampleProvider', -); -``` - -The name for Android will be chosen by checking `qualifiedAndroidName`, falling back to `.androidName` and if that was not provided it will fallback to `.name`. -This Name needs to be equal to the Classname of the [WidgetProvider](#Write-your-Widget) - -The name for iOS will be chosen by checking `iOSName` if that was not provided it will fallback to `name`. -This name needs to be equal to the Kind specified in you Widget - -#### Android (Jetpack Glance) - -If you followed the guide and use `HomeWidgetGlanceWidgetReceiver` as your Receiver, `HomeWidgetGlanceStateDefinition` as the AppWidgetStateDefinition, `currentState()` in the composable view and `currentState.preferences` for data access. No further work is necessary. - -#### Android (XML) -Calling `HomeWidget.updateWidget` only notifies the specified provider. -To update widgets using this provider, -update them from the provider like this: - -```kotlin -class HomeWidgetExampleProvider : HomeWidgetProvider() { - - override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, widgetData: SharedPreferences) { - appWidgetIds.forEach { widgetId -> - val views = RemoteViews(context.packageName, R.layout.example_layout).apply { - // ... - } - - // Update widget. - appWidgetManager.updateAppWidget(widgetId, views) - } - } -} -``` - - -### Retrieve Data -To retrieve the current Data saved in the Widget call `HomeWidget.getWidgetData('id', defaultValue: data)` - -### Interactive Widgets - -Android and iOS (starting with iOS 17) allow widgets to have interactive Elements like Buttons - -
Dart - -1. Write a **static** function that takes a Uri as an argument. This will get called when a user clicks on the View - ```dart - @pragma("vm:entry-point") - FutureOr backgroundCallback(Uri data) async { - // do something with data - ... - } - ``` - `@pragma('vm:entry-point')` must be placed above the `callback` function to avoid tree shaking in release mode. - -2. Register the callback function by calling - ```dart - HomeWidget.registerInteractivityCallback(backgroundCallback); - ``` -
- -
iOS - -1. Adjust your Podfile to add `home_widget` as a dependency to your WidgetExtension - ```rb - target 'YourWidgetExtension' do - use_frameworks! - use_modular_headers! - - pod 'home_widget', :path => '.symlinks/plugins/home_widget/ios' - end - ``` -2. To be able to use plugins with the Background Callback add this to your AppDelegate's `application` function - ```swift - if #available(iOS 17, *) { - HomeWidgetBackgroundWorker.setPluginRegistrantCallback { registry in - GeneratedPluginRegistrant.register(with: registry) - } - } - ``` -3. Create a custom `AppIntent` in your App Target (Runner) and make sure to select both your App and your WidgetExtension in the Target Membership panel - - ![Target Membership](https://github.com/ABausG/home_widget/blob/main/.github/assets/target_membership.png?raw=true) - - In this Intent you should import `home_widget` and call `HomeWidgetBackgroundWorker.run(url: url, appGroup: appGroup!)` in the perform method. `url` and `appGroup` can be either hardcoded or set as parameters from the Widget - ```swift - import AppIntents - import Flutter - import Foundation - import home_widget - - @available(iOS 16, *) - public struct BackgroundIntent: AppIntent { - static public var title: LocalizedStringResource = "HomeWidget Background Intent" - - @Parameter(title: "Widget URI") - var url: URL? - - @Parameter(title: "AppGroup") - var appGroup: String? - - public init() {} - - public init(url: URL?, appGroup: String?) { - self.url = url - self.appGroup = appGroup - } - - public func perform() async throws -> some IntentResult { - await HomeWidgetBackgroundWorker.run(url: url, appGroup: appGroup!) - - return .result() - } - } - ``` -4. Add a Button to your Widget. This Button might be encapsulated by a Version check. Pass in an instance of the `AppIntent` created in the previous step - ```swift - Button( - intent: BackgroundIntent( - url: URL(string: "homeWidgetExample://titleClicked"), appGroup: widgetGroupId) - ) { - Text(entry.title).bold().font( /*@START_MENU_TOKEN@*/.title /*@END_MENU_TOKEN@*/) - }.buttonStyle(.plain) - ``` -5. With the current setup the Widget is now Interactive as long as the App is still in the background. If you want to have the Widget be able to wake the App up you need to add the following to your `AppIntent` file - ```swift - @available(iOS 16, *) - @available(iOSApplicationExtension, unavailable) - extension BackgroundIntent: ForegroundContinuableIntent {} - ``` - This code tells the system to always perform the Intent in the App and not in a process attached to the Widget. Note however that this will start your Flutter App using the normal main entrypoint meaning your full app might be run in the background. To counter this you should add checks in the very first Widget you build inside `runApp` to only perform necessary calls/setups while the App is launched in the background -
- - -
Android Jetpack Glance - -1. Add the necessary Receiver and Service to your `AndroidManifest.xml` file - ``` - - - - - - - ``` -2. Create a custom Action - ```kotlin - class InteractiveAction : ActionCallback { - override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { - val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(context, Uri.parse("homeWidgetExample://titleClicked")) - backgroundIntent.send() - } - } - ``` -3. Add the Action as a modifier to a view - ```kotlin - Text( - title, - style = TextStyle(fontSize = 36.sp, fontWeight = FontWeight.Bold), - modifier = GlanceModifier.clickable(onClick = actionRunCallback()), - ) - ``` - -
- -
Android XML - -1. Add the necessary Receiver and Service to your `AndroidManifest.xml` file - ``` - - - - - - - ``` -2. Add a `HomeWidgetBackgroundIntent.getBroadcast` PendingIntent to the View you want to add a click listener to - ```kotlin - val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast( - context, - Uri.parse("homeWidgetExample://titleClicked") - ) - setOnClickPendingIntent(R.id.widget_title, backgroundIntent) - ``` -
- -### Using images of Flutter widgets - -In some cases, you may not want to rewrite UI code in the native frameworks for your widgets. - -
Dart -For example, say you have a chart in your Flutter app configured with `CustomPaint`: - -```dart -class LineChart extends StatelessWidget { - const LineChart({ - super.key, - }); - - @override - Widget build(BuildContext context) { - return CustomPaint( - painter: LineChartPainter(), - child: const SizedBox( - height: 200, - width: 200, - ), - ); - } -} -``` - -Screenshot 2023-06-07 at 12 33 44 PM - -Rewriting the code to create this chart on both Android and iOS might be time consuming. -Instead, you can generate a png file of the Flutter widget and save it to a shared container -between your Flutter app and the home screen widget. - -```dart -var path = await HomeWidget.renderFlutterWidget( - const LineChart(), - key: 'lineChart', - logicalSize: const Size(400, 400), -); -``` -- `LineChart()` is the widget that will be rendered as an image. -- `key` is the key in the key/value storage on the device that stores the path of the file for easy retrieval on the native side -
- -
iOS -To retrieve the image and display it in a widget, you can use the following SwiftUI code: - -1. In your `TimelineEntry` struct add a property to retrieve the path: - ```swift - struct MyEntry: TimelineEntry { - … - let lineChartPath: String - } - ``` - -2. Get the path from the `UserDefaults` in `getSnapshot`: - ```swift - func getSnapshot( - ... - let lineChartPath = userDefaults?.string(forKey: "lineChart") ?? "No screenshot available" - ``` -3. Create a `View` to display the chart and resize the image based on the `displaySize` of the widget: - ```swift - struct WidgetEntryView : View { - … - var ChartImage: some View { - if let uiImage = UIImage(contentsOfFile: entry.lineChartPath) { - let image = Image(uiImage: uiImage) - .resizable() - .frame(width: entry.displaySize.height*0.5, height: entry.displaySize.height*0.5, alignment: .center) - return AnyView(image) - } - print("The image file could not be loaded") - return AnyView(EmptyView()) - } - … - } - ``` - -4. Display the chart in the body of the widget's `View`: - ```swift - VStack { - Text(entry.title) - Text(entry.description) - ChartImage - } - ``` - -Screenshot 2023-06-07 at 12 57 28 PM -
- -
Android (Jetpack Glance) - -```kotlin -// Access data -val data = currentState.preferences - -// Get Path -val imagePath = data.getString("lineChart", null) - -// Add Image to Compose Tree -imagePath?.let { - val bitmap = BitmapFactory.decodeFile(it) - Image(androidx.glance.ImageProvider(bitmap), null) -} -``` - -
- -
Android (XML) - -1. Add an image UI element to your xml file: - ```xml - - ``` -2. Update your Kotlin code to get the chart image and put it into the widget, if it exists. - ```kotlin - class NewsWidget : AppWidgetProvider() { - override fun onUpdate( - context: Context, - appWidgetManager: AppWidgetManager, - appWidgetIds: IntArray, - ) { - for (appWidgetId in appWidgetIds) { - // Get reference to SharedPreferences - val widgetData = HomeWidgetPlugin.getData(context) - val views = RemoteViews(context.packageName, R.layout.news_widget).apply { - // Get chart image and put it in the widget, if it exists - val imagePath = widgetData.getString("lineChart", null) - val imageFile = File(imagePath) - val imageExists = imageFile.exists() - if (imageExists) { - val myBitmap: Bitmap = BitmapFactory.decodeFile(imageFile.absolutePath) - setImageViewBitmap(R.id.widget_image, myBitmap) - } else { - println("image not found!, looked @: $imagePath") - } - // End new code - } - appWidgetManager.updateAppWidget(appWidgetId, views) - } - } - } - ``` -
- -### Launch App and Detect which Widget was clicked -To detect if the App has been initially started by clicking the Widget you can call `HomeWidget.initiallyLaunchedFromHomeWidget()` if the App was already running in the Background you can receive these Events by listening to `HomeWidget.widgetClicked`. Both methods will provide Uris, so you can easily send back data from the Widget to the App to for example navigate to a content page. - -In order for these methods to work you need to follow these steps: - -
iOS - -Add `.widgetUrl` to your WidgetComponent -```swift -Text(entry.message) - .font(.body) - .widgetURL(URL(string: "homeWidgetExample://message?message=\(entry.message)&homeWidget")) -``` -In order to only detect Widget Links you need to add the queryParameter`homeWidget` to the URL -
- -
Android Jetpack Glance - -Add an `IntentFilter` to the `Activity` Section in your `AndroidManifest` -``` - - - -``` - -Add the following modifier to your Widget (import from HomeWidget) -```kotlin -Text( - message, - style = TextStyle(fontSize = 18.sp), - modifier = GlanceModifier.clickable( - onClick = actionStartActivity( - context, - Uri.parse("homeWidgetExample://message?message=$message") - ) - ) -) -``` - -
- -
Android XML - -Add an `IntentFilter` to the `Activity` Section in your `AndroidManifest` -``` - - - -``` - -In your WidgetProvider add a PendingIntent to your View using `HomeWidgetLaunchIntent.getActivity` -```kotlin -val pendingIntentWithData = HomeWidgetLaunchIntent.getActivity( - context, - MainActivity::class.java, - Uri.parse("homeWidgetExample://message?message=$message")) -setOnClickPendingIntent(R.id.widget_message, pendingIntentWithData) -``` - -
- -### Background Update -As the methods of HomeWidget are static it is possible to use HomeWidget in the background to update the Widget even when the App is in the background. - -The example App is using the [flutter_workmanager](https://pub.dev/packages/workmanager) plugin to achieve this. -Please follow the Setup Instructions for flutter_workmanager (or your preferred background code execution plugin). Most notably make sure that Plugins get registered in iOS in order to be able to communicate with the HomeWidget Plugin. -In case of flutter_workmanager this achieved by adding: -```swift -WorkmanagerPlugin.setPluginRegistrantCallback { registry in - GeneratedPluginRegistrant.register(with: registry) -} -``` -to [AppDelegate.swift](example/ios/Runner/AppDelegate.swift) - -### Request Pin Widget -Requests to Pin (Add) the Widget to the users HomeScreen by pinning it to the users HomeScreen. - -```dart -HomeWidget.requestPinWidget( - name: 'HomeWidgetExampleProvider', - androidName: 'HomeWidgetExampleProvider', - qualifiedAndroidName: 'com.example.app.HomeWidgetExampleProvider', -); -``` - -This method is only supported on [Android, API 26+](https://developer.android.com/develop/ui/views/appwidgets/configuration#pin). -If you want to check whether it is supported on current device, use: - -```dart -HomeWidget.isRequestPinWidgetSupported(); -``` - ---- - -## Resources, Articles, Talks -Please add to this list if you have interesting and helpful resources -- [Google Codelab](https://codelabs.developers.google.com/flutter-home-screen-widgets#0) -- [Interactive HomeScreen Widgets with Flutter using home_widget](https://medium.com/p/83cb0706a417) -- [iOS Lockscreen Widgets with Flutter and home_widget](https://medium.com/p/0dfecc18cfa0) diff --git a/README.md b/README.md new file mode 120000 index 00000000..7b3e42dc --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +packages/home_widget/README.md \ No newline at end of file diff --git a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundReceiver.kt b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundReceiver.kt deleted file mode 100644 index dcffdcc7..00000000 --- a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundReceiver.kt +++ /dev/null @@ -1,15 +0,0 @@ -package es.antonborri.home_widget - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import io.flutter.FlutterInjector - -class HomeWidgetBackgroundReceiver : BroadcastReceiver() { - override fun onReceive(context: Context, intent: Intent) { - val flutterLoader = FlutterInjector.instance().flutterLoader() - flutterLoader.startInitialization(context) - flutterLoader.ensureInitializationComplete(context, null) - HomeWidgetBackgroundService.enqueueWork(context, intent) - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundService.kt b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundService.kt deleted file mode 100644 index f2a28777..00000000 --- a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundService.kt +++ /dev/null @@ -1,91 +0,0 @@ -package es.antonborri.home_widget - -import android.content.Context -import android.content.Intent -import android.os.Handler -import android.util.Log -import androidx.core.app.JobIntentService -import io.flutter.FlutterInjector -import io.flutter.embedding.engine.FlutterEngine -import io.flutter.embedding.engine.dart.DartExecutor -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.view.FlutterCallbackInformation -import java.util.* -import java.util.concurrent.atomic.AtomicBoolean - -class HomeWidgetBackgroundService : MethodChannel.MethodCallHandler, JobIntentService() { - - private val queue = ArrayDeque>() - private lateinit var channel: MethodChannel - private lateinit var context: Context - - companion object { - private const val TAG = "HomeWidgetService" - private val JOB_ID = UUID.randomUUID().mostSignificantBits.toInt() - private var engine: FlutterEngine? = null - - private val serviceStarted = AtomicBoolean(false) - - fun enqueueWork(context: Context, work: Intent) { - enqueueWork(context, HomeWidgetBackgroundService::class.java, JOB_ID, work) - } - } - - override fun onCreate() { - super.onCreate() - synchronized(serviceStarted) { - context = this - if (engine == null) { - val callbackHandle = HomeWidgetPlugin.getDispatcherHandle(context) - - if (callbackHandle == 0L) { - Log.e(TAG, "No callbackHandle saved. Did you call HomeWidget.registerBackgroundCallback?") - } - - engine = FlutterEngine(context) - - val callbackInfo = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle) ?: return - - - - val callback = DartExecutor.DartCallback( - context.assets, - FlutterInjector.instance().flutterLoader().findAppBundlePath(), - callbackInfo - ) - engine?.dartExecutor?.executeDartCallback(callback) - } - } - channel = MethodChannel(engine!!.getDartExecutor().getBinaryMessenger(), - "home_widget/background") - channel.setMethodCallHandler(this) - } - - override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { - if (call.method == "HomeWidget.backgroundInitialized") { - synchronized(serviceStarted) { - while (!queue.isEmpty()) { - channel.invokeMethod("", queue.remove()) - } - serviceStarted.set(true) - } - } - } - - override fun onHandleWork(intent: Intent) { - val data = intent.data?.toString() ?: "" - val args = listOf( - HomeWidgetPlugin.getHandle(context), - data - ) - - synchronized(serviceStarted) { - if (!serviceStarted.get()) { - queue.add(args) - } else { - Handler(context.mainLooper).post { channel.invokeMethod("", args) } - } - } - } -} diff --git a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceState.kt b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceState.kt deleted file mode 100644 index 31e6595a..00000000 --- a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceState.kt +++ /dev/null @@ -1,32 +0,0 @@ -import android.content.Context -import android.content.SharedPreferences -import android.os.Environment -import androidx.datastore.core.DataStore -import androidx.glance.state.GlanceStateDefinition -import es.antonborri.home_widget.HomeWidgetPlugin -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.flow -import java.io.File - -class HomeWidgetGlanceState(val preferences: SharedPreferences) - -class HomeWidgetGlanceStateDefinition : GlanceStateDefinition { - override suspend fun getDataStore(context: Context, fileKey: String): DataStore { - val preferences = context.getSharedPreferences(HomeWidgetPlugin.PREFERENCES, Context.MODE_PRIVATE) - return HomeWidgetGlanceDataStore(preferences) - } - - override fun getLocation(context: Context, fileKey: String): File { - return Environment.getDataDirectory() - } - -} - -private class HomeWidgetGlanceDataStore(private val preferences: SharedPreferences) : DataStore { - override val data: Flow - get() = flow { emit(HomeWidgetGlanceState(preferences)) } - - override suspend fun updateData(transform: suspend (t: HomeWidgetGlanceState) -> HomeWidgetGlanceState): HomeWidgetGlanceState { - return transform(HomeWidgetGlanceState(preferences)) - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceWidgetReceiver.kt b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceWidgetReceiver.kt deleted file mode 100644 index c4ee3413..00000000 --- a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceWidgetReceiver.kt +++ /dev/null @@ -1,29 +0,0 @@ -import android.appwidget.AppWidgetManager -import android.content.Context -import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.GlanceAppWidgetManager -import androidx.glance.appwidget.GlanceAppWidgetReceiver -import androidx.glance.appwidget.state.updateAppWidgetState -import kotlinx.coroutines.runBlocking - -abstract class HomeWidgetGlanceWidgetReceiver : GlanceAppWidgetReceiver() { - - abstract override val glanceAppWidget: T - - override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { - super.onUpdate(context, appWidgetManager, appWidgetIds) - runBlocking { - appWidgetIds.forEach { - val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(it) - glanceAppWidget.apply { - if (this.stateDefinition is HomeWidgetGlanceStateDefinition) { - // Must Update State - updateAppWidgetState(context = context, this.stateDefinition as HomeWidgetGlanceStateDefinition, glanceId) { currentState -> currentState } - } - // Update widget. - update(context, glanceId) - } - } - } - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt deleted file mode 100644 index 770a1d3f..00000000 --- a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt +++ /dev/null @@ -1,62 +0,0 @@ -package es.antonborri.home_widget - -import android.app.Activity -import android.app.ActivityOptions -import android.app.PendingIntent -import android.content.Context -import android.content.Intent -import android.net.Uri -import android.os.Build -import androidx.glance.action.Action -import androidx.glance.appwidget.action.actionStartActivity - -object HomeWidgetLaunchIntent { - - const val HOME_WIDGET_LAUNCH_ACTION = "es.antonborri.home_widget.action.LAUNCH" - - fun getActivity(context: Context, activityClass: Class, uri: Uri? = null): PendingIntent where T : Activity { - val intent = Intent(context, activityClass) - intent.data = uri - intent.action = HOME_WIDGET_LAUNCH_ACTION - - var flags = PendingIntent.FLAG_UPDATE_CURRENT - if (Build.VERSION.SDK_INT >= 23) { - flags = flags or PendingIntent.FLAG_IMMUTABLE - } - - if (Build.VERSION.SDK_INT < 34) { - return PendingIntent.getActivity(context, 0, intent, flags) - } - - val options = ActivityOptions.makeBasic() - options.pendingIntentBackgroundActivityStartMode = ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED - - return PendingIntent.getActivity(context, 0, intent, flags, options.toBundle()) - } -} - -inline fun actionStartActivity(context: Context, uri: Uri? = null): Action { - val intent = Intent(context, T::class.java) - intent.data = uri - intent.action = HomeWidgetLaunchIntent.HOME_WIDGET_LAUNCH_ACTION - - return actionStartActivity(intent) -} - - -object HomeWidgetBackgroundIntent { - private const val HOME_WIDGET_BACKGROUND_ACTION = "es.antonborri.home_widget.action.BACKGROUND" - - fun getBroadcast(context: Context, uri: Uri? = null): PendingIntent { - val intent = Intent(context, HomeWidgetBackgroundReceiver::class.java) - intent.data = uri - intent.action = HOME_WIDGET_BACKGROUND_ACTION - - var flags = PendingIntent.FLAG_UPDATE_CURRENT - if (Build.VERSION.SDK_INT >= 23) { - flags = flags or PendingIntent.FLAG_IMMUTABLE - } - - return PendingIntent.getBroadcast(context, 0, intent, flags) - } -} \ No newline at end of file diff --git a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetPlugin.kt b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetPlugin.kt deleted file mode 100644 index 03c7b042..00000000 --- a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetPlugin.kt +++ /dev/null @@ -1,282 +0,0 @@ -package es.antonborri.home_widget - -import android.app.Activity -import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProviderInfo -import android.content.BroadcastReceiver -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.SharedPreferences -import android.os.Build -import io.flutter.embedding.engine.plugins.FlutterPlugin -import io.flutter.embedding.engine.plugins.activity.ActivityAware -import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding -import io.flutter.plugin.common.EventChannel -import io.flutter.plugin.common.MethodCall -import io.flutter.plugin.common.MethodChannel -import io.flutter.plugin.common.MethodChannel.MethodCallHandler -import io.flutter.plugin.common.MethodChannel.Result -import io.flutter.plugin.common.PluginRegistry - - -/** HomeWidgetPlugin */ -class HomeWidgetPlugin : FlutterPlugin, MethodCallHandler, ActivityAware, - EventChannel.StreamHandler, PluginRegistry.NewIntentListener { - private lateinit var channel: MethodChannel - private lateinit var eventChannel: EventChannel - private lateinit var context: Context - - private var activity: Activity? = null - private var receiver: BroadcastReceiver? = null - private val doubleLongPrefix : String = "home_widget.double." - - override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { - channel = MethodChannel(flutterPluginBinding.binaryMessenger, "home_widget") - channel.setMethodCallHandler(this) - - eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "home_widget/updates") - eventChannel.setStreamHandler(this) - context = flutterPluginBinding.applicationContext - } - override fun onMethodCall(call: MethodCall, result: Result) { - when (call.method) { - "saveWidgetData" -> { - if (call.hasArgument("id") && call.hasArgument("data")) { - val id = call.argument("id") - val data = call.argument("data") - val prefs = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE).edit() - if (data != null) { - prefs.putBoolean("$doubleLongPrefix$id", data is Double) - when (data) { - is Boolean -> prefs.putBoolean(id, data) - is Float -> prefs.putFloat(id, data) - is String -> prefs.putString(id, data) - is Double -> prefs.putLong(id, java.lang.Double.doubleToRawLongBits(data)) - is Int -> prefs.putInt(id, data) - is Long -> prefs.putLong(id, data) - else -> result.error("-10", "Invalid Type ${data!!::class.java.simpleName}. Supported types are Boolean, Float, String, Double, Long", IllegalArgumentException()) - } - } else { - prefs.remove(id) - prefs.remove("$doubleLongPrefix$id") - } - result.success(prefs.commit()) - } else { - result.error("-1", "InvalidArguments saveWidgetData must be called with id and data", IllegalArgumentException()) - } - } - "getWidgetData" -> { - if (call.hasArgument("id")) { - val id = call.argument("id") - val defaultValue = call.argument("defaultValue") - - val prefs = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE) - - val value = prefs.all[id] ?: defaultValue - - if(value is Long && prefs.getBoolean("$doubleLongPrefix$id", false)) { - result.success(java.lang.Double.longBitsToDouble(value)) - } else { - result.success(value) - } - } else { - result.error("-2", "InvalidArguments getWidgetData must be called with id", IllegalArgumentException()) - } - } - "updateWidget" -> { - val qualifiedName = call.argument("qualifiedAndroidName") - val className = call.argument("android") ?: call.argument("name") - try { - val javaClass = Class.forName(qualifiedName ?: "${context.packageName}.${className}") - val intent = Intent(context, javaClass) - intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE - val ids: IntArray = AppWidgetManager.getInstance(context.applicationContext).getAppWidgetIds(ComponentName(context, javaClass)) - intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids) - context.sendBroadcast(intent) - result.success(true) - } catch (classException: ClassNotFoundException) { - result.error("-3", "No Widget found with Name $className. Argument 'name' must be the same as your AppWidgetProvider you wish to update", classException) - } - } - "setAppGroupId" -> { - result.success(true) - } - "initiallyLaunchedFromHomeWidget" -> { - return if (activity?.intent?.action?.equals(HomeWidgetLaunchIntent.HOME_WIDGET_LAUNCH_ACTION) == true) { - result.success(activity?.intent?.data?.toString() ?: "") - } else { - result.success(null) - } - } - "registerBackgroundCallback" -> { - val dispatcher = ((call.arguments as Iterable<*>).toList()[0] as Number).toLong() - val callback = ((call.arguments as Iterable<*>).toList()[1] as Number).toLong() - saveCallbackHandle(context, dispatcher, callback) - return result.success(true) - } - "isRequestPinWidgetSupported" -> { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - return result.success(false) - } - - val appWidgetManager = AppWidgetManager.getInstance(context.applicationContext) - return result.success(appWidgetManager.isRequestPinAppWidgetSupported) - } - "requestPinWidget" -> { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { - return result.success(null) - } - - val qualifiedName = call.argument("qualifiedAndroidName") - val className = call.argument("android") ?: call.argument("name") - - try { - val javaClass = Class.forName(qualifiedName ?: "${context.packageName}.${className}") - val myProvider = ComponentName(context, javaClass) - - val appWidgetManager = AppWidgetManager.getInstance(context.applicationContext) - - if (appWidgetManager.isRequestPinAppWidgetSupported) { - appWidgetManager.requestPinAppWidget(myProvider, null, null) - } - - return result.success(null) - } catch (classException: ClassNotFoundException) { - result.error("-4", "No Widget found with Name $className. Argument 'name' must be the same as your AppWidgetProvider you wish to update", classException) - } - } - "getInstalledWidgets" -> { - try { - val pinnedWidgetInfoList = getInstalledWidgets(context) - result.success(pinnedWidgetInfoList) - } catch (e: Exception) { - result.error("-5", "Failed to get installed widgets: ${e.message}", null) - } - } - else -> { - result.notImplemented() - } - } - } - - private fun getInstalledWidgets(context: Context): List> { - val pinnedWidgetInfoList = mutableListOf>() - val appWidgetManager = AppWidgetManager.getInstance(context.applicationContext) - val installedProviders = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - appWidgetManager.getInstalledProvidersForPackage(context.packageName, null) - } else { - appWidgetManager.installedProviders.filter { it.provider.packageName == context.packageName } - } - for (provider in installedProviders) { - val widgetIds = appWidgetManager.getAppWidgetIds(provider.provider) - for (widgetId in widgetIds) { - val widgetInfo = appWidgetManager.getAppWidgetInfo(widgetId) - pinnedWidgetInfoList.add(widgetInfoToMap(widgetId, widgetInfo)) - } - } - return pinnedWidgetInfoList - } - - private fun widgetInfoToMap(widgetId: Int, widgetInfo: AppWidgetProviderInfo): Map { - val label = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - widgetInfo.loadLabel(context.packageManager).toString() - } else { - @Suppress("DEPRECATION") - widgetInfo.label - } - - return mapOf( - WIDGET_INFO_KEY_WIDGET_ID to widgetId, - WIDGET_INFO_KEY_ANDROID_CLASS_NAME to widgetInfo.provider.shortClassName, - WIDGET_INFO_KEY_LABEL to label - ) - } - - override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { - channel.setMethodCallHandler(null) - } - - companion object { - internal const val PREFERENCES = "HomeWidgetPreferences" - - private const val INTERNAL_PREFERENCES = "InternalHomeWidgetPreferences" - private const val CALLBACK_DISPATCHER_HANDLE = "callbackDispatcherHandle" - private const val CALLBACK_HANDLE = "callbackHandle" - - private const val WIDGET_INFO_KEY_WIDGET_ID = "widgetId" - private const val WIDGET_INFO_KEY_ANDROID_CLASS_NAME = "androidClassName" - private const val WIDGET_INFO_KEY_LABEL = "label" - - private fun saveCallbackHandle(context: Context, dispatcher: Long, handle: Long) { - context.getSharedPreferences(INTERNAL_PREFERENCES, Context.MODE_PRIVATE) - .edit() - .putLong(CALLBACK_DISPATCHER_HANDLE, dispatcher) - .putLong(CALLBACK_HANDLE, handle) - .apply() - } - - fun getDispatcherHandle(context: Context): Long = - context.getSharedPreferences(INTERNAL_PREFERENCES, Context.MODE_PRIVATE).getLong(CALLBACK_DISPATCHER_HANDLE, 0) - - fun getHandle(context: Context): Long = - context.getSharedPreferences(INTERNAL_PREFERENCES, Context.MODE_PRIVATE).getLong(CALLBACK_HANDLE, 0) - - fun getData(context: Context): SharedPreferences = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE) - } - - override fun onAttachedToActivity(binding: ActivityPluginBinding) { - activity = binding.activity - binding.addOnNewIntentListener(this) - } - - override fun onDetachedFromActivityForConfigChanges() { - unregisterReceiver() - activity = null - } - - override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { - activity = binding.activity - binding.addOnNewIntentListener(this) - } - - override fun onDetachedFromActivity() { - unregisterReceiver() - activity = null - } - - override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { - receiver = createReceiver(events) - } - - override fun onCancel(arguments: Any?) { - unregisterReceiver() - receiver = null - } - - private fun createReceiver(events: EventChannel.EventSink?): BroadcastReceiver { - return object : BroadcastReceiver() { - override fun onReceive(context: Context?, intent: Intent?) { - if (intent?.action.equals(HomeWidgetLaunchIntent.HOME_WIDGET_LAUNCH_ACTION)) { - events?.success(intent?.data?.toString() ?: true) - } - } - - } - } - - private fun unregisterReceiver() { - try { - if (receiver != null) { - context.unregisterReceiver(receiver) - } - } catch (e: IllegalArgumentException) { - // Receiver not registered - } - } - - override fun onNewIntent(intent: Intent): Boolean { - receiver?.onReceive(context, intent) - return receiver != null - } -} diff --git a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetProvider.kt b/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetProvider.kt deleted file mode 100644 index 14ff2be9..00000000 --- a/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetProvider.kt +++ /dev/null @@ -1,16 +0,0 @@ -package es.antonborri.home_widget - -import android.appwidget.AppWidgetManager -import android.appwidget.AppWidgetProvider -import android.content.Context -import android.content.SharedPreferences - -abstract class HomeWidgetProvider : AppWidgetProvider() { - - override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) { - super.onUpdate(context, appWidgetManager, appWidgetIds) - onUpdate(context, appWidgetManager, appWidgetIds, HomeWidgetPlugin.getData(context)) - } - - abstract fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, widgetData: SharedPreferences) -} \ No newline at end of file diff --git a/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/HomeWidgetExampleProvider.kt b/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/HomeWidgetExampleProvider.kt deleted file mode 100644 index 21d0d6d6..00000000 --- a/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/HomeWidgetExampleProvider.kt +++ /dev/null @@ -1,57 +0,0 @@ -package es.antonborri.home_widget_example - -import android.appwidget.AppWidgetManager -import android.content.Context -import android.content.SharedPreferences -import android.graphics.BitmapFactory -import android.net.Uri -import android.view.View -import android.widget.RemoteViews -import es.antonborri.home_widget.HomeWidgetBackgroundIntent -import es.antonborri.home_widget.HomeWidgetLaunchIntent -import es.antonborri.home_widget.HomeWidgetProvider - -class HomeWidgetExampleProvider : HomeWidgetProvider() { - - override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, widgetData: SharedPreferences) { - appWidgetIds.forEach { widgetId -> - val views = RemoteViews(context.packageName, R.layout.example_layout).apply { - // Open App on Widget Click - val pendingIntent = HomeWidgetLaunchIntent.getActivity( - context, - MainActivity::class.java) - setOnClickPendingIntent(R.id.widget_container, pendingIntent) - - // Swap Title Text by calling Dart Code in the Background - setTextViewText(R.id.widget_title, widgetData.getString("title", null) - ?: "No Title Set") - val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast( - context, - Uri.parse("homeWidgetExample://titleClicked") - ) - setOnClickPendingIntent(R.id.widget_title, backgroundIntent) - - val message = widgetData.getString("message", null) - setTextViewText(R.id.widget_message, message - ?: "No Message Set") - // Show Images saved with `renderFlutterWidget` - val image = widgetData.getString("dashIcon", null) - if (image != null) { - setImageViewBitmap(R.id.widget_img, BitmapFactory.decodeFile(image)) - setViewVisibility(R.id.widget_img, View.VISIBLE) - } else { - setViewVisibility(R.id.widget_img, View.GONE) - } - - // Detect App opened via Click inside Flutter - val pendingIntentWithData = HomeWidgetLaunchIntent.getActivity( - context, - MainActivity::class.java, - Uri.parse("homeWidgetExample://message?message=$message")) - setOnClickPendingIntent(R.id.widget_message, pendingIntentWithData) - } - - appWidgetManager.updateAppWidget(widgetId, views) - } - } -} \ No newline at end of file diff --git a/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt b/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt deleted file mode 100644 index ce8349bd..00000000 --- a/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt +++ /dev/null @@ -1,93 +0,0 @@ -package es.antonborri.home_widget_example.glance - -import HomeWidgetGlanceState -import HomeWidgetGlanceStateDefinition -import android.content.Context -import android.graphics.BitmapFactory -import android.net.Uri -import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.unit.dp -import androidx.compose.ui.unit.sp -import androidx.glance.GlanceId -import androidx.glance.GlanceModifier -import androidx.glance.Image -import androidx.glance.action.ActionParameters -import androidx.glance.action.clickable -import androidx.glance.appwidget.GlanceAppWidget -import androidx.glance.appwidget.action.ActionCallback -import androidx.glance.appwidget.action.actionRunCallback -import androidx.glance.appwidget.provideContent -import androidx.glance.background -import androidx.glance.currentState -import androidx.glance.layout.Alignment -import androidx.glance.layout.Box -import androidx.glance.layout.Column -import androidx.glance.layout.fillMaxSize -import androidx.glance.layout.padding -import androidx.glance.text.FontWeight -import androidx.glance.text.Text -import androidx.glance.text.TextStyle -import es.antonborri.home_widget.HomeWidgetBackgroundIntent -import es.antonborri.home_widget.actionStartActivity -import es.antonborri.home_widget_example.MainActivity - -class HomeWidgetGlanceAppWidget : GlanceAppWidget() { - - /** - * Needed for Updating - */ - override val stateDefinition = HomeWidgetGlanceStateDefinition() - - override suspend fun provideGlance(context: Context, id: GlanceId) { - provideContent { - GlanceContent(context, currentState()) - } - } - - @Composable - private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) { - val data = currentState.preferences - val imagePath = data.getString("dashIcon", null) - - val title = data.getString("title", "")!! - val message = data.getString("message", "")!! - - Box(modifier = GlanceModifier.background(Color.White).padding(16.dp).clickable(onClick = actionStartActivity(context))) { - Column( - modifier = GlanceModifier.fillMaxSize(), - verticalAlignment = Alignment.Vertical.Top, - horizontalAlignment = Alignment.Horizontal.Start, - ) { - Text("Glance") - Text( - title, - style = TextStyle(fontSize = 36.sp, fontWeight = FontWeight.Bold), - modifier = GlanceModifier.clickable(onClick = actionRunCallback()), - ) - Text( - message, - style = TextStyle(fontSize = 18.sp), - modifier = GlanceModifier.clickable( - onClick = actionStartActivity( - context, - Uri.parse("homeWidgetExample://message?message=$message") - ) - ) - ) - imagePath?.let { - val bitmap = BitmapFactory.decodeFile(it) - Image(androidx.glance.ImageProvider(bitmap), null) - } - } - } - } -} - -class InteractiveAction : ActionCallback { - override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { - val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(context, Uri.parse("homeWidgetExample://titleClicked")) - backgroundIntent.send() - } -} - diff --git a/example/linux/flutter/generated_plugin_registrant.cc b/example/linux/flutter/generated_plugin_registrant.cc deleted file mode 100644 index e71a16d2..00000000 --- a/example/linux/flutter/generated_plugin_registrant.cc +++ /dev/null @@ -1,11 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#include "generated_plugin_registrant.h" - - -void fl_register_plugins(FlPluginRegistry* registry) { -} diff --git a/example/linux/flutter/generated_plugin_registrant.h b/example/linux/flutter/generated_plugin_registrant.h deleted file mode 100644 index e0f0a47b..00000000 --- a/example/linux/flutter/generated_plugin_registrant.h +++ /dev/null @@ -1,15 +0,0 @@ -// -// Generated file. Do not edit. -// - -// clang-format off - -#ifndef GENERATED_PLUGIN_REGISTRANT_ -#define GENERATED_PLUGIN_REGISTRANT_ - -#include - -// Registers Flutter plugins. -void fl_register_plugins(FlPluginRegistry* registry); - -#endif // GENERATED_PLUGIN_REGISTRANT_ diff --git a/example/linux/flutter/generated_plugins.cmake b/example/linux/flutter/generated_plugins.cmake deleted file mode 100644 index 2e1de87a..00000000 --- a/example/linux/flutter/generated_plugins.cmake +++ /dev/null @@ -1,23 +0,0 @@ -# -# Generated file, do not edit. -# - -list(APPEND FLUTTER_PLUGIN_LIST -) - -list(APPEND FLUTTER_FFI_PLUGIN_LIST -) - -set(PLUGIN_BUNDLED_LIBRARIES) - -foreach(plugin ${FLUTTER_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${plugin}/linux plugins/${plugin}) - target_link_libraries(${BINARY_NAME} PRIVATE ${plugin}_plugin) - list(APPEND PLUGIN_BUNDLED_LIBRARIES $) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${plugin}_bundled_libraries}) -endforeach(plugin) - -foreach(ffi_plugin ${FLUTTER_FFI_PLUGIN_LIST}) - add_subdirectory(flutter/ephemeral/.plugin_symlinks/${ffi_plugin}/linux plugins/${ffi_plugin}) - list(APPEND PLUGIN_BUNDLED_LIBRARIES ${${ffi_plugin}_bundled_libraries}) -endforeach(ffi_plugin) diff --git a/melos.yaml b/melos.yaml new file mode 100644 index 00000000..626b5aec --- /dev/null +++ b/melos.yaml @@ -0,0 +1,27 @@ +name: home_widget + +packages: + - packages/** + +scripts: + format:all: + description: Format Dart, Kotlin, and Swift Files + steps: + - format:dart + - format:kotlin + - format:swift + + format:dart: + description: Format Dart Files + run: | + dart format . --set-exit-if-changed + + format:kotlin: + description: Format Kotlin Files + run: ktfmt . --set-exit-if-changed + + format:swift: + description: Format Swift Files + run: | + swift-format -r -i -p . + swift-format lint -r -s . \ No newline at end of file diff --git a/packages/home_widget/.gitignore b/packages/home_widget/.gitignore new file mode 100644 index 00000000..7c836e79 --- /dev/null +++ b/packages/home_widget/.gitignore @@ -0,0 +1,135 @@ +.DS_Store +.dart_tool/ + +.packages +.pub/ + +build/ +.idea + +# Miscellaneous +*.class +*.lock +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.buildlog/ +.history +.svn/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# Visual Studio Code related +.classpath +.project +.settings/ +.vscode/ + +# Flutter repo-specific +/bin/cache/ +/bin/mingit/ +/dev/benchmarks/mega_gallery/ +/dev/bots/.recipe_deps +/dev/bots/android_tools/ +/dev/devicelab/ABresults*.json +/dev/docs/doc/ +/dev/docs/flutter.docs.zip +/dev/docs/lib/ +/dev/docs/pubspec.yaml +/dev/integration_tests/**/xcuserdata +/dev/integration_tests/**/Pods +/packages/flutter/coverage/ +version +analysis_benchmark.json + +# packages file containing multi-root paths +.packages.generated + +# Flutter/Dart/Pub related +**/doc/api/ +.dart_tool/ +.flutter-plugins +.flutter-plugins-dependencies +**/generated_plugin_registrant.dart +.packages +.pub-cache/ +.pub/ +build/ +flutter_*.png +linked_*.ds +unlinked.ds +unlinked_spec.ds + +# Android related +**/android/**/gradle-wrapper.jar +**/android/.gradle +**/android/captures/ +**/android/gradlew +**/android/gradlew.bat +**/android/local.properties +**/android/**/GeneratedPluginRegistrant.java +**/android/key.properties +*.jks + +# iOS/XCode related +**/ios/**/*.mode1v3 +**/ios/**/*.mode2v3 +**/ios/**/*.moved-aside +**/ios/**/*.pbxuser +**/ios/**/*.perspectivev3 +**/ios/**/*sync/ +**/ios/**/.sconsign.dblite +**/ios/**/.tags* +**/ios/**/.vagrant/ +**/ios/**/DerivedData/ +**/ios/**/Icon? +**/ios/**/Pods/ +**/ios/**/.symlinks/ +**/ios/**/profile +**/ios/**/xcuserdata +**/ios/.generated/ +**/ios/Flutter/.last_build_id +**/ios/Flutter/App.framework +**/ios/Flutter/Flutter.framework +**/ios/Flutter/Flutter.podspec +**/ios/Flutter/Generated.xcconfig +**/ios/Flutter/app.flx +**/ios/Flutter/app.zip +**/ios/Flutter/flutter_assets/ +**/ios/Flutter/flutter_export_environment.sh +**/ios/ServiceDefinitions.json +**/ios/Runner/GeneratedPluginRegistrant.* + +# macOS +**/macos/Flutter/GeneratedPluginRegistrant.swift +**/macos/Flutter/Flutter-Debug.xcconfig +**/macos/Flutter/Flutter-Release.xcconfig +**/macos/Flutter/Flutter-Profile.xcconfig + +# Coverage +coverage/ + +# Symbols +app.*.symbols + +# Exceptions to above rules. +!**/ios/**/default.mode1v3 +!**/ios/**/default.mode2v3 +!**/ios/**/default.pbxuser +!**/ios/**/default.perspectivev3 +!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages +!/dev/ci/**/Gemfile.lock +.gradle +*.xcworkspacedata +*.jar + +# don't check in golden failure output +**/failures/*.png + +pubspec_overrides.yaml diff --git a/.metadata b/packages/home_widget/.metadata similarity index 100% rename from .metadata rename to packages/home_widget/.metadata diff --git a/CHANGELOG.md b/packages/home_widget/CHANGELOG.md similarity index 100% rename from CHANGELOG.md rename to packages/home_widget/CHANGELOG.md diff --git a/LICENSE b/packages/home_widget/LICENSE similarity index 100% rename from LICENSE rename to packages/home_widget/LICENSE diff --git a/packages/home_widget/README.md b/packages/home_widget/README.md new file mode 100644 index 00000000..c325b116 --- /dev/null +++ b/packages/home_widget/README.md @@ -0,0 +1,675 @@ +# Home Widget + +[![Pub](https://img.shields.io/pub/v/home_widget.svg)](https://pub.dartlang.org/packages/home_widget) +[![likes](https://img.shields.io/pub/likes/home_widget)](https://pub.dev/packages/home_widget/score) +[![popularity](https://img.shields.io/pub/popularity/home_widget)](https://pub.dev/packages/home_widget/score) +[![pub points](https://img.shields.io/pub/points/home_widget)](https://pub.dev/packages/home_widget/score) +[![Build](https://github.com/abausg/home_widget/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/ABausG/home_widget/actions/workflows/main.yml?query=branch%3Amain) +[![codecov](https://codecov.io/gh/ABausG/home_widget/branch/main/graph/badge.svg?token=ZXTZOL6KFO)](https://codecov.io/gh/ABausG/home_widget) + +HomeWidget is a Plugin to make it easier to create HomeScreen Widgets on Android and iOS. +HomeWidget does **not** allow writing Widgets with Flutter itself. It still requires writing the Widgets with native code. However, it provides a unified Interface for sending data, retrieving data and updating the Widgets + +| iOS |  Android | +|----------------------------------------------------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------------------| +| | | + +## Platform Setup +In order to work correctly there needs to be some platform specific setup. Check below on how to add support for Android and iOS + +
iOS + +### Add a Widget to your App in Xcode +Add a widget extension by going File > New > Target > Widget Extension + +![Widget Extension](https://github.com/ABausG/home_widget/blob/main/.github/assets/widget_extension.png?raw=true) + + +### Add GroupId +You need to add a groupId to the App and the Widget Extension + +**Note: in order to add groupIds you need a paid Apple Developer Account** + +Go to your [Apple Developer Account](https://developer.apple.com/account/resources/identifiers/list/applicationGroup) and add a new group. +Add this group to your Runner and the Widget Extension inside XCode: Signing & Capabilities > App Groups > +. +(To swap between your App, and the Extension change the Target) + +![Build Targets](https://github.com/ABausG/home_widget/blob/main/.github/assets/target.png?raw=true) + +### Sync CFBundleVersion (optional) +This step is optional, this will sync the widget extension build version with your app version, so you don't get warnings of mismatch version from App Store Connect when uploading your app. + +![Build Phases](https://github.com/ABausG/home_widget/blob/main/.github/assets/build_phases.png?raw=true) + +In your Runner (app) target go to Build Phases > + > New Run Script Phase and add the following script: +```bash +generatedPath="$SRCROOT/Flutter/Generated.xcconfig" + +# Read and trim versionNumber and buildNumber +versionNumber=$(grep FLUTTER_BUILD_NAME "$generatedPath" | cut -d '=' -f2 | xargs) +buildNumber=$(grep FLUTTER_BUILD_NUMBER "$generatedPath" | cut -d '=' -f2 | xargs) + +infoPlistPath="$SRCROOT/HomeExampleWidget/Info.plist" + +# Check and add CFBundleVersion if it does not exist +/usr/libexec/PlistBuddy -c "Print :CFBundleVersion" "$infoPlistPath" 2>/dev/null +if [ $? != 0 ]; then + /usr/libexec/PlistBuddy -c "Add :CFBundleVersion string $buildNumber" "$infoPlistPath" +else + /usr/libexec/PlistBuddy -c "Set :CFBundleVersion $buildNumber" "$infoPlistPath" +fi + +# Check and add CFBundleShortVersionString if it does not exist +/usr/libexec/PlistBuddy -c "Print :CFBundleShortVersionString" "$infoPlistPath" 2>/dev/null +if [ $? != 0 ]; then + /usr/libexec/PlistBuddy -c "Add :CFBundleShortVersionString string $versionNumber" "$infoPlistPath" +else + /usr/libexec/PlistBuddy -c "Set :CFBundleShortVersionString $versionNumber" "$infoPlistPath" +fi + +``` + +Replace `HomeExampleWidget` with the name of the widget extension folder that you have created. + + +### Write your Widget +Check the [Example App](example/ios/HomeWidgetExample/HomeWidgetExample.swift) for an Implementation of a Widget. +A more detailed overview on how to write Widgets for iOS 14 can be found on the [Apple Developer documentation](https://developer.apple.com/documentation/swiftui/widget). +In order to access the Data send with Flutter can be access with +```swift +let data = UserDefaults.init(suiteName:"YOUR_GROUP_ID") +``` +
+ +
Android (Jetpack Glance) + +### Add Jetpack Glance as a dependency to you app's Gradle File +```groovy +implementation 'androidx.glance:glance-appwidget:LATEST-VERSION' +``` + +### Create Widget Configuration into `android/app/src/main/res/xml` +```xml + + +``` + +### Add WidgetReceiver to AndroidManifest +```xml + + + + + + +``` + +### Create WidgetReceiver + +To get automatic Updates you should extend from [HomeWidgetGlanceWidgetReceiver](android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceWidgetReceiver.kt) + +Your Receiver should then look like this + +```kotlin +package es.antonborri.home_widget_example.glance + +import HomeWidgetGlanceWidgetReceiver + +class HomeWidgetReceiver : HomeWidgetGlanceWidgetReceiver() { + override val glanceAppWidget = HomeWidgetGlanceAppWidget() +} +``` + +### Build Your AppWidget + +```kotlin + +class HomeWidgetGlanceAppWidget : GlanceAppWidget() { + + /** + * Needed for Updating + */ + override val stateDefinition = HomeWidgetGlanceStateDefinition() + + override suspend fun provideGlance(context: Context, id: GlanceId) { + provideContent { + GlanceContent(context, currentState()) + } + } + + @Composable + private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) { + // Use data to access the data you save with + val data = currentState.preferences + + + // Build Your Composable Widget + Column( + ... + } + +``` + +
+ +
Android (XML) + +### Create Widget Layout inside `android/app/src/main/res/layout` + +### Create Widget Configuration into `android/app/src/main/res/xml` +```xml + + + +``` + +### Add WidgetReceiver to AndroidManifest +```xml + + + + + + +``` + +### Write your WidgetProvider +For convenience, you can extend from [HomeWidgetProvider](android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetProvider.kt) which gives you access to a SharedPreferences Object with the Data in the `onUpdate` method. +In case you don't want to use the convenience Method you can access the Data using +```kotlin +import es.antonborri.home_widget.HomeWidgetPlugin +... +HomeWidgetPlugin.getData(context) +``` +which will give you access to the same SharedPreferences + +### More Information +For more Information on how to create and configure Android Widgets, check out [this guide](https://developer.android.com/develop/ui/views/appwidgets) on the Android Developers Page. + +
+ +## Usage + +### Setup +
iOS + +For iOS, you need to call `HomeWidget.setAppGroupId('YOUR_GROUP_ID');` +Without this you won't be able to share data between your App and the Widget and calls to `saveWidgetData` and `getWidgetData` will return an error + +
+ +### Save Data +In order to save Data call `HomeWidget.saveWidgetData('id', data)` + +### Update a Widget +In order to force a reload of the HomeScreenWidget you need to call +```dart +HomeWidget.updateWidget( + name: 'HomeWidgetExampleProvider', + androidName: 'HomeWidgetExampleProvider', + iOSName: 'HomeWidgetExample', + qualifiedAndroidName: 'com.example.app.HomeWidgetExampleProvider', +); +``` + +The name for Android will be chosen by checking `qualifiedAndroidName`, falling back to `.androidName` and if that was not provided it will fallback to `.name`. +This Name needs to be equal to the Classname of the [WidgetProvider](#Write-your-Widget) + +The name for iOS will be chosen by checking `iOSName` if that was not provided it will fallback to `name`. +This name needs to be equal to the Kind specified in you Widget + +#### Android (Jetpack Glance) + +If you followed the guide and use `HomeWidgetGlanceWidgetReceiver` as your Receiver, `HomeWidgetGlanceStateDefinition` as the AppWidgetStateDefinition, `currentState()` in the composable view and `currentState.preferences` for data access. No further work is necessary. + +#### Android (XML) +Calling `HomeWidget.updateWidget` only notifies the specified provider. +To update widgets using this provider, +update them from the provider like this: + +```kotlin +class HomeWidgetExampleProvider : HomeWidgetProvider() { + + override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray, widgetData: SharedPreferences) { + appWidgetIds.forEach { widgetId -> + val views = RemoteViews(context.packageName, R.layout.example_layout).apply { + // ... + } + + // Update widget. + appWidgetManager.updateAppWidget(widgetId, views) + } + } +} +``` + + +### Retrieve Data +To retrieve the current Data saved in the Widget call `HomeWidget.getWidgetData('id', defaultValue: data)` + +### Interactive Widgets + +Android and iOS (starting with iOS 17) allow widgets to have interactive Elements like Buttons + +
Dart + +1. Write a **static** function that takes a Uri as an argument. This will get called when a user clicks on the View + ```dart + @pragma("vm:entry-point") + FutureOr backgroundCallback(Uri data) async { + // do something with data + ... + } + ``` + `@pragma('vm:entry-point')` must be placed above the `callback` function to avoid tree shaking in release mode. + +2. Register the callback function by calling + ```dart + HomeWidget.registerInteractivityCallback(backgroundCallback); + ``` +
+ +
iOS + +1. Adjust your Podfile to add `home_widget` as a dependency to your WidgetExtension + ```rb + target 'YourWidgetExtension' do + use_frameworks! + use_modular_headers! + + pod 'home_widget', :path => '.symlinks/plugins/home_widget/ios' + end + ``` +2. To be able to use plugins with the Background Callback add this to your AppDelegate's `application` function + ```swift + if #available(iOS 17, *) { + HomeWidgetBackgroundWorker.setPluginRegistrantCallback { registry in + GeneratedPluginRegistrant.register(with: registry) + } + } + ``` +3. Create a custom `AppIntent` in your App Target (Runner) and make sure to select both your App and your WidgetExtension in the Target Membership panel + + ![Target Membership](https://github.com/ABausG/home_widget/blob/main/.github/assets/target_membership.png?raw=true) + + In this Intent you should import `home_widget` and call `HomeWidgetBackgroundWorker.run(url: url, appGroup: appGroup!)` in the perform method. `url` and `appGroup` can be either hardcoded or set as parameters from the Widget + ```swift + import AppIntents + import Flutter + import Foundation + import home_widget + + @available(iOS 16, *) + public struct BackgroundIntent: AppIntent { + static public var title: LocalizedStringResource = "HomeWidget Background Intent" + + @Parameter(title: "Widget URI") + var url: URL? + + @Parameter(title: "AppGroup") + var appGroup: String? + + public init() {} + + public init(url: URL?, appGroup: String?) { + self.url = url + self.appGroup = appGroup + } + + public func perform() async throws -> some IntentResult { + await HomeWidgetBackgroundWorker.run(url: url, appGroup: appGroup!) + + return .result() + } + } + ``` +4. Add a Button to your Widget. This Button might be encapsulated by a Version check. Pass in an instance of the `AppIntent` created in the previous step + ```swift + Button( + intent: BackgroundIntent( + url: URL(string: "homeWidgetExample://titleClicked"), appGroup: widgetGroupId) + ) { + Text(entry.title).bold().font( /*@START_MENU_TOKEN@*/.title /*@END_MENU_TOKEN@*/) + }.buttonStyle(.plain) + ``` +5. With the current setup the Widget is now Interactive as long as the App is still in the background. If you want to have the Widget be able to wake the App up you need to add the following to your `AppIntent` file + ```swift + @available(iOS 16, *) + @available(iOSApplicationExtension, unavailable) + extension BackgroundIntent: ForegroundContinuableIntent {} + ``` + This code tells the system to always perform the Intent in the App and not in a process attached to the Widget. Note however that this will start your Flutter App using the normal main entrypoint meaning your full app might be run in the background. To counter this you should add checks in the very first Widget you build inside `runApp` to only perform necessary calls/setups while the App is launched in the background +
+ + +
Android Jetpack Glance + +1. Add the necessary Receiver and Service to your `AndroidManifest.xml` file + ``` + + + + + + + ``` +2. Create a custom Action + ```kotlin + class InteractiveAction : ActionCallback { + override suspend fun onAction(context: Context, glanceId: GlanceId, parameters: ActionParameters) { + val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast(context, Uri.parse("homeWidgetExample://titleClicked")) + backgroundIntent.send() + } + } + ``` +3. Add the Action as a modifier to a view + ```kotlin + Text( + title, + style = TextStyle(fontSize = 36.sp, fontWeight = FontWeight.Bold), + modifier = GlanceModifier.clickable(onClick = actionRunCallback()), + ) + ``` + +
+ +
Android XML + +1. Add the necessary Receiver and Service to your `AndroidManifest.xml` file + ``` + + + + + + + ``` +2. Add a `HomeWidgetBackgroundIntent.getBroadcast` PendingIntent to the View you want to add a click listener to + ```kotlin + val backgroundIntent = HomeWidgetBackgroundIntent.getBroadcast( + context, + Uri.parse("homeWidgetExample://titleClicked") + ) + setOnClickPendingIntent(R.id.widget_title, backgroundIntent) + ``` +
+ +### Using images of Flutter widgets + +In some cases, you may not want to rewrite UI code in the native frameworks for your widgets. + +
Dart +For example, say you have a chart in your Flutter app configured with `CustomPaint`: + +```dart +class LineChart extends StatelessWidget { + const LineChart({ + super.key, + }); + + @override + Widget build(BuildContext context) { + return CustomPaint( + painter: LineChartPainter(), + child: const SizedBox( + height: 200, + width: 200, + ), + ); + } +} +``` + +Screenshot 2023-06-07 at 12 33 44 PM + +Rewriting the code to create this chart on both Android and iOS might be time consuming. +Instead, you can generate a png file of the Flutter widget and save it to a shared container +between your Flutter app and the home screen widget. + +```dart +var path = await HomeWidget.renderFlutterWidget( + const LineChart(), + key: 'lineChart', + logicalSize: const Size(400, 400), +); +``` +- `LineChart()` is the widget that will be rendered as an image. +- `key` is the key in the key/value storage on the device that stores the path of the file for easy retrieval on the native side +
+ +
iOS +To retrieve the image and display it in a widget, you can use the following SwiftUI code: + +1. In your `TimelineEntry` struct add a property to retrieve the path: + ```swift + struct MyEntry: TimelineEntry { + … + let lineChartPath: String + } + ``` + +2. Get the path from the `UserDefaults` in `getSnapshot`: + ```swift + func getSnapshot( + ... + let lineChartPath = userDefaults?.string(forKey: "lineChart") ?? "No screenshot available" + ``` +3. Create a `View` to display the chart and resize the image based on the `displaySize` of the widget: + ```swift + struct WidgetEntryView : View { + … + var ChartImage: some View { + if let uiImage = UIImage(contentsOfFile: entry.lineChartPath) { + let image = Image(uiImage: uiImage) + .resizable() + .frame(width: entry.displaySize.height*0.5, height: entry.displaySize.height*0.5, alignment: .center) + return AnyView(image) + } + print("The image file could not be loaded") + return AnyView(EmptyView()) + } + … + } + ``` + +4. Display the chart in the body of the widget's `View`: + ```swift + VStack { + Text(entry.title) + Text(entry.description) + ChartImage + } + ``` + +Screenshot 2023-06-07 at 12 57 28 PM +
+ +
Android (Jetpack Glance) + +```kotlin +// Access data +val data = currentState.preferences + +// Get Path +val imagePath = data.getString("lineChart", null) + +// Add Image to Compose Tree +imagePath?.let { + val bitmap = BitmapFactory.decodeFile(it) + Image(androidx.glance.ImageProvider(bitmap), null) +} +``` + +
+ +
Android (XML) + +1. Add an image UI element to your xml file: + ```xml + + ``` +2. Update your Kotlin code to get the chart image and put it into the widget, if it exists. + ```kotlin + class NewsWidget : AppWidgetProvider() { + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray, + ) { + for (appWidgetId in appWidgetIds) { + // Get reference to SharedPreferences + val widgetData = HomeWidgetPlugin.getData(context) + val views = RemoteViews(context.packageName, R.layout.news_widget).apply { + // Get chart image and put it in the widget, if it exists + val imagePath = widgetData.getString("lineChart", null) + val imageFile = File(imagePath) + val imageExists = imageFile.exists() + if (imageExists) { + val myBitmap: Bitmap = BitmapFactory.decodeFile(imageFile.absolutePath) + setImageViewBitmap(R.id.widget_image, myBitmap) + } else { + println("image not found!, looked @: $imagePath") + } + // End new code + } + appWidgetManager.updateAppWidget(appWidgetId, views) + } + } + } + ``` +
+ +### Launch App and Detect which Widget was clicked +To detect if the App has been initially started by clicking the Widget you can call `HomeWidget.initiallyLaunchedFromHomeWidget()` if the App was already running in the Background you can receive these Events by listening to `HomeWidget.widgetClicked`. Both methods will provide Uris, so you can easily send back data from the Widget to the App to for example navigate to a content page. + +In order for these methods to work you need to follow these steps: + +
iOS + +Add `.widgetUrl` to your WidgetComponent +```swift +Text(entry.message) + .font(.body) + .widgetURL(URL(string: "homeWidgetExample://message?message=\(entry.message)&homeWidget")) +``` +In order to only detect Widget Links you need to add the queryParameter`homeWidget` to the URL +
+ +
Android Jetpack Glance + +Add an `IntentFilter` to the `Activity` Section in your `AndroidManifest` +``` + + + +``` + +Add the following modifier to your Widget (import from HomeWidget) +```kotlin +Text( + message, + style = TextStyle(fontSize = 18.sp), + modifier = GlanceModifier.clickable( + onClick = actionStartActivity( + context, + Uri.parse("homeWidgetExample://message?message=$message") + ) + ) +) +``` + +
+ +
Android XML + +Add an `IntentFilter` to the `Activity` Section in your `AndroidManifest` +``` + + + +``` + +In your WidgetProvider add a PendingIntent to your View using `HomeWidgetLaunchIntent.getActivity` +```kotlin +val pendingIntentWithData = HomeWidgetLaunchIntent.getActivity( + context, + MainActivity::class.java, + Uri.parse("homeWidgetExample://message?message=$message")) +setOnClickPendingIntent(R.id.widget_message, pendingIntentWithData) +``` + +
+ +### Background Update +As the methods of HomeWidget are static it is possible to use HomeWidget in the background to update the Widget even when the App is in the background. + +The example App is using the [flutter_workmanager](https://pub.dev/packages/workmanager) plugin to achieve this. +Please follow the Setup Instructions for flutter_workmanager (or your preferred background code execution plugin). Most notably make sure that Plugins get registered in iOS in order to be able to communicate with the HomeWidget Plugin. +In case of flutter_workmanager this achieved by adding: +```swift +WorkmanagerPlugin.setPluginRegistrantCallback { registry in + GeneratedPluginRegistrant.register(with: registry) +} +``` +to [AppDelegate.swift](example/ios/Runner/AppDelegate.swift) + +### Request Pin Widget +Requests to Pin (Add) the Widget to the users HomeScreen by pinning it to the users HomeScreen. + +```dart +HomeWidget.requestPinWidget( + name: 'HomeWidgetExampleProvider', + androidName: 'HomeWidgetExampleProvider', + qualifiedAndroidName: 'com.example.app.HomeWidgetExampleProvider', +); +``` + +This method is only supported on [Android, API 26+](https://developer.android.com/develop/ui/views/appwidgets/configuration#pin). +If you want to check whether it is supported on current device, use: + +```dart +HomeWidget.isRequestPinWidgetSupported(); +``` + +--- + +## Resources, Articles, Talks +Please add to this list if you have interesting and helpful resources +- [Google Codelab](https://codelabs.developers.google.com/flutter-home-screen-widgets#0) +- [Interactive HomeScreen Widgets with Flutter using home_widget](https://medium.com/p/83cb0706a417) +- [iOS Lockscreen Widgets with Flutter and home_widget](https://medium.com/p/0dfecc18cfa0) diff --git a/analysis_options.yaml b/packages/home_widget/analysis_options.yaml similarity index 100% rename from analysis_options.yaml rename to packages/home_widget/analysis_options.yaml diff --git a/android/.gitignore b/packages/home_widget/android/.gitignore similarity index 100% rename from android/.gitignore rename to packages/home_widget/android/.gitignore diff --git a/android/build.gradle b/packages/home_widget/android/build.gradle similarity index 100% rename from android/build.gradle rename to packages/home_widget/android/build.gradle diff --git a/android/gradle.properties b/packages/home_widget/android/gradle.properties similarity index 100% rename from android/gradle.properties rename to packages/home_widget/android/gradle.properties diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/packages/home_widget/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from android/gradle/wrapper/gradle-wrapper.properties rename to packages/home_widget/android/gradle/wrapper/gradle-wrapper.properties diff --git a/android/settings.gradle b/packages/home_widget/android/settings.gradle similarity index 100% rename from android/settings.gradle rename to packages/home_widget/android/settings.gradle diff --git a/android/src/main/AndroidManifest.xml b/packages/home_widget/android/src/main/AndroidManifest.xml similarity index 100% rename from android/src/main/AndroidManifest.xml rename to packages/home_widget/android/src/main/AndroidManifest.xml diff --git a/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundReceiver.kt b/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundReceiver.kt new file mode 100644 index 00000000..82176623 --- /dev/null +++ b/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundReceiver.kt @@ -0,0 +1,15 @@ +package es.antonborri.home_widget + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import io.flutter.FlutterInjector + +class HomeWidgetBackgroundReceiver : BroadcastReceiver() { + override fun onReceive(context: Context, intent: Intent) { + val flutterLoader = FlutterInjector.instance().flutterLoader() + flutterLoader.startInitialization(context) + flutterLoader.ensureInitializationComplete(context, null) + HomeWidgetBackgroundService.enqueueWork(context, intent) + } +} diff --git a/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundService.kt b/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundService.kt new file mode 100644 index 00000000..3505bb6f --- /dev/null +++ b/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetBackgroundService.kt @@ -0,0 +1,87 @@ +package es.antonborri.home_widget + +import android.content.Context +import android.content.Intent +import android.os.Handler +import android.util.Log +import androidx.core.app.JobIntentService +import io.flutter.FlutterInjector +import io.flutter.embedding.engine.FlutterEngine +import io.flutter.embedding.engine.dart.DartExecutor +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.view.FlutterCallbackInformation +import java.util.* +import java.util.concurrent.atomic.AtomicBoolean + +class HomeWidgetBackgroundService : MethodChannel.MethodCallHandler, JobIntentService() { + + private val queue = ArrayDeque>() + private lateinit var channel: MethodChannel + private lateinit var context: Context + + companion object { + private const val TAG = "HomeWidgetService" + private val JOB_ID = UUID.randomUUID().mostSignificantBits.toInt() + private var engine: FlutterEngine? = null + + private val serviceStarted = AtomicBoolean(false) + + fun enqueueWork(context: Context, work: Intent) { + enqueueWork(context, HomeWidgetBackgroundService::class.java, JOB_ID, work) + } + } + + override fun onCreate() { + super.onCreate() + synchronized(serviceStarted) { + context = this + if (engine == null) { + val callbackHandle = HomeWidgetPlugin.getDispatcherHandle(context) + + if (callbackHandle == 0L) { + Log.e(TAG, "No callbackHandle saved. Did you call HomeWidget.registerBackgroundCallback?") + } + + engine = FlutterEngine(context) + + val callbackInfo = + FlutterCallbackInformation.lookupCallbackInformation(callbackHandle) ?: return + + val callback = + DartExecutor.DartCallback( + context.assets, + FlutterInjector.instance().flutterLoader().findAppBundlePath(), + callbackInfo) + engine?.dartExecutor?.executeDartCallback(callback) + } + } + channel = + MethodChannel(engine!!.getDartExecutor().getBinaryMessenger(), "home_widget/background") + channel.setMethodCallHandler(this) + } + + override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { + if (call.method == "HomeWidget.backgroundInitialized") { + synchronized(serviceStarted) { + while (!queue.isEmpty()) { + channel.invokeMethod("", queue.remove()) + } + serviceStarted.set(true) + } + } + } + + override fun onHandleWork(intent: Intent) { + val data = intent.data?.toString() ?: "" + val args = listOf(HomeWidgetPlugin.getHandle(context), data) + + synchronized(serviceStarted) { + if (!serviceStarted.get()) { + queue.add(args) + } else { + Handler(context.mainLooper).post { channel.invokeMethod("", args) } + } + } + } +} diff --git a/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceState.kt b/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceState.kt new file mode 100644 index 00000000..92a07d20 --- /dev/null +++ b/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceState.kt @@ -0,0 +1,38 @@ +import android.content.Context +import android.content.SharedPreferences +import android.os.Environment +import androidx.datastore.core.DataStore +import androidx.glance.state.GlanceStateDefinition +import es.antonborri.home_widget.HomeWidgetPlugin +import java.io.File +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flow + +class HomeWidgetGlanceState(val preferences: SharedPreferences) + +class HomeWidgetGlanceStateDefinition : GlanceStateDefinition { + override suspend fun getDataStore( + context: Context, + fileKey: String + ): DataStore { + val preferences = + context.getSharedPreferences(HomeWidgetPlugin.PREFERENCES, Context.MODE_PRIVATE) + return HomeWidgetGlanceDataStore(preferences) + } + + override fun getLocation(context: Context, fileKey: String): File { + return Environment.getDataDirectory() + } +} + +private class HomeWidgetGlanceDataStore(private val preferences: SharedPreferences) : + DataStore { + override val data: Flow + get() = flow { emit(HomeWidgetGlanceState(preferences)) } + + override suspend fun updateData( + transform: suspend (t: HomeWidgetGlanceState) -> HomeWidgetGlanceState + ): HomeWidgetGlanceState { + return transform(HomeWidgetGlanceState(preferences)) + } +} diff --git a/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceWidgetReceiver.kt b/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceWidgetReceiver.kt new file mode 100644 index 00000000..e7904eb4 --- /dev/null +++ b/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetGlanceWidgetReceiver.kt @@ -0,0 +1,38 @@ +import android.appwidget.AppWidgetManager +import android.content.Context +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.GlanceAppWidgetManager +import androidx.glance.appwidget.GlanceAppWidgetReceiver +import androidx.glance.appwidget.state.updateAppWidgetState +import kotlinx.coroutines.runBlocking + +abstract class HomeWidgetGlanceWidgetReceiver : GlanceAppWidgetReceiver() { + + abstract override val glanceAppWidget: T + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + super.onUpdate(context, appWidgetManager, appWidgetIds) + runBlocking { + appWidgetIds.forEach { + val glanceId = GlanceAppWidgetManager(context).getGlanceIdBy(it) + glanceAppWidget.apply { + if (this.stateDefinition is HomeWidgetGlanceStateDefinition) { + // Must Update State + updateAppWidgetState( + context = context, + this.stateDefinition as HomeWidgetGlanceStateDefinition, + glanceId) { currentState -> + currentState + } + } + // Update widget. + update(context, glanceId) + } + } + } + } +} diff --git a/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt b/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt new file mode 100644 index 00000000..2560558e --- /dev/null +++ b/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetIntent.kt @@ -0,0 +1,66 @@ +package es.antonborri.home_widget + +import android.app.Activity +import android.app.ActivityOptions +import android.app.PendingIntent +import android.content.Context +import android.content.Intent +import android.net.Uri +import android.os.Build +import androidx.glance.action.Action +import androidx.glance.appwidget.action.actionStartActivity + +object HomeWidgetLaunchIntent { + + const val HOME_WIDGET_LAUNCH_ACTION = "es.antonborri.home_widget.action.LAUNCH" + + fun getActivity( + context: Context, + activityClass: Class, + uri: Uri? = null + ): PendingIntent where T : Activity { + val intent = Intent(context, activityClass) + intent.data = uri + intent.action = HOME_WIDGET_LAUNCH_ACTION + + var flags = PendingIntent.FLAG_UPDATE_CURRENT + if (Build.VERSION.SDK_INT >= 23) { + flags = flags or PendingIntent.FLAG_IMMUTABLE + } + + if (Build.VERSION.SDK_INT < 34) { + return PendingIntent.getActivity(context, 0, intent, flags) + } + + val options = ActivityOptions.makeBasic() + options.pendingIntentBackgroundActivityStartMode = + ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED + + return PendingIntent.getActivity(context, 0, intent, flags, options.toBundle()) + } +} + +inline fun actionStartActivity(context: Context, uri: Uri? = null): Action { + val intent = Intent(context, T::class.java) + intent.data = uri + intent.action = HomeWidgetLaunchIntent.HOME_WIDGET_LAUNCH_ACTION + + return actionStartActivity(intent) +} + +object HomeWidgetBackgroundIntent { + private const val HOME_WIDGET_BACKGROUND_ACTION = "es.antonborri.home_widget.action.BACKGROUND" + + fun getBroadcast(context: Context, uri: Uri? = null): PendingIntent { + val intent = Intent(context, HomeWidgetBackgroundReceiver::class.java) + intent.data = uri + intent.action = HOME_WIDGET_BACKGROUND_ACTION + + var flags = PendingIntent.FLAG_UPDATE_CURRENT + if (Build.VERSION.SDK_INT >= 23) { + flags = flags or PendingIntent.FLAG_IMMUTABLE + } + + return PendingIntent.getBroadcast(context, 0, intent, flags) + } +} diff --git a/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetPlugin.kt b/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetPlugin.kt new file mode 100644 index 00000000..1fc63c42 --- /dev/null +++ b/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetPlugin.kt @@ -0,0 +1,314 @@ +package es.antonborri.home_widget + +import android.app.Activity +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProviderInfo +import android.content.BroadcastReceiver +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.SharedPreferences +import android.os.Build +import io.flutter.embedding.engine.plugins.FlutterPlugin +import io.flutter.embedding.engine.plugins.activity.ActivityAware +import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding +import io.flutter.plugin.common.EventChannel +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import io.flutter.plugin.common.PluginRegistry + +/** HomeWidgetPlugin */ +class HomeWidgetPlugin : + FlutterPlugin, + MethodCallHandler, + ActivityAware, + EventChannel.StreamHandler, + PluginRegistry.NewIntentListener { + private lateinit var channel: MethodChannel + private lateinit var eventChannel: EventChannel + private lateinit var context: Context + + private var activity: Activity? = null + private var receiver: BroadcastReceiver? = null + private val doubleLongPrefix: String = "home_widget.double." + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "home_widget") + channel.setMethodCallHandler(this) + + eventChannel = EventChannel(flutterPluginBinding.binaryMessenger, "home_widget/updates") + eventChannel.setStreamHandler(this) + context = flutterPluginBinding.applicationContext + } + + override fun onMethodCall(call: MethodCall, result: Result) { + when (call.method) { + "saveWidgetData" -> { + if (call.hasArgument("id") && call.hasArgument("data")) { + val id = call.argument("id") + val data = call.argument("data") + val prefs = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE).edit() + if (data != null) { + prefs.putBoolean("$doubleLongPrefix$id", data is Double) + when (data) { + is Boolean -> prefs.putBoolean(id, data) + is Float -> prefs.putFloat(id, data) + is String -> prefs.putString(id, data) + is Double -> prefs.putLong(id, java.lang.Double.doubleToRawLongBits(data)) + is Int -> prefs.putInt(id, data) + is Long -> prefs.putLong(id, data) + else -> + result.error( + "-10", + "Invalid Type ${data!!::class.java.simpleName}. Supported types are Boolean, Float, String, Double, Long", + IllegalArgumentException()) + } + } else { + prefs.remove(id) + prefs.remove("$doubleLongPrefix$id") + } + result.success(prefs.commit()) + } else { + result.error( + "-1", + "InvalidArguments saveWidgetData must be called with id and data", + IllegalArgumentException()) + } + } + "getWidgetData" -> { + if (call.hasArgument("id")) { + val id = call.argument("id") + val defaultValue = call.argument("defaultValue") + + val prefs = context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE) + + val value = prefs.all[id] ?: defaultValue + + if (value is Long && prefs.getBoolean("$doubleLongPrefix$id", false)) { + result.success(java.lang.Double.longBitsToDouble(value)) + } else { + result.success(value) + } + } else { + result.error( + "-2", + "InvalidArguments getWidgetData must be called with id", + IllegalArgumentException()) + } + } + "updateWidget" -> { + val qualifiedName = call.argument("qualifiedAndroidName") + val className = call.argument("android") ?: call.argument("name") + try { + val javaClass = Class.forName(qualifiedName ?: "${context.packageName}.${className}") + val intent = Intent(context, javaClass) + intent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE + val ids: IntArray = + AppWidgetManager.getInstance(context.applicationContext) + .getAppWidgetIds(ComponentName(context, javaClass)) + intent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, ids) + context.sendBroadcast(intent) + result.success(true) + } catch (classException: ClassNotFoundException) { + result.error( + "-3", + "No Widget found with Name $className. Argument 'name' must be the same as your AppWidgetProvider you wish to update", + classException) + } + } + "setAppGroupId" -> { + result.success(true) + } + "initiallyLaunchedFromHomeWidget" -> { + return if (activity + ?.intent + ?.action + ?.equals(HomeWidgetLaunchIntent.HOME_WIDGET_LAUNCH_ACTION) == true) { + result.success(activity?.intent?.data?.toString() ?: "") + } else { + result.success(null) + } + } + "registerBackgroundCallback" -> { + val dispatcher = ((call.arguments as Iterable<*>).toList()[0] as Number).toLong() + val callback = ((call.arguments as Iterable<*>).toList()[1] as Number).toLong() + saveCallbackHandle(context, dispatcher, callback) + return result.success(true) + } + "isRequestPinWidgetSupported" -> { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return result.success(false) + } + + val appWidgetManager = AppWidgetManager.getInstance(context.applicationContext) + return result.success(appWidgetManager.isRequestPinAppWidgetSupported) + } + "requestPinWidget" -> { + if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) { + return result.success(null) + } + + val qualifiedName = call.argument("qualifiedAndroidName") + val className = call.argument("android") ?: call.argument("name") + + try { + val javaClass = Class.forName(qualifiedName ?: "${context.packageName}.${className}") + val myProvider = ComponentName(context, javaClass) + + val appWidgetManager = AppWidgetManager.getInstance(context.applicationContext) + + if (appWidgetManager.isRequestPinAppWidgetSupported) { + appWidgetManager.requestPinAppWidget(myProvider, null, null) + } + + return result.success(null) + } catch (classException: ClassNotFoundException) { + result.error( + "-4", + "No Widget found with Name $className. Argument 'name' must be the same as your AppWidgetProvider you wish to update", + classException) + } + } + "getInstalledWidgets" -> { + try { + val pinnedWidgetInfoList = getInstalledWidgets(context) + result.success(pinnedWidgetInfoList) + } catch (e: Exception) { + result.error("-5", "Failed to get installed widgets: ${e.message}", null) + } + } + else -> { + result.notImplemented() + } + } + } + + private fun getInstalledWidgets(context: Context): List> { + val pinnedWidgetInfoList = mutableListOf>() + val appWidgetManager = AppWidgetManager.getInstance(context.applicationContext) + val installedProviders = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + appWidgetManager.getInstalledProvidersForPackage(context.packageName, null) + } else { + appWidgetManager.installedProviders.filter { + it.provider.packageName == context.packageName + } + } + for (provider in installedProviders) { + val widgetIds = appWidgetManager.getAppWidgetIds(provider.provider) + for (widgetId in widgetIds) { + val widgetInfo = appWidgetManager.getAppWidgetInfo(widgetId) + pinnedWidgetInfoList.add(widgetInfoToMap(widgetId, widgetInfo)) + } + } + return pinnedWidgetInfoList + } + + private fun widgetInfoToMap(widgetId: Int, widgetInfo: AppWidgetProviderInfo): Map { + val label = + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { + widgetInfo.loadLabel(context.packageManager).toString() + } else { + @Suppress("DEPRECATION") widgetInfo.label + } + + return mapOf( + WIDGET_INFO_KEY_WIDGET_ID to widgetId, + WIDGET_INFO_KEY_ANDROID_CLASS_NAME to widgetInfo.provider.shortClassName, + WIDGET_INFO_KEY_LABEL to label) + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + companion object { + internal const val PREFERENCES = "HomeWidgetPreferences" + + private const val INTERNAL_PREFERENCES = "InternalHomeWidgetPreferences" + private const val CALLBACK_DISPATCHER_HANDLE = "callbackDispatcherHandle" + private const val CALLBACK_HANDLE = "callbackHandle" + + private const val WIDGET_INFO_KEY_WIDGET_ID = "widgetId" + private const val WIDGET_INFO_KEY_ANDROID_CLASS_NAME = "androidClassName" + private const val WIDGET_INFO_KEY_LABEL = "label" + + private fun saveCallbackHandle(context: Context, dispatcher: Long, handle: Long) { + context + .getSharedPreferences(INTERNAL_PREFERENCES, Context.MODE_PRIVATE) + .edit() + .putLong(CALLBACK_DISPATCHER_HANDLE, dispatcher) + .putLong(CALLBACK_HANDLE, handle) + .apply() + } + + fun getDispatcherHandle(context: Context): Long = + context + .getSharedPreferences(INTERNAL_PREFERENCES, Context.MODE_PRIVATE) + .getLong(CALLBACK_DISPATCHER_HANDLE, 0) + + fun getHandle(context: Context): Long = + context + .getSharedPreferences(INTERNAL_PREFERENCES, Context.MODE_PRIVATE) + .getLong(CALLBACK_HANDLE, 0) + + fun getData(context: Context): SharedPreferences = + context.getSharedPreferences(PREFERENCES, Context.MODE_PRIVATE) + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activity = binding.activity + binding.addOnNewIntentListener(this) + } + + override fun onDetachedFromActivityForConfigChanges() { + unregisterReceiver() + activity = null + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + activity = binding.activity + binding.addOnNewIntentListener(this) + } + + override fun onDetachedFromActivity() { + unregisterReceiver() + activity = null + } + + override fun onListen(arguments: Any?, events: EventChannel.EventSink?) { + receiver = createReceiver(events) + } + + override fun onCancel(arguments: Any?) { + unregisterReceiver() + receiver = null + } + + private fun createReceiver(events: EventChannel.EventSink?): BroadcastReceiver { + return object : BroadcastReceiver() { + override fun onReceive(context: Context?, intent: Intent?) { + if (intent?.action.equals(HomeWidgetLaunchIntent.HOME_WIDGET_LAUNCH_ACTION)) { + events?.success(intent?.data?.toString() ?: true) + } + } + } + } + + private fun unregisterReceiver() { + try { + if (receiver != null) { + context.unregisterReceiver(receiver) + } + } catch (e: IllegalArgumentException) { + // Receiver not registered + } + } + + override fun onNewIntent(intent: Intent): Boolean { + receiver?.onReceive(context, intent) + return receiver != null + } +} diff --git a/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetProvider.kt b/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetProvider.kt new file mode 100644 index 00000000..e8e1cc44 --- /dev/null +++ b/packages/home_widget/android/src/main/kotlin/es/antonborri/home_widget/HomeWidgetProvider.kt @@ -0,0 +1,25 @@ +package es.antonborri.home_widget + +import android.appwidget.AppWidgetManager +import android.appwidget.AppWidgetProvider +import android.content.Context +import android.content.SharedPreferences + +abstract class HomeWidgetProvider : AppWidgetProvider() { + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray + ) { + super.onUpdate(context, appWidgetManager, appWidgetIds) + onUpdate(context, appWidgetManager, appWidgetIds, HomeWidgetPlugin.getData(context)) + } + + abstract fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray, + widgetData: SharedPreferences + ) +} diff --git a/dart_test.yaml b/packages/home_widget/dart_test.yaml similarity index 100% rename from dart_test.yaml rename to packages/home_widget/dart_test.yaml diff --git a/example/.gitignore b/packages/home_widget/example/.gitignore similarity index 100% rename from example/.gitignore rename to packages/home_widget/example/.gitignore diff --git a/example/.metadata b/packages/home_widget/example/.metadata similarity index 65% rename from example/.metadata rename to packages/home_widget/example/.metadata index 78a65c5a..607910d0 100644 --- a/example/.metadata +++ b/packages/home_widget/example/.metadata @@ -21,18 +21,6 @@ migration: - platform: ios create_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 base_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 - - platform: linux - create_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 - base_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 - - platform: macos - create_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 - base_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 - - platform: web - create_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 - base_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 - - platform: windows - create_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 - base_revision: 9e1c857886f07d342cf106f2cd588bcd5e031bb2 # User provided section diff --git a/example/README.md b/packages/home_widget/example/README.md similarity index 100% rename from example/README.md rename to packages/home_widget/example/README.md diff --git a/example/analysis_options.yaml b/packages/home_widget/example/analysis_options.yaml similarity index 100% rename from example/analysis_options.yaml rename to packages/home_widget/example/analysis_options.yaml diff --git a/example/android/.gitignore b/packages/home_widget/example/android/.gitignore similarity index 100% rename from example/android/.gitignore rename to packages/home_widget/example/android/.gitignore diff --git a/example/android/app/build.gradle b/packages/home_widget/example/android/app/build.gradle similarity index 100% rename from example/android/app/build.gradle rename to packages/home_widget/example/android/app/build.gradle diff --git a/example/android/app/proguard-rules.pro b/packages/home_widget/example/android/app/proguard-rules.pro similarity index 100% rename from example/android/app/proguard-rules.pro rename to packages/home_widget/example/android/app/proguard-rules.pro diff --git a/example/android/app/src/debug/AndroidManifest.xml b/packages/home_widget/example/android/app/src/debug/AndroidManifest.xml similarity index 100% rename from example/android/app/src/debug/AndroidManifest.xml rename to packages/home_widget/example/android/app/src/debug/AndroidManifest.xml diff --git a/example/android/app/src/main/AndroidManifest.xml b/packages/home_widget/example/android/app/src/main/AndroidManifest.xml similarity index 100% rename from example/android/app/src/main/AndroidManifest.xml rename to packages/home_widget/example/android/app/src/main/AndroidManifest.xml diff --git a/packages/home_widget/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/HomeWidgetExampleProvider.kt b/packages/home_widget/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/HomeWidgetExampleProvider.kt new file mode 100644 index 00000000..469f2b9a --- /dev/null +++ b/packages/home_widget/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/HomeWidgetExampleProvider.kt @@ -0,0 +1,61 @@ +package es.antonborri.home_widget_example + +import android.appwidget.AppWidgetManager +import android.content.Context +import android.content.SharedPreferences +import android.graphics.BitmapFactory +import android.net.Uri +import android.view.View +import android.widget.RemoteViews +import es.antonborri.home_widget.HomeWidgetBackgroundIntent +import es.antonborri.home_widget.HomeWidgetLaunchIntent +import es.antonborri.home_widget.HomeWidgetProvider + +class HomeWidgetExampleProvider : HomeWidgetProvider() { + + override fun onUpdate( + context: Context, + appWidgetManager: AppWidgetManager, + appWidgetIds: IntArray, + widgetData: SharedPreferences + ) { + appWidgetIds.forEach { widgetId -> + val views = + RemoteViews(context.packageName, R.layout.example_layout).apply { + // Open App on Widget Click + val pendingIntent = + HomeWidgetLaunchIntent.getActivity(context, MainActivity::class.java) + setOnClickPendingIntent(R.id.widget_container, pendingIntent) + + // Swap Title Text by calling Dart Code in the Background + setTextViewText( + R.id.widget_title, widgetData.getString("title", null) ?: "No Title Set") + val backgroundIntent = + HomeWidgetBackgroundIntent.getBroadcast( + context, Uri.parse("homeWidgetExample://titleClicked")) + setOnClickPendingIntent(R.id.widget_title, backgroundIntent) + + val message = widgetData.getString("message", null) + setTextViewText(R.id.widget_message, message ?: "No Message Set") + // Show Images saved with `renderFlutterWidget` + val image = widgetData.getString("dashIcon", null) + if (image != null) { + setImageViewBitmap(R.id.widget_img, BitmapFactory.decodeFile(image)) + setViewVisibility(R.id.widget_img, View.VISIBLE) + } else { + setViewVisibility(R.id.widget_img, View.GONE) + } + + // Detect App opened via Click inside Flutter + val pendingIntentWithData = + HomeWidgetLaunchIntent.getActivity( + context, + MainActivity::class.java, + Uri.parse("homeWidgetExample://message?message=$message")) + setOnClickPendingIntent(R.id.widget_message, pendingIntentWithData) + } + + appWidgetManager.updateAppWidget(widgetId, views) + } + } +} diff --git a/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/MainActivity.kt b/packages/home_widget/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/MainActivity.kt similarity index 69% rename from example/android/app/src/main/kotlin/es/antonborri/home_widget_example/MainActivity.kt rename to packages/home_widget/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/MainActivity.kt index 75a3e68e..e44ebb2d 100644 --- a/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/MainActivity.kt +++ b/packages/home_widget/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/MainActivity.kt @@ -2,5 +2,4 @@ package es.antonborri.home_widget_example import io.flutter.embedding.android.FlutterActivity -class MainActivity : FlutterActivity() { -} +class MainActivity : FlutterActivity() {} diff --git a/packages/home_widget/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt b/packages/home_widget/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt new file mode 100644 index 00000000..c811e307 --- /dev/null +++ b/packages/home_widget/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetGlanceAppWidget.kt @@ -0,0 +1,98 @@ +package es.antonborri.home_widget_example.glance + +import HomeWidgetGlanceState +import HomeWidgetGlanceStateDefinition +import android.content.Context +import android.graphics.BitmapFactory +import android.net.Uri +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp +import androidx.glance.GlanceId +import androidx.glance.GlanceModifier +import androidx.glance.Image +import androidx.glance.action.ActionParameters +import androidx.glance.action.clickable +import androidx.glance.appwidget.GlanceAppWidget +import androidx.glance.appwidget.action.ActionCallback +import androidx.glance.appwidget.action.actionRunCallback +import androidx.glance.appwidget.provideContent +import androidx.glance.background +import androidx.glance.currentState +import androidx.glance.layout.Alignment +import androidx.glance.layout.Box +import androidx.glance.layout.Column +import androidx.glance.layout.fillMaxSize +import androidx.glance.layout.padding +import androidx.glance.text.FontWeight +import androidx.glance.text.Text +import androidx.glance.text.TextStyle +import es.antonborri.home_widget.HomeWidgetBackgroundIntent +import es.antonborri.home_widget.actionStartActivity +import es.antonborri.home_widget_example.MainActivity + +class HomeWidgetGlanceAppWidget : GlanceAppWidget() { + + /** Needed for Updating */ + override val stateDefinition = HomeWidgetGlanceStateDefinition() + + override suspend fun provideGlance(context: Context, id: GlanceId) { + provideContent { GlanceContent(context, currentState()) } + } + + @Composable + private fun GlanceContent(context: Context, currentState: HomeWidgetGlanceState) { + val data = currentState.preferences + val imagePath = data.getString("dashIcon", null) + + val title = data.getString("title", "")!! + val message = data.getString("message", "")!! + + Box( + modifier = + GlanceModifier.background(Color.White) + .padding(16.dp) + .clickable(onClick = actionStartActivity(context))) { + Column( + modifier = GlanceModifier.fillMaxSize(), + verticalAlignment = Alignment.Vertical.Top, + horizontalAlignment = Alignment.Horizontal.Start, + ) { + Text("Glance") + Text( + title, + style = TextStyle(fontSize = 36.sp, fontWeight = FontWeight.Bold), + modifier = + GlanceModifier.clickable(onClick = actionRunCallback()), + ) + Text( + message, + style = TextStyle(fontSize = 18.sp), + modifier = + GlanceModifier.clickable( + onClick = + actionStartActivity( + context, + Uri.parse("homeWidgetExample://message?message=$message")))) + imagePath?.let { + val bitmap = BitmapFactory.decodeFile(it) + Image(androidx.glance.ImageProvider(bitmap), null) + } + } + } + } +} + +class InteractiveAction : ActionCallback { + override suspend fun onAction( + context: Context, + glanceId: GlanceId, + parameters: ActionParameters + ) { + val backgroundIntent = + HomeWidgetBackgroundIntent.getBroadcast( + context, Uri.parse("homeWidgetExample://titleClicked")) + backgroundIntent.send() + } +} diff --git a/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetReceiver.kt b/packages/home_widget/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetReceiver.kt similarity index 73% rename from example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetReceiver.kt rename to packages/home_widget/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetReceiver.kt index b9eb5ba9..33e56bf2 100644 --- a/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetReceiver.kt +++ b/packages/home_widget/example/android/app/src/main/kotlin/es/antonborri/home_widget_example/glance/HomeWidgetReceiver.kt @@ -3,5 +3,5 @@ package es.antonborri.home_widget_example.glance import HomeWidgetGlanceWidgetReceiver class HomeWidgetReceiver : HomeWidgetGlanceWidgetReceiver() { - override val glanceAppWidget = HomeWidgetGlanceAppWidget() -} \ No newline at end of file + override val glanceAppWidget = HomeWidgetGlanceAppWidget() +} diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/packages/home_widget/example/android/app/src/main/res/drawable/launch_background.xml similarity index 100% rename from example/android/app/src/main/res/drawable/launch_background.xml rename to packages/home_widget/example/android/app/src/main/res/drawable/launch_background.xml diff --git a/example/android/app/src/main/res/drawable/widget_background.xml b/packages/home_widget/example/android/app/src/main/res/drawable/widget_background.xml similarity index 100% rename from example/android/app/src/main/res/drawable/widget_background.xml rename to packages/home_widget/example/android/app/src/main/res/drawable/widget_background.xml diff --git a/example/android/app/src/main/res/layout/example_layout.xml b/packages/home_widget/example/android/app/src/main/res/layout/example_layout.xml similarity index 100% rename from example/android/app/src/main/res/layout/example_layout.xml rename to packages/home_widget/example/android/app/src/main/res/layout/example_layout.xml diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/packages/home_widget/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png rename to packages/home_widget/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/packages/home_widget/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png rename to packages/home_widget/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/packages/home_widget/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png rename to packages/home_widget/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/packages/home_widget/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png rename to packages/home_widget/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/packages/home_widget/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png similarity index 100% rename from example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png rename to packages/home_widget/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/example/android/app/src/main/res/values/styles.xml b/packages/home_widget/example/android/app/src/main/res/values/styles.xml similarity index 100% rename from example/android/app/src/main/res/values/styles.xml rename to packages/home_widget/example/android/app/src/main/res/values/styles.xml diff --git a/example/android/app/src/main/res/xml/home_widget_example.xml b/packages/home_widget/example/android/app/src/main/res/xml/home_widget_example.xml similarity index 100% rename from example/android/app/src/main/res/xml/home_widget_example.xml rename to packages/home_widget/example/android/app/src/main/res/xml/home_widget_example.xml diff --git a/example/android/app/src/main/res/xml/home_widget_glance_example.xml b/packages/home_widget/example/android/app/src/main/res/xml/home_widget_glance_example.xml similarity index 100% rename from example/android/app/src/main/res/xml/home_widget_glance_example.xml rename to packages/home_widget/example/android/app/src/main/res/xml/home_widget_glance_example.xml diff --git a/example/android/app/src/profile/AndroidManifest.xml b/packages/home_widget/example/android/app/src/profile/AndroidManifest.xml similarity index 100% rename from example/android/app/src/profile/AndroidManifest.xml rename to packages/home_widget/example/android/app/src/profile/AndroidManifest.xml diff --git a/example/android/build.gradle b/packages/home_widget/example/android/build.gradle similarity index 100% rename from example/android/build.gradle rename to packages/home_widget/example/android/build.gradle diff --git a/example/android/gradle.properties b/packages/home_widget/example/android/gradle.properties similarity index 100% rename from example/android/gradle.properties rename to packages/home_widget/example/android/gradle.properties diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/packages/home_widget/example/android/gradle/wrapper/gradle-wrapper.properties similarity index 100% rename from example/android/gradle/wrapper/gradle-wrapper.properties rename to packages/home_widget/example/android/gradle/wrapper/gradle-wrapper.properties diff --git a/example/android/settings.gradle b/packages/home_widget/example/android/settings.gradle similarity index 100% rename from example/android/settings.gradle rename to packages/home_widget/example/android/settings.gradle diff --git a/example/integration_test/android_test.dart b/packages/home_widget/example/integration_test/android_test.dart similarity index 100% rename from example/integration_test/android_test.dart rename to packages/home_widget/example/integration_test/android_test.dart diff --git a/example/integration_test/ios_test.dart b/packages/home_widget/example/integration_test/ios_test.dart similarity index 100% rename from example/integration_test/ios_test.dart rename to packages/home_widget/example/integration_test/ios_test.dart diff --git a/example/ios/.gitignore b/packages/home_widget/example/ios/.gitignore similarity index 100% rename from example/ios/.gitignore rename to packages/home_widget/example/ios/.gitignore diff --git a/example/ios/Flutter/AppFrameworkInfo.plist b/packages/home_widget/example/ios/Flutter/AppFrameworkInfo.plist similarity index 100% rename from example/ios/Flutter/AppFrameworkInfo.plist rename to packages/home_widget/example/ios/Flutter/AppFrameworkInfo.plist diff --git a/example/ios/Flutter/Debug.xcconfig b/packages/home_widget/example/ios/Flutter/Debug.xcconfig similarity index 100% rename from example/ios/Flutter/Debug.xcconfig rename to packages/home_widget/example/ios/Flutter/Debug.xcconfig diff --git a/example/ios/Flutter/Release.xcconfig b/packages/home_widget/example/ios/Flutter/Release.xcconfig similarity index 100% rename from example/ios/Flutter/Release.xcconfig rename to packages/home_widget/example/ios/Flutter/Release.xcconfig diff --git a/example/ios/HomeWidgetExample/Assets.xcassets/AccentColor.colorset/Contents.json b/packages/home_widget/example/ios/HomeWidgetExample/Assets.xcassets/AccentColor.colorset/Contents.json similarity index 100% rename from example/ios/HomeWidgetExample/Assets.xcassets/AccentColor.colorset/Contents.json rename to packages/home_widget/example/ios/HomeWidgetExample/Assets.xcassets/AccentColor.colorset/Contents.json diff --git a/example/ios/HomeWidgetExample/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/home_widget/example/ios/HomeWidgetExample/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from example/ios/HomeWidgetExample/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/home_widget/example/ios/HomeWidgetExample/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/example/ios/HomeWidgetExample/Assets.xcassets/Contents.json b/packages/home_widget/example/ios/HomeWidgetExample/Assets.xcassets/Contents.json similarity index 100% rename from example/ios/HomeWidgetExample/Assets.xcassets/Contents.json rename to packages/home_widget/example/ios/HomeWidgetExample/Assets.xcassets/Contents.json diff --git a/example/ios/HomeWidgetExample/Assets.xcassets/WidgetBackground.colorset/Contents.json b/packages/home_widget/example/ios/HomeWidgetExample/Assets.xcassets/WidgetBackground.colorset/Contents.json similarity index 100% rename from example/ios/HomeWidgetExample/Assets.xcassets/WidgetBackground.colorset/Contents.json rename to packages/home_widget/example/ios/HomeWidgetExample/Assets.xcassets/WidgetBackground.colorset/Contents.json diff --git a/example/ios/HomeWidgetExample/HomeWidgetExample.swift b/packages/home_widget/example/ios/HomeWidgetExample/HomeWidgetExample.swift similarity index 91% rename from example/ios/HomeWidgetExample/HomeWidgetExample.swift rename to packages/home_widget/example/ios/HomeWidgetExample/HomeWidgetExample.swift index db0ffa7e..76c1b701 100644 --- a/example/ios/HomeWidgetExample/HomeWidgetExample.swift +++ b/packages/home_widget/example/ios/HomeWidgetExample/HomeWidgetExample.swift @@ -50,17 +50,21 @@ struct HomeWidgetExampleEntryView: View { var body: some View { VStack.init( - alignment: .center, spacing: /*@START_MENU_TOKEN@*/ nil /*@END_MENU_TOKEN@*/, + alignment: .center, + spacing: nil, content: { if #available(iOSApplicationExtension 17, *) { Button( intent: BackgroundIntent( url: URL(string: "homeWidgetExample://titleClicked"), appGroup: widgetGroupId) ) { - Text(entry.title).bold().font( /*@START_MENU_TOKEN@*/.title /*@END_MENU_TOKEN@*/) + Text(entry.title).bold().font( + .title) }.buttonStyle(.plain).frame(maxWidth: .infinity, alignment: .leading) } else { - Text(entry.title).bold().font( /*@START_MENU_TOKEN@*/.title /*@END_MENU_TOKEN@*/).frame( + Text(entry.title).bold().font( + .title + ).frame( maxWidth: .infinity, alignment: .leading) } Text(entry.message) diff --git a/example/ios/HomeWidgetExample/Info.plist b/packages/home_widget/example/ios/HomeWidgetExample/Info.plist similarity index 100% rename from example/ios/HomeWidgetExample/Info.plist rename to packages/home_widget/example/ios/HomeWidgetExample/Info.plist diff --git a/example/ios/HomeWidgetExampleExtension.entitlements b/packages/home_widget/example/ios/HomeWidgetExampleExtension.entitlements similarity index 100% rename from example/ios/HomeWidgetExampleExtension.entitlements rename to packages/home_widget/example/ios/HomeWidgetExampleExtension.entitlements diff --git a/example/ios/Podfile b/packages/home_widget/example/ios/Podfile similarity index 100% rename from example/ios/Podfile rename to packages/home_widget/example/ios/Podfile diff --git a/example/ios/Runner.xcodeproj/project.pbxproj b/packages/home_widget/example/ios/Runner.xcodeproj/project.pbxproj similarity index 100% rename from example/ios/Runner.xcodeproj/project.pbxproj rename to packages/home_widget/example/ios/Runner.xcodeproj/project.pbxproj diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/home_widget/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/home_widget/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/home_widget/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to packages/home_widget/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme b/packages/home_widget/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme similarity index 100% rename from example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme rename to packages/home_widget/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme diff --git a/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist b/packages/home_widget/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist similarity index 100% rename from example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist rename to packages/home_widget/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist diff --git a/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings b/packages/home_widget/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings similarity index 100% rename from example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings rename to packages/home_widget/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings diff --git a/example/ios/Runner/AppDelegate.swift b/packages/home_widget/example/ios/Runner/AppDelegate.swift similarity index 100% rename from example/ios/Runner/AppDelegate.swift rename to packages/home_widget/example/ios/Runner/AppDelegate.swift diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json b/packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json rename to packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png diff --git a/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json b/packages/home_widget/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json rename to packages/home_widget/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png b/packages/home_widget/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png rename to packages/home_widget/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png diff --git a/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md b/packages/home_widget/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md similarity index 100% rename from example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md rename to packages/home_widget/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md diff --git a/example/ios/Runner/BackgroundIntent.swift b/packages/home_widget/example/ios/Runner/BackgroundIntent.swift similarity index 100% rename from example/ios/Runner/BackgroundIntent.swift rename to packages/home_widget/example/ios/Runner/BackgroundIntent.swift diff --git a/example/ios/Runner/Base.lproj/LaunchScreen.storyboard b/packages/home_widget/example/ios/Runner/Base.lproj/LaunchScreen.storyboard similarity index 100% rename from example/ios/Runner/Base.lproj/LaunchScreen.storyboard rename to packages/home_widget/example/ios/Runner/Base.lproj/LaunchScreen.storyboard diff --git a/example/ios/Runner/Base.lproj/Main.storyboard b/packages/home_widget/example/ios/Runner/Base.lproj/Main.storyboard similarity index 100% rename from example/ios/Runner/Base.lproj/Main.storyboard rename to packages/home_widget/example/ios/Runner/Base.lproj/Main.storyboard diff --git a/example/ios/Runner/Info.plist b/packages/home_widget/example/ios/Runner/Info.plist similarity index 100% rename from example/ios/Runner/Info.plist rename to packages/home_widget/example/ios/Runner/Info.plist diff --git a/example/ios/Runner/Runner-Bridging-Header.h b/packages/home_widget/example/ios/Runner/Runner-Bridging-Header.h similarity index 100% rename from example/ios/Runner/Runner-Bridging-Header.h rename to packages/home_widget/example/ios/Runner/Runner-Bridging-Header.h diff --git a/example/ios/Runner/Runner.entitlements b/packages/home_widget/example/ios/Runner/Runner.entitlements similarity index 100% rename from example/ios/Runner/Runner.entitlements rename to packages/home_widget/example/ios/Runner/Runner.entitlements diff --git a/example/ios/RunnerTests/RunnerTests.swift b/packages/home_widget/example/ios/RunnerTests/RunnerTests.swift similarity index 100% rename from example/ios/RunnerTests/RunnerTests.swift rename to packages/home_widget/example/ios/RunnerTests/RunnerTests.swift diff --git a/example/lib/main.dart b/packages/home_widget/example/lib/main.dart similarity index 100% rename from example/lib/main.dart rename to packages/home_widget/example/lib/main.dart diff --git a/example/pubspec.yaml b/packages/home_widget/example/pubspec.yaml similarity index 100% rename from example/pubspec.yaml rename to packages/home_widget/example/pubspec.yaml diff --git a/example/test_driver/integration_test.dart b/packages/home_widget/example/test_driver/integration_test.dart similarity index 100% rename from example/test_driver/integration_test.dart rename to packages/home_widget/example/test_driver/integration_test.dart diff --git a/ios/.gitignore b/packages/home_widget/ios/.gitignore similarity index 100% rename from ios/.gitignore rename to packages/home_widget/ios/.gitignore diff --git a/ios/Assets/.gitkeep b/packages/home_widget/ios/Assets/.gitkeep similarity index 100% rename from ios/Assets/.gitkeep rename to packages/home_widget/ios/Assets/.gitkeep diff --git a/ios/Classes/HomeWidgetBackgroundWorker.swift b/packages/home_widget/ios/Classes/HomeWidgetBackgroundWorker.swift similarity index 100% rename from ios/Classes/HomeWidgetBackgroundWorker.swift rename to packages/home_widget/ios/Classes/HomeWidgetBackgroundWorker.swift diff --git a/ios/Classes/HomeWidgetPlugin.h b/packages/home_widget/ios/Classes/HomeWidgetPlugin.h similarity index 100% rename from ios/Classes/HomeWidgetPlugin.h rename to packages/home_widget/ios/Classes/HomeWidgetPlugin.h diff --git a/ios/Classes/HomeWidgetPlugin.m b/packages/home_widget/ios/Classes/HomeWidgetPlugin.m similarity index 100% rename from ios/Classes/HomeWidgetPlugin.m rename to packages/home_widget/ios/Classes/HomeWidgetPlugin.m diff --git a/ios/Classes/SwiftHomeWidgetPlugin.swift b/packages/home_widget/ios/Classes/SwiftHomeWidgetPlugin.swift similarity index 95% rename from ios/Classes/SwiftHomeWidgetPlugin.swift rename to packages/home_widget/ios/Classes/SwiftHomeWidgetPlugin.swift index cd05fbba..45465b99 100644 --- a/ios/Classes/SwiftHomeWidgetPlugin.swift +++ b/packages/home_widget/ios/Classes/SwiftHomeWidgetPlugin.swift @@ -175,13 +175,17 @@ public class SwiftHomeWidgetPlugin: NSObject, FlutterPlugin, FlutterStreamHandle #if arch(arm64) || arch(i386) || arch(x86_64) WidgetCenter.shared.getCurrentConfigurations { result2 in switch result2 { - case let .success(widgets): + case .success(let widgets): let widgetInfoList = widgets.map { widget in - return ["family": "\(widget.family)", "kind": widget.kind] + return ["family": "\(widget.family)", "kind": widget.kind] } result(widgetInfoList) - case let .failure(error): - result(FlutterError(code: "-8", message: "Failed to get installed widgets: \(error.localizedDescription)", details: nil)) + case .failure(let error): + result( + FlutterError( + code: "-8", + message: "Failed to get installed widgets: \(error.localizedDescription)", + details: nil)) } } #endif diff --git a/ios/home_widget.podspec b/packages/home_widget/ios/home_widget.podspec similarity index 100% rename from ios/home_widget.podspec rename to packages/home_widget/ios/home_widget.podspec diff --git a/lib/home_widget.dart b/packages/home_widget/lib/home_widget.dart similarity index 100% rename from lib/home_widget.dart rename to packages/home_widget/lib/home_widget.dart diff --git a/lib/src/home_widget.dart b/packages/home_widget/lib/src/home_widget.dart similarity index 100% rename from lib/src/home_widget.dart rename to packages/home_widget/lib/src/home_widget.dart diff --git a/lib/src/home_widget_callback_dispatcher.dart b/packages/home_widget/lib/src/home_widget_callback_dispatcher.dart similarity index 100% rename from lib/src/home_widget_callback_dispatcher.dart rename to packages/home_widget/lib/src/home_widget_callback_dispatcher.dart diff --git a/lib/src/home_widget_info.dart b/packages/home_widget/lib/src/home_widget_info.dart similarity index 100% rename from lib/src/home_widget_info.dart rename to packages/home_widget/lib/src/home_widget_info.dart diff --git a/packages/home_widget/pubspec.yaml b/packages/home_widget/pubspec.yaml new file mode 100644 index 00000000..7db57f12 --- /dev/null +++ b/packages/home_widget/pubspec.yaml @@ -0,0 +1,32 @@ +name: home_widget +description: A plugin to provide a common interface for creating HomeScreen Widgets for Android and iOS. +version: 0.6.0 +repository: https://github.com/ABausG/home_widget + +environment: + sdk: '>=3.4.0 <4.0.0' + flutter: '>=3.20.0-1.2.pre' + +dependencies: + flutter: + sdk: flutter + path_provider: ^2.1.3 + path_provider_foundation: ^2.4.0 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^3.0.1 + golden_toolkit: ^0.15.0 + mocktail: ^1.0.3 + path_provider_platform_interface: + plugin_platform_interface: + +flutter: + plugin: + platforms: + android: + package: es.antonborri.home_widget + pluginClass: HomeWidgetPlugin + ios: + pluginClass: HomeWidgetPlugin diff --git a/test/background_test.dart b/packages/home_widget/test/background_test.dart similarity index 100% rename from test/background_test.dart rename to packages/home_widget/test/background_test.dart diff --git a/test/goldens/render-flutter-widget.png b/packages/home_widget/test/goldens/render-flutter-widget.png similarity index 100% rename from test/goldens/render-flutter-widget.png rename to packages/home_widget/test/goldens/render-flutter-widget.png diff --git a/test/home_widget_info_test.dart b/packages/home_widget/test/home_widget_info_test.dart similarity index 100% rename from test/home_widget_info_test.dart rename to packages/home_widget/test/home_widget_info_test.dart diff --git a/test/home_widget_test.dart b/packages/home_widget/test/home_widget_test.dart similarity index 100% rename from test/home_widget_test.dart rename to packages/home_widget/test/home_widget_test.dart diff --git a/test/mocks.dart b/packages/home_widget/test/mocks.dart similarity index 100% rename from test/mocks.dart rename to packages/home_widget/test/mocks.dart diff --git a/pubspec.yaml b/pubspec.yaml index 7db57f12..cb06ba96 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,32 +1,6 @@ -name: home_widget -description: A plugin to provide a common interface for creating HomeScreen Widgets for Android and iOS. -version: 0.6.0 -repository: https://github.com/ABausG/home_widget +name: home_widget_workspace environment: - sdk: '>=3.4.0 <4.0.0' - flutter: '>=3.20.0-1.2.pre' - + sdk: '>=3.0.0 <4.0.0' dependencies: - flutter: - sdk: flutter - path_provider: ^2.1.3 - path_provider_foundation: ^2.4.0 - -dev_dependencies: - flutter_test: - sdk: flutter - flutter_lints: ^3.0.1 - golden_toolkit: ^0.15.0 - mocktail: ^1.0.3 - path_provider_platform_interface: - plugin_platform_interface: - -flutter: - plugin: - platforms: - android: - package: es.antonborri.home_widget - pluginClass: HomeWidgetPlugin - ios: - pluginClass: HomeWidgetPlugin + melos: ^6.1.0