├── app_widget ├── lib │ ├── app_widget.dart │ └── src │ │ └── app_widget_plugin.dart ├── .fvm │ └── fvm_config.json ├── assets │ ├── example_app.gif │ └── screen_shot.webp ├── example │ ├── android │ │ ├── gradle.properties │ │ ├── app │ │ │ ├── src │ │ │ │ ├── main │ │ │ │ │ ├── res │ │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ │ ├── widget_background.xml │ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ │ ├── drawable │ │ │ │ │ │ │ ├── widget_background.xml │ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ │ ├── drawable-night │ │ │ │ │ │ │ ├── widget_background.xml │ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ │ ├── xml │ │ │ │ │ │ │ └── app_widget_example_info.xml │ │ │ │ │ │ ├── values │ │ │ │ │ │ │ └── styles.xml │ │ │ │ │ │ ├── values-night │ │ │ │ │ │ │ └── styles.xml │ │ │ │ │ │ └── layout │ │ │ │ │ │ │ └── example_layout.xml │ │ │ │ │ ├── kotlin │ │ │ │ │ │ └── tech │ │ │ │ │ │ │ └── noxasch │ │ │ │ │ │ │ └── app_widget_example │ │ │ │ │ │ │ ├── MainActivity.kt │ │ │ │ │ │ │ ├── AppWidgetExampleDiffProvider.kt │ │ │ │ │ │ │ └── AppWidgetExampleProvider.kt │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ ├── debug │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ └── profile │ │ │ │ │ └── AndroidManifest.xml │ │ │ └── build.gradle │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ └── gradle-wrapper.properties │ │ ├── .gitignore │ │ ├── settings.gradle │ │ └── build.gradle │ ├── README.md │ ├── .gitignore │ ├── pubspec.yaml │ ├── test │ │ └── widget_test.dart │ ├── analysis_options.yaml │ ├── integration_test │ │ ├── android_test_2.dart │ │ ├── android_test.dart │ │ └── android_diff_package_name_test.dart │ ├── pubspec.lock │ └── lib │ │ └── main.dart ├── .gitignore ├── pubspec.yaml ├── .metadata ├── LICENSE ├── CHANGELOG.md ├── test │ ├── app_widget_ios_test.dart │ └── app_widget_test.dart └── README.md ├── app_widget_android ├── android │ ├── settings.gradle │ ├── .gitignore │ ├── src │ │ └── main │ │ │ ├── AndroidManifest.xml │ │ │ └── kotlin │ │ │ └── tech │ │ │ └── noxasch │ │ │ └── app_widget │ │ │ ├── AppWidgetPlugin.kt │ │ │ └── AppWidgetMethodCallHandler.kt │ └── build.gradle ├── example │ ├── android │ │ ├── gradle.properties │ │ ├── app │ │ │ ├── src │ │ │ │ ├── main │ │ │ │ │ ├── res │ │ │ │ │ │ ├── mipmap-hdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── mipmap-mdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── mipmap-xhdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── mipmap-xxhdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── mipmap-xxxhdpi │ │ │ │ │ │ │ └── ic_launcher.png │ │ │ │ │ │ ├── drawable │ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ │ ├── drawable-v21 │ │ │ │ │ │ │ └── launch_background.xml │ │ │ │ │ │ ├── values │ │ │ │ │ │ │ └── styles.xml │ │ │ │ │ │ └── values-night │ │ │ │ │ │ │ └── styles.xml │ │ │ │ │ ├── kotlin │ │ │ │ │ │ └── tech │ │ │ │ │ │ │ └── noxasch │ │ │ │ │ │ │ └── app_widget_android_example │ │ │ │ │ │ │ └── MainActivity.kt │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ ├── debug │ │ │ │ │ └── AndroidManifest.xml │ │ │ │ └── profile │ │ │ │ │ └── AndroidManifest.xml │ │ │ └── build.gradle │ │ ├── gradle │ │ │ └── wrapper │ │ │ │ └── gradle-wrapper.properties │ │ ├── .gitignore │ │ ├── settings.gradle │ │ └── build.gradle │ ├── README.md │ ├── lib │ │ └── main.dart │ ├── .gitignore │ ├── test │ │ └── widget_test.dart │ ├── analysis_options.yaml │ ├── pubspec.yaml │ └── pubspec.lock ├── lib │ ├── app_widget_android.dart │ └── src │ │ ├── app_widget_android_platform.dart │ │ └── app_widget_android_plugin.dart ├── README.md ├── .gitignore ├── pubspec.yaml ├── .metadata ├── LICENSE ├── CHANGELOG.md └── test │ └── app_widget_android_test.dart ├── app_widget_platform_interface ├── lib │ ├── app_widget_platform_interface.dart │ └── src │ │ └── app_widget_platform.dart ├── pubspec.yaml ├── .gitignore ├── README.md ├── .metadata ├── CHANGELOG.md └── LICENSE ├── .github ├── ISSUE_TEMPLATE │ ├── feature_request.md │ └── bug_report.md └── workflows │ ├── android.yaml │ ├── interface.yaml │ └── main.yaml ├── README.md ├── LICENSE └── analysis_options.yaml /app_widget/lib/app_widget.dart: -------------------------------------------------------------------------------- 1 | export 'src/app_widget_plugin.dart'; 2 | -------------------------------------------------------------------------------- /app_widget_android/android/settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'app_widget_android' 2 | -------------------------------------------------------------------------------- /app_widget/.fvm/fvm_config.json: -------------------------------------------------------------------------------- 1 | { 2 | "flutterSdkVersion": "3.13.8", 3 | "flavors": {} 4 | } -------------------------------------------------------------------------------- /app_widget_platform_interface/lib/app_widget_platform_interface.dart: -------------------------------------------------------------------------------- 1 | export 'src/app_widget_platform.dart'; 2 | -------------------------------------------------------------------------------- /app_widget/assets/example_app.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noxasch/flutter_app_widget/HEAD/app_widget/assets/example_app.gif -------------------------------------------------------------------------------- /app_widget/assets/screen_shot.webp: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noxasch/flutter_app_widget/HEAD/app_widget/assets/screen_shot.webp -------------------------------------------------------------------------------- /app_widget/example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /app_widget_android/example/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs=-Xmx1536M 2 | android.useAndroidX=true 3 | android.enableJetifier=true 4 | -------------------------------------------------------------------------------- /app_widget_android/lib/app_widget_android.dart: -------------------------------------------------------------------------------- 1 | export 'src/app_widget_android_platform.dart'; 2 | export 'src/app_widget_android_plugin.dart'; 3 | -------------------------------------------------------------------------------- /app_widget_android/android/.gitignore: -------------------------------------------------------------------------------- 1 | *.iml 2 | .gradle 3 | /local.properties 4 | /.idea/workspace.xml 5 | /.idea/libraries 6 | .DS_Store 7 | /build 8 | /captures 9 | .cxx 10 | -------------------------------------------------------------------------------- /app_widget_android/android/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noxasch/flutter_app_widget/HEAD/app_widget/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noxasch/flutter_app_widget/HEAD/app_widget/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noxasch/flutter_app_widget/HEAD/app_widget/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noxasch/flutter_app_widget/HEAD/app_widget/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noxasch/flutter_app_widget/HEAD/app_widget/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app_widget_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noxasch/flutter_app_widget/HEAD/app_widget_android/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png -------------------------------------------------------------------------------- /app_widget_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noxasch/flutter_app_widget/HEAD/app_widget_android/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png -------------------------------------------------------------------------------- /app_widget_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noxasch/flutter_app_widget/HEAD/app_widget_android/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app_widget_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noxasch/flutter_app_widget/HEAD/app_widget_android/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app_widget_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/noxasch/flutter_app_widget/HEAD/app_widget_android/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png -------------------------------------------------------------------------------- /app_widget_android/example/android/app/src/main/kotlin/tech/noxasch/app_widget_android_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package tech.noxasch.app_widget_example 2 | 3 | import io.flutter.embedding.android.FlutterActivity 4 | 5 | class MainActivity: FlutterActivity() { 6 | } 7 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/res/drawable-v21/widget_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/res/drawable/widget_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/res/drawable-night/widget_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | -------------------------------------------------------------------------------- /app_widget/example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 7 | -------------------------------------------------------------------------------- /app_widget_android/example/android/gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Fri Jun 23 08:50:38 CEST 2017 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip 7 | -------------------------------------------------------------------------------- /app_widget/example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /app_widget_android/example/android/.gitignore: -------------------------------------------------------------------------------- 1 | gradle-wrapper.jar 2 | /.gradle 3 | /captures/ 4 | /gradlew 5 | /gradlew.bat 6 | /local.properties 7 | GeneratedPluginRegistrant.java 8 | 9 | # Remember to never publicly share your keystore. 10 | # See https://flutter.dev/docs/deployment/android#reference-the-keystore-from-the-app 11 | key.properties 12 | **/*.keystore 13 | **/*.jks 14 | -------------------------------------------------------------------------------- /app_widget_android/lib/src/app_widget_android_platform.dart: -------------------------------------------------------------------------------- 1 | import 'package:app_widget_platform_interface/app_widget_platform_interface.dart'; 2 | 3 | class AppWidgetAndroid extends AppWidgetPlatform { 4 | // this is use by flutter pluginRegistrant 5 | // this will register with no callback 6 | // no methodChannel are allowed since this are call before the app 7 | static void registerWith() { 8 | AppWidgetPlatform.instance = AppWidgetAndroid(); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app_widget_android/example/android/app/src/debug/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app_widget_android/example/android/app/src/profile/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /app_widget/example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /app_widget_android/example/android/settings.gradle: -------------------------------------------------------------------------------- 1 | include ':app' 2 | 3 | def localPropertiesFile = new File(rootProject.projectDir, "local.properties") 4 | def properties = new Properties() 5 | 6 | assert localPropertiesFile.exists() 7 | localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) } 8 | 9 | def flutterSdkPath = properties.getProperty("flutter.sdk") 10 | assert flutterSdkPath != null, "flutter.sdk not set in local.properties" 11 | apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle" 12 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/res/drawable-night/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /app_widget_android/example/android/app/src/main/res/drawable/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /app_widget_android/example/android/app/src/main/res/drawable-v21/launch_background.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 12 | 13 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/kotlin/tech/noxasch/app_widget_example/MainActivity.kt: -------------------------------------------------------------------------------- 1 | package tech.noxasch.app_widget_example 2 | 3 | import android.appwidget.AppWidgetManager 4 | import io.flutter.embedding.android.FlutterActivity 5 | import io.flutter.embedding.engine.FlutterEngine 6 | import tech.noxasch.app_widget.AppWidgetPlugin 7 | 8 | // this need to be implemented manually 9 | class MainActivity: FlutterActivity() { 10 | override fun onFlutterUiDisplayed() { 11 | super.onFlutterUiDisplayed() 12 | 13 | AppWidgetPlugin.Companion.handleWidgetAction(context, intent) 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/res/xml/app_widget_example_info.xml: -------------------------------------------------------------------------------- 1 | 2 | 12 | 13 | -------------------------------------------------------------------------------- /app_widget_platform_interface/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: app_widget_platform_interface 2 | description: Common platform interface for app_widget plugin. 3 | version: 0.4.0 4 | homepage: https://noxasch.tech/ 5 | repository: https://github.com/noxasch/flutter_app_widget/tree/master/app_widget_platform_interface 6 | issue_tracker: https://github.com/noxasch/flutter_app_widget/issues 7 | 8 | environment: 9 | sdk: ">=2.17.6 <4.0.0" 10 | flutter: ">=2.5.0" 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | plugin_platform_interface: ^2.1.7 16 | 17 | dev_dependencies: 18 | flutter_test: 19 | sdk: flutter 20 | flutter_lints: ^2.0.0 21 | -------------------------------------------------------------------------------- /app_widget/example/README.md: -------------------------------------------------------------------------------- 1 | # app_widget_example 2 | 3 | Demonstrates how to use the app_widget plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /app_widget_android/README.md: -------------------------------------------------------------------------------- 1 | # app_widget_android 2 | 3 | Android implementation for [app_widget](https://pub.dev/packages/app_widget) plugin. This package are not meant to be used 4 | on it's own. 5 | 6 | ## Getting Started 7 | 8 | This project is a starting point for a Flutter 9 | [plug-in package](https://flutter.dev/developing-packages/), 10 | a specialized package that includes platform-specific implementation code for 11 | Android and/or iOS. 12 | 13 | For help getting started with Flutter development, view the 14 | [online documentation](https://flutter.dev/docs), which offers tutorials, 15 | samples, guidance on mobile development, and a full API reference. 16 | 17 | -------------------------------------------------------------------------------- /app_widget_android/example/README.md: -------------------------------------------------------------------------------- 1 | # app_widget_android_example 2 | 3 | Demonstrates how to use the app_widget_android plugin. 4 | 5 | ## Getting Started 6 | 7 | This project is a starting point for a Flutter application. 8 | 9 | A few resources to get you started if this is your first Flutter project: 10 | 11 | - [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) 12 | - [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) 13 | 14 | For help getting started with Flutter development, view the 15 | [online documentation](https://docs.flutter.dev/), which offers tutorials, 16 | samples, guidance on mobile development, and a full API reference. 17 | -------------------------------------------------------------------------------- /app_widget_android/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ 31 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: feature-request 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /app_widget_platform_interface/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ 31 | 32 | .fvm/flutter_sdk 33 | -------------------------------------------------------------------------------- /app_widget/example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | tasks.register("clean", Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /app_widget_android/example/android/build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext.kotlin_version = '1.6.10' 3 | repositories { 4 | google() 5 | mavenCentral() 6 | } 7 | 8 | dependencies { 9 | classpath 'com.android.tools.build:gradle:7.1.2' 10 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 11 | } 12 | } 13 | 14 | allprojects { 15 | repositories { 16 | google() 17 | mavenCentral() 18 | } 19 | } 20 | 21 | rootProject.buildDir = '../build' 22 | subprojects { 23 | project.buildDir = "${rootProject.buildDir}/${project.name}" 24 | } 25 | subprojects { 26 | project.evaluationDependsOn(':app') 27 | } 28 | 29 | task clean(type: Delete) { 30 | delete rootProject.buildDir 31 | } 32 | -------------------------------------------------------------------------------- /app_widget_android/example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'package:flutter/material.dart'; 2 | 3 | void main() { 4 | runApp(const MyApp()); 5 | } 6 | 7 | class MyApp extends StatefulWidget { 8 | const MyApp({Key? key}) : super(key: key); 9 | 10 | @override 11 | State createState() => _MyAppState(); 12 | } 13 | 14 | class _MyAppState extends State { 15 | @override 16 | void initState() { 17 | super.initState(); 18 | } 19 | 20 | @override 21 | Widget build(BuildContext context) { 22 | return MaterialApp( 23 | home: Scaffold( 24 | appBar: AppBar( 25 | title: const Text('Plugin example app'), 26 | ), 27 | body: const Center( 28 | child: Text('Running on AppWidgetAndroid'), 29 | ), 30 | ), 31 | ); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /app_widget/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | # Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. 26 | /pubspec.lock 27 | **/doc/api/ 28 | .dart_tool/ 29 | .packages 30 | build/ 31 | .idea 32 | 33 | # fvm 34 | .fvm/flutter_sdk 35 | 36 | coverage 37 | 38 | .flutter-plugins 39 | .flutter-plugins-dependencies 40 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: "[BUG]" 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Platform (please complete the following information):** 27 | - Device: [e.g. iPhone6] 28 | - OS: [e.g. iOS8.1] 29 | - Browser [e.g. stock browser, safari] 30 | - Version [e.g. 22] 31 | 32 | Development details 33 | - flutter version 34 | - plugins version 35 | 36 | **Additional context** 37 | Add any other context about the problem here. 38 | -------------------------------------------------------------------------------- /app_widget/example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Web related 36 | lib/generated_plugin_registrant.dart 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | 44 | # Android Studio will place build artifacts here 45 | /android/app/debug 46 | /android/app/profile 47 | /android/app/release 48 | -------------------------------------------------------------------------------- /app_widget/example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: app_widget_example 2 | description: Demonstrates how to use the app_widget plugin. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | environment: 9 | sdk: ">=2.17.6 <3.0.0" 10 | dependencies: 11 | flutter: 12 | sdk: flutter 13 | 14 | app_widget: 15 | path: ../ 16 | 17 | # The following adds the Cupertino Icons font to your application. 18 | # Use with the CupertinoIcons class for iOS style icons. 19 | cupertino_icons: ^1.0.2 20 | # url_launcher: ^6.1.5 21 | # workmanager: ^0.5.0 22 | 23 | dev_dependencies: 24 | flutter_test: 25 | sdk: flutter 26 | integration_test: 27 | sdk: flutter 28 | flutter_driver: 29 | sdk: flutter 30 | 31 | flutter_lints: ^2.0.0 32 | 33 | flutter: 34 | uses-material-design: true 35 | -------------------------------------------------------------------------------- /app_widget_android/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: app_widget_android 2 | description: Android implementation for app_widget plugin 3 | version: 0.4.0 4 | homepage: https://noxasch.tech/ 5 | repository: https://github.com/noxasch/flutter_app_widget/tree/master/app_widget_android 6 | issue_tracker: https://github.com/noxasch/flutter_app_widget/issues 7 | 8 | environment: 9 | sdk: ">=2.17.6 <4.0.0" 10 | flutter: ">=2.5.0" 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | plugin_platform_interface: ^2.1.7 16 | app_widget_platform_interface: ^0.4.0 17 | # app_widget_platform_interface: # local dev 18 | # path: ../app_widget_platform_interface 19 | 20 | dev_dependencies: 21 | flutter_test: 22 | sdk: flutter 23 | flutter_lints: ^2.0.0 24 | 25 | flutter: 26 | plugin: 27 | implements: app_widget 28 | platforms: 29 | android: 30 | package: tech.noxasch.app_widget 31 | pluginClass: AppWidgetPlugin 32 | dartPluginClass: AppWidgetAndroid 33 | -------------------------------------------------------------------------------- /app_widget_android/example/.gitignore: -------------------------------------------------------------------------------- 1 | # Miscellaneous 2 | *.class 3 | *.log 4 | *.pyc 5 | *.swp 6 | .DS_Store 7 | .atom/ 8 | .buildlog/ 9 | .history 10 | .svn/ 11 | migrate_working_dir/ 12 | 13 | # IntelliJ related 14 | *.iml 15 | *.ipr 16 | *.iws 17 | .idea/ 18 | 19 | # The .vscode folder contains launch configuration and tasks you configure in 20 | # VS Code which you may wish to be included in version control, so this line 21 | # is commented out by default. 22 | #.vscode/ 23 | 24 | # Flutter/Dart/Pub related 25 | **/doc/api/ 26 | **/ios/Flutter/.last_build_id 27 | .dart_tool/ 28 | .flutter-plugins 29 | .flutter-plugins-dependencies 30 | .packages 31 | .pub-cache/ 32 | .pub/ 33 | /build/ 34 | 35 | # Web related 36 | lib/generated_plugin_registrant.dart 37 | 38 | # Symbolication related 39 | app.*.symbols 40 | 41 | # Obfuscation related 42 | app.*.map.json 43 | 44 | # Android Studio will place build artifacts here 45 | /android/app/debug 46 | /android/app/profile 47 | /android/app/release 48 | -------------------------------------------------------------------------------- /app_widget/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: app_widget 2 | description: Flutter plugin to manage app widget / home screen widget from within flutter app. 3 | version: 0.4.0 4 | homepage: https://noxasch.tech/ 5 | repository: https://github.com/noxasch/flutter_app_widget/tree/master/app_widget 6 | issue_tracker: https://github.com/noxasch/flutter_app_widget/issues 7 | 8 | environment: 9 | sdk: ">=2.17.6 <4.0.0" 10 | flutter: ">=2.5.0" 11 | 12 | dependencies: 13 | flutter: 14 | sdk: flutter 15 | plugin_platform_interface: ^2.1.7 16 | app_widget_platform_interface: ^0.4.0 17 | app_widget_android: ^0.4.0 18 | # app_widget_platform_interface: # local dev 19 | # path: ../app_widget_platform_interface 20 | # app_widget_android: # local dev 21 | # path: ../app_widget_android 22 | 23 | dev_dependencies: 24 | flutter_test: 25 | sdk: flutter 26 | flutter_lints: ^2.0.0 27 | 28 | flutter: 29 | plugin: 30 | platforms: 31 | android: 32 | default_package: app_widget_android 33 | -------------------------------------------------------------------------------- /app_widget_platform_interface/README.md: -------------------------------------------------------------------------------- 1 | # app_widget_platform_interface 2 | 3 | Common platform interface for [app_widget](https://pub.dev/packages/app_widget) plugin. 4 | 5 | ## Usage 6 | 7 | To implement a new platform-specific, extend `AppWidgetPlatform` with a `registerWith` as static method that set default 8 | `AppWidgetPlatform.instance = AppWidgetMyPlatform();`. And then create another class `AppWidgetMyPlatformlugin` with an implementation that performs the platform-specific behavior. Finally add your platform initialization in AppWidgetPlugin private 9 | constructor to reinstantiate with the platform specific implementation `AppWidgetPlatform.instance = AppWidgetMyPlatformPlugin();` when the app run. This is because the plugin registrar will register the first intance before `FlutterWidgetBindings.ensureInitialized()` and will throw an error if we try to access any `methodChannel`. 10 | 11 | We try to avoid breaking changes at all. Try to reuse the existing interface to keep the api clean whenever possible. 12 | -------------------------------------------------------------------------------- /app_widget/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: f1875d570e39de09040c8f79aa13cc56baab8db1 8 | channel: stable 9 | 10 | project_type: plugin 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 17 | base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 18 | - platform: android 19 | create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 20 | base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /app_widget_android/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: f1875d570e39de09040c8f79aa13cc56baab8db1 8 | channel: stable 9 | 10 | project_type: plugin 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 17 | base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 18 | - platform: android 19 | create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 20 | base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /app_widget_android/example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // This is a basic Flutter widget test. 2 | // 3 | // To perform an interaction with a widget in your test, use the WidgetTester 4 | // utility in the flutter_test package. For example, you can send tap and scroll 5 | // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | import 'package:flutter/material.dart'; 9 | import 'package:flutter_test/flutter_test.dart'; 10 | 11 | import 'package:app_widget_android_example/main.dart'; 12 | 13 | void main() { 14 | testWidgets('Verify Platform version', (WidgetTester tester) async { 15 | // Build our app and trigger a frame. 16 | await tester.pumpWidget(const MyApp()); 17 | 18 | // Verify that platform version is retrieved. 19 | expect( 20 | find.byWidgetPredicate( 21 | (Widget widget) => widget is Text && 22 | widget.data!.startsWith('Running on:'), 23 | ), 24 | findsOneWidget, 25 | ); 26 | }); 27 | } 28 | -------------------------------------------------------------------------------- /app_widget_platform_interface/.metadata: -------------------------------------------------------------------------------- 1 | # This file tracks properties of this Flutter project. 2 | # Used by Flutter tool to assess capabilities and perform upgrades etc. 3 | # 4 | # This file should be version controlled. 5 | 6 | version: 7 | revision: f1875d570e39de09040c8f79aa13cc56baab8db1 8 | channel: stable 9 | 10 | project_type: plugin 11 | 12 | # Tracks metadata for the flutter migrate command 13 | migration: 14 | platforms: 15 | - platform: root 16 | create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 17 | base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 18 | - platform: android 19 | create_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 20 | base_revision: f1875d570e39de09040c8f79aa13cc56baab8db1 21 | 22 | # User provided section 23 | 24 | # List of Local paths (relative to this file) that should be 25 | # ignored by the migrate tool. 26 | # 27 | # Files that are not part of the templates will be ignored by default. 28 | unmanaged_files: 29 | - 'lib/main.dart' 30 | - 'ios/Runner.xcodeproj/project.pbxproj' 31 | -------------------------------------------------------------------------------- /app_widget_platform_interface/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.0 2 | * breaking changes: `configureWidget` and `updateWidget` accept `layoutId` instead of `layoutName` 3 | 4 | ## 0.3.1 5 | 6 | * update platform interface version 7 | 8 | ## 0.3.0 9 | 10 | * support dart sdk 4.0 11 | 12 | ## 0.2.0 13 | 14 | * breaking changes: `androidAppName` renamed to `androidPackageName` 15 | * breaking changes: parameter `textViewsIdMap` renamed to `textViews` for `configureWidget` and `updateWidget` 16 | * feat: androidPackageName are now accepted for reloadWidget and getWidgetIds 17 | 18 | 19 | ## 0.1.0 20 | 21 | * finalize interface based on android usage 22 | 23 | ## 0.0.3 24 | 25 | * implement getWidgetIds api interface 26 | 27 | ## 0.0.2 28 | 29 | * implement updateWidget api interface 30 | 31 | ## 0.0.1 32 | 33 | * implement configureWidget api interface 34 | * implement cancelConfifureWidget api interface 35 | * implement reloadWidgets api interface 36 | * implement widgetExist api interface 37 | * implement onConfigureWidget callback api interface 38 | * implement onClickWidget callback api interface 39 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app_widget_android/example/android/app/src/main/res/values/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /app_widget_android/example/android/app/src/main/res/values-night/styles.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 15 | 18 | 19 | -------------------------------------------------------------------------------- /.github/workflows/android.yaml: -------------------------------------------------------------------------------- 1 | name: android 2 | on: 3 | workflow_dispatch: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: ['master'] 8 | paths: ['app_widget_android/lib/**', 'app_widget_android/test/**'] 9 | push: 10 | branches: ['master'] 11 | paths: ['app_widget_android/lib/**', 'app_widget_android/test/**'] 12 | jobs: 13 | analysis: 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: ./app_widget_android 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest] 21 | steps: 22 | - uses: actions/checkout@v3 # checkout to current dir 23 | - uses: subosito/flutter-action@v2 24 | with: 25 | channel: 'stable' 26 | cache: true 27 | cache-key: ${{ runner.os }}-flutter-install-cache 28 | cache-path: ${{ runner.tool_cache }}/flutter 29 | - run: flutter pub get 30 | - name: Static Analysis 31 | run: flutter analyze 32 | - name: unit test 33 | run: flutter test 34 | - name: pub publishable 35 | run: flutter pub publish --dry-run 36 | -------------------------------------------------------------------------------- /app_widget/example/test/widget_test.dart: -------------------------------------------------------------------------------- 1 | // // This is a basic Flutter widget test. 2 | // // 3 | // // To perform an interaction with a widget in your test, use the WidgetTester 4 | // // utility in the flutter_test package. For example, you can send tap and scroll 5 | // // gestures. You can also use WidgetTester to find child widgets in the widget 6 | // // tree, read text, and verify that the values of widget properties are correct. 7 | 8 | // import 'package:flutter/material.dart'; 9 | // import 'package:flutter_test/flutter_test.dart'; 10 | 11 | // import 'package:app_widget_example/main.dart'; 12 | 13 | // void main() { 14 | // testWidgets('Verify Platform version', (WidgetTester tester) async { 15 | // // Build our app and trigger a frame. 16 | // await tester.pumpWidget(const MyApp()); 17 | 18 | // // Verify that platform version is retrieved. 19 | // expect( 20 | // find.byWidgetPredicate( 21 | // (Widget widget) => widget is Text && 22 | // widget.data!.startsWith('Running on:'), 23 | // ), 24 | // findsOneWidget, 25 | // ); 26 | // }); 27 | // } 28 | -------------------------------------------------------------------------------- /.github/workflows/interface.yaml: -------------------------------------------------------------------------------- 1 | name: platform-interface 2 | on: 3 | workflow_dispatch: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: ['master'] 8 | paths: ['app_widget_platform_interface/lib/**', 'app_widget_platform_interface/test/**'] 9 | push: 10 | branches: ['master'] 11 | paths: ['app_widget_platform_interface/lib/**', 'app_widget_platform_interface/test/**'] 12 | jobs: 13 | analysis: 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: ./app_widget_platform_interface 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest] 21 | steps: 22 | - uses: actions/checkout@v3 # checkout to current dir 23 | - uses: subosito/flutter-action@v2 24 | with: 25 | channel: 'stable' 26 | cache: true 27 | cache-key: ${{ runner.os }}-flutter-install-cache 28 | cache-path: ${{ runner.tool_cache }}/flutter 29 | - run: flutter pub get 30 | - name: Static Analysis 31 | run: flutter analyze 32 | - name: pub publishable 33 | run: flutter pub publish --dry-run 34 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Flutter App Widget 2 | App Widget / Home Screen widget plugin for flutter app 3 | 4 | ## Usage 5 | 6 | Please see [app_widget](./app_widget) subdirectory for the usage documentation. 7 | 8 | ## Plaform Support 9 | 10 | | Android | iOS | 11 | | :-----: | :-: | 12 | | ✔️ | | 13 | 14 | ## Project Structure 15 | 16 | This project follow the standard for flutter plugin development where each 17 | pieces are independent of each other. Other platform interface can be easily added 18 | by inherit the base platform interface and expose the require api to the main `app_wiget` 19 | plugin. 20 | 21 | ## Contributing 22 | 0. Fork 23 | 1. Checkout to feature branch 24 | 2. Add feature and unit test 25 | 3. Make sure all unit test pass 26 | 4. Add platform integration test and run integration test 27 | 5. Create a PR 28 | 29 | ## Android Integration Test 30 | - Make sure to run on the latest and minSdk supported 31 | 32 | in `app_widget/example/integration_test/app_widget_test.dart` 33 | 34 | ```sh 35 | cd app_widget/example 36 | # this will require a connected device for android 37 | flutter test integration_test/app_widget_test.dart 38 | ``` 39 | -------------------------------------------------------------------------------- /app_widget_android/android/build.gradle: -------------------------------------------------------------------------------- 1 | group 'tech.noxasch.app_widget' 2 | version '1.0-SNAPSHOT' 3 | 4 | buildscript { 5 | ext.kotlin_version = '1.6.10' 6 | repositories { 7 | google() 8 | mavenCentral() 9 | } 10 | 11 | dependencies { 12 | classpath 'com.android.tools.build:gradle:7.1.2' 13 | classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version" 14 | } 15 | } 16 | 17 | rootProject.allprojects { 18 | repositories { 19 | google() 20 | mavenCentral() 21 | } 22 | } 23 | 24 | apply plugin: 'com.android.library' 25 | apply plugin: 'kotlin-android' 26 | 27 | android { 28 | compileSdkVersion 33 29 | 30 | compileOptions { 31 | sourceCompatibility JavaVersion.VERSION_1_8 32 | targetCompatibility JavaVersion.VERSION_1_8 33 | } 34 | 35 | kotlinOptions { 36 | jvmTarget = '1.8' 37 | } 38 | 39 | sourceSets { 40 | main.java.srcDirs += 'src/main/kotlin' 41 | } 42 | 43 | defaultConfig { 44 | minSdkVersion 16 45 | } 46 | } 47 | 48 | dependencies { 49 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 50 | } 51 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/res/layout/example_layout.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 12 | 13 | 22 | 23 | 31 | 32 | -------------------------------------------------------------------------------- /app_widget/example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /app_widget_android/example/analysis_options.yaml: -------------------------------------------------------------------------------- 1 | # This file configures the analyzer, which statically analyzes Dart code to 2 | # check for errors, warnings, and lints. 3 | # 4 | # The issues identified by the analyzer are surfaced in the UI of Dart-enabled 5 | # IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be 6 | # invoked from the command line by running `flutter analyze`. 7 | 8 | # The following line activates a set of recommended lints for Flutter apps, 9 | # packages, and plugins designed to encourage good coding practices. 10 | include: package:flutter_lints/flutter.yaml 11 | 12 | linter: 13 | # The lint rules applied to this project can be customized in the 14 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 15 | # included above or to enable additional rules. A list of all available lints 16 | # and their documentation is published at 17 | # https://dart-lang.github.io/linter/lints/index.html. 18 | # 19 | # Instead of disabling a lint rule for the entire project in the 20 | # section below, it can also be suppressed for a single line of code 21 | # or a specific dart file by using the `// ignore: name_of_lint` and 22 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 23 | # producing the lint. 24 | rules: 25 | # avoid_print: false # Uncomment to disable the `avoid_print` rule 26 | # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule 27 | 28 | # Additional information about this file can be found at 29 | # https://dart.dev/guides/language/analysis-options 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Alexander Dischberg 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /app_widget/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Alexander Dischberg 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /app_widget_android/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Alexander Dischberg 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /app_widget_platform_interface/LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Alexander Dischberg 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | 1. Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | 2. Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | 3. Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/kotlin/tech/noxasch/app_widget_example/AppWidgetExampleDiffProvider.kt: -------------------------------------------------------------------------------- 1 | package tech.noxasch.diff_name 2 | 3 | import android.appwidget.AppWidgetManager 4 | import android.appwidget.AppWidgetProvider 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.util.Log 8 | import android.widget.RemoteViews 9 | import androidx.core.content.ContextCompat.startActivity 10 | import androidx.core.view.accessibility.AccessibilityEventCompat.setAction 11 | import io.flutter.embedding.engine.FlutterEngine 12 | import io.flutter.plugin.common.MethodChannel 13 | import tech.noxasch.app_widget.AppWidgetPlugin 14 | 15 | class AppWidgetExampleDiffProvider : AppWidgetProvider() { 16 | override fun onUpdate( 17 | context: Context?, 18 | appWidgetManager: AppWidgetManager?, 19 | appWidgetIds: IntArray? 20 | ) { 21 | super.onUpdate(context, appWidgetManager, appWidgetIds) 22 | Log.d("APP_WIDGET_PLUGIN", "ON_UPDATE") 23 | if (appWidgetIds != null) { 24 | for (widgetId in appWidgetIds) { 25 | Log.d("APP_WIDGET_PLUGIN", "WIDGET_ID: $widgetId") 26 | } 27 | } 28 | 29 | // check if widgetId store sharedPreferences 30 | // fetch data from sharedPreferences 31 | // then update 32 | // for (widgetId in appWidgetIds!!) { 33 | // val remoteViews = RemoteViews(context!!.packageName, R.layout.example_layout).apply() { 34 | // setTextViewText(R.id.widget_title, "Widget Title") 35 | // setTextViewText(R.id.widget_message, "This is my message") 36 | // } 37 | // 38 | // appWidgetManager!!.partiallyUpdateAppWidget(widgetId, remoteViews) 39 | // } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/kotlin/tech/noxasch/app_widget_example/AppWidgetExampleProvider.kt: -------------------------------------------------------------------------------- 1 | package tech.noxasch.app_widget_example 2 | 3 | import android.appwidget.AppWidgetManager 4 | import android.appwidget.AppWidgetProvider 5 | import android.content.Context 6 | import android.content.Intent 7 | import android.util.Log 8 | import android.widget.RemoteViews 9 | import androidx.core.content.ContextCompat.startActivity 10 | import androidx.core.view.accessibility.AccessibilityEventCompat.setAction 11 | import io.flutter.embedding.engine.FlutterEngine 12 | import io.flutter.plugin.common.MethodChannel 13 | import tech.noxasch.app_widget.AppWidgetPlugin 14 | 15 | class AppWidgetExampleProvider : AppWidgetProvider() { 16 | override fun onUpdate( 17 | context: Context?, 18 | appWidgetManager: AppWidgetManager?, 19 | appWidgetIds: IntArray? 20 | ) { 21 | super.onUpdate(context, appWidgetManager, appWidgetIds) 22 | Log.d("APP_WIDGET_PLUGIN", "ON_UPDATE") 23 | if (appWidgetIds != null) { 24 | for (widgetId in appWidgetIds) { 25 | Log.d("APP_WIDGET_PLUGIN", "WIDGET_ID: $widgetId") 26 | } 27 | } 28 | 29 | // check if widgetId store sharedPreferences 30 | // fetch data from sharedPreferences 31 | // then update 32 | // for (widgetId in appWidgetIds!!) { 33 | // val remoteViews = RemoteViews(context!!.packageName, R.layout.example_layout).apply() { 34 | // setTextViewText(R.id.widget_title, "Widget Title") 35 | // setTextViewText(R.id.widget_message, "This is my message") 36 | // } 37 | // 38 | // appWidgetManager!!.partiallyUpdateAppWidget(widgetId, remoteViews) 39 | // } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /app_widget_android/example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 30 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /app_widget_android/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.0 2 | * breaking changes: `configureWidget` and `updateWidget` accept `layoutId` instead of `layoutName` 3 | * breaking changes: `onConfigureWidget` now accept 3 params (`widgetId`, `layoutId`, `layoutName`) 4 | 5 | ## 0.3.3 6 | * fix: fix trigger update widget updating the widget multiple time 7 | 8 | ## 0.3.2 9 | 10 | * chore: update latest dependencies 11 | 12 | ## 0.3.1 13 | 14 | * chore: use latest platform interface 15 | ## 0.3.0 16 | 17 | *feat: support widget provider with different package name 18 | *fix: fix MainActivity not using application package name 19 | 20 | ## 0.2.1 21 | 22 | * fix(android): onClickWidget callback should be independent on each widget 23 | 24 | ## 0.2.0 25 | 26 | * breaking changes: `androidAppName` renamed to `androidPackageName` 27 | * breaking changes: parameter `textViewsIdMap` renamed to `textViews` for `configureWidget` and `updateWidget` 28 | * breaking changes: `onClickWidget` now accept String instead of Map 29 | * feat: androidPackageName are now accepted for reloadWidget and getWidgetIds 30 | * feat: accept uri parameters in `configureWidget` and `updatewidget` 31 | * fix: onClickWidget callback 32 | 33 | ## 0.1.1 34 | 35 | * perf: improve configure widget callback response 36 | * fix: fix reload wigdets 37 | * fix: fix getWidgetIds 38 | 39 | ## 0.1.0 40 | 41 | * feat: support flavored app 42 | * fix: update method 43 | * chore: finalized android params name 44 | ## 0.0.4 45 | 46 | * implement `handleConfigureAction` android method 47 | 48 | ## 0.0.3 49 | 50 | * implement getWidgetIds api 51 | 52 | ## 0.0.2 53 | 54 | * implement updateWidget api 55 | * remove debug log - Android and Dart 56 | 57 | ## 0.0.1 58 | 59 | * implement configureWidget api 60 | * implement cancelConfifureWidget api 61 | * implement reloadWidgets api 62 | * implement widgetExist api 63 | * implement onConfigureWidget callback api 64 | * implement onClickWidget callback api 65 | -------------------------------------------------------------------------------- /app_widget/CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 0.4.0 2 | * breaking changes: `configureWidget` and `updateWidget` accept `layoutId` instead of `layoutName` 3 | * breaking changes: `onConfigureWidget` now accept 3 params (`widgetId`, `layoutId`, `layoutName`) 4 | 5 | ## 0.3.1 6 | * fix(android): fix trigger update widget updating the widget multiple time 7 | 8 | ## 0.3.0 9 | * feat(android): support widget provider with diff androidPackageName 10 | * test: update widget test 11 | * test(android): update integration test 12 | 13 | ## 0.2.2 14 | 15 | * fix(android): `reloadWidgets` to use initialized `androidPackageName` 16 | 17 | ## 0.2.1 18 | 19 | * fix(android): onClickWidget callback should be independent on each widget 20 | 21 | ## 0.2.0 22 | 23 | * rename interface into more generic 24 | * feat(android): support uri payload for onClick intent 25 | * fix: onClickWidget callback are now called properly 26 | 27 | ### Breaking Changes 28 | - `androidPackageName` are no longer accepted in `configureWidget` and `updateWidget` method 29 | - `onClickWidget` callback now accept a string instead of Map 30 | 31 | ## 0.1.1 32 | 33 | * perf: improve configure widget callback response 34 | * fix: fix reload wigdets 35 | * fix: fix getWidgetIds 36 | * docs: update docs 37 | * docs: update example app 38 | 39 | ## 0.1.0 40 | 41 | * Support flavored app 42 | * Fix update method 43 | * finalized android params name 44 | 45 | ## 0.0.4 46 | 47 | * implement `handleConfigureAction` android method 48 | 49 | ## 0.0.3 50 | 51 | * implement getWidgetIds api - Android 52 | 53 | ## 0.0.2 54 | 55 | * implement updateWidget api - Android 56 | * remove debug log - Android and Dart 57 | 58 | ## 0.0.1 59 | 60 | * implement configureWidget api - Android 61 | * implement cancelConfifureWidget api - Android 62 | * implement reloadWidgets api - Android 63 | * implement widgetExist api - Android 64 | * implement onConfigureWidget callback api - Android 65 | * implement onClickWidget callback api - Android 66 | -------------------------------------------------------------------------------- /app_widget/example/integration_test/android_test_2.dart: -------------------------------------------------------------------------------- 1 | import 'package:app_widget/app_widget.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:integration_test/integration_test.dart'; 4 | 5 | // there is no way to test callback as it need to interact with actual widgets 6 | void main() { 7 | IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 8 | 9 | group('without androidPackageName', () { 10 | final AppWidgetPlugin appWidgetPlugin = AppWidgetPlugin(); 11 | 12 | testWidgets('configureWidget', (tester) async { 13 | final res = await appWidgetPlugin.configureWidget( 14 | widgetId: 1, 15 | layoutId: 1, 16 | payload: '{"itemId": 1, "stringUid": "uid"}', 17 | url: 'https://google.come', 18 | ); 19 | expect(res, isTrue); 20 | }); 21 | 22 | testWidgets('cancelConfigureWidget', (tester) async { 23 | final res = await appWidgetPlugin.cancelConfigureWidget(); 24 | 25 | expect(res, isTrue); 26 | }); 27 | 28 | testWidgets('updateWidget', (tester) async { 29 | final res = await appWidgetPlugin.updateWidget( 30 | widgetId: 1, 31 | layoutId: 1, 32 | payload: '{"itemId": 1, "stringUid": "uid"}', 33 | url: 'https://google.come', 34 | textViews: {'widget_title': 'my title'}, 35 | ); 36 | 37 | expect(res, isTrue); 38 | }); 39 | 40 | testWidgets('getWidgetIds', (tester) async { 41 | final res = await appWidgetPlugin.getWidgetIds( 42 | androidProviderName: 'AppWidgetExampleProvider', 43 | ); 44 | 45 | expect(res, []); 46 | }); 47 | 48 | testWidgets('reloadWidgets', (tester) async { 49 | final res = await appWidgetPlugin.reloadWidgets( 50 | androidProviderName: 'AppWidgetExampleProvider', 51 | ); 52 | 53 | expect(res, isTrue); 54 | }); 55 | 56 | testWidgets('widgetExist', (tester) async { 57 | final res = await appWidgetPlugin.widgetExist(12); 58 | 59 | expect(res, isFalse); 60 | }); 61 | }); 62 | } 63 | -------------------------------------------------------------------------------- /app_widget/example/integration_test/android_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:app_widget/app_widget.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:integration_test/integration_test.dart'; 4 | 5 | // there is no way to test callback as it need to interact with actual widgets 6 | void main() { 7 | IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 8 | 9 | group('with androidPackageName', () { 10 | final AppWidgetPlugin appWidgetPlugin = AppWidgetPlugin( 11 | androidPackageName: 'tech.noxasch.app_widget_example', 12 | ); 13 | 14 | testWidgets('configureWidget', (tester) async { 15 | final res = await appWidgetPlugin.configureWidget( 16 | widgetId: 1, 17 | layoutId: 1, 18 | payload: '{"itemId": 1, "stringUid": "uid"}', 19 | url: 'https://google.come', 20 | ); 21 | expect(res, isTrue); 22 | }); 23 | 24 | testWidgets('cancelConfigureWidget', (tester) async { 25 | final res = await appWidgetPlugin.cancelConfigureWidget(); 26 | 27 | expect(res, isTrue); 28 | }); 29 | 30 | testWidgets('updateWidget', (tester) async { 31 | final res = await appWidgetPlugin.updateWidget( 32 | widgetId: 1, 33 | layoutId: 1, 34 | payload: '{"itemId": 1, "stringUid": "uid"}', 35 | url: 'https://google.come', 36 | textViews: {'widget_title': 'my title'}, 37 | ); 38 | 39 | expect(res, isTrue); 40 | }); 41 | 42 | testWidgets('getWidgetIds', (tester) async { 43 | final res = await appWidgetPlugin.getWidgetIds( 44 | androidProviderName: 'AppWidgetExampleProvider', 45 | ); 46 | 47 | expect(res, []); 48 | }); 49 | 50 | testWidgets('reloadWidgets', (tester) async { 51 | final res = await appWidgetPlugin.reloadWidgets( 52 | androidProviderName: 'AppWidgetExampleProvider', 53 | ); 54 | 55 | expect(res, isTrue); 56 | }); 57 | 58 | testWidgets('widgetExist', (tester) async { 59 | final res = await appWidgetPlugin.widgetExist(12); 60 | 61 | expect(res, isFalse); 62 | }); 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /app_widget/example/integration_test/android_diff_package_name_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:app_widget/app_widget.dart'; 2 | import 'package:flutter_test/flutter_test.dart'; 3 | import 'package:integration_test/integration_test.dart'; 4 | 5 | // there is no way to test callback as it need to interact with actual widgets 6 | void main() { 7 | IntegrationTestWidgetsFlutterBinding.ensureInitialized(); 8 | 9 | group('with androidPackageName', () { 10 | final AppWidgetPlugin appWidgetPlugin = AppWidgetPlugin( 11 | androidPackageName: 'tech.noxasch.app_widget_example', 12 | ); 13 | 14 | testWidgets('configureWidget', (tester) async { 15 | final res = await appWidgetPlugin.configureWidget( 16 | androidPackageName: 'tech.noxasch.diff_name', 17 | widgetId: 1, 18 | layoutId: 1, 19 | payload: '{"itemId": 1, "stringUid": "uid"}', 20 | url: 'https://google.come', 21 | ); 22 | expect(res, isTrue); 23 | }); 24 | 25 | testWidgets('cancelConfigureWidget', (tester) async { 26 | final res = await appWidgetPlugin.cancelConfigureWidget(); 27 | 28 | expect(res, isTrue); 29 | }); 30 | 31 | testWidgets('updateWidget', (tester) async { 32 | final res = await appWidgetPlugin.updateWidget( 33 | androidPackageName: 'tech.noxasch.diff_name', 34 | widgetId: 1, 35 | layoutId: 1, 36 | payload: '{"itemId": 1, "stringUid": "uid"}', 37 | url: 'https://google.come', 38 | textViews: {'widget_title': 'my title'}, 39 | ); 40 | 41 | expect(res, isTrue); 42 | }); 43 | 44 | testWidgets('getWidgetIds', (tester) async { 45 | final res = await appWidgetPlugin.getWidgetIds( 46 | androidPackageName: 'tech.noxasch.diff_name', 47 | androidProviderName: 'AppWidgetExampleDiffProvider', 48 | ); 49 | 50 | expect(res, []); 51 | }); 52 | 53 | testWidgets('reloadWidgets', (tester) async { 54 | final res = await appWidgetPlugin.reloadWidgets( 55 | androidPackageName: 'tech.noxasch.diff_name', 56 | androidProviderName: 'AppWidgetExampleDiffProvider', 57 | ); 58 | 59 | expect(res, isTrue); 60 | }); 61 | 62 | testWidgets('widgetExist', (tester) async { 63 | final res = await appWidgetPlugin.widgetExist(12); 64 | 65 | expect(res, isFalse); 66 | }); 67 | }); 68 | } 69 | -------------------------------------------------------------------------------- /app_widget_android/example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "tech.noxasch.app_widget_example" 48 | // You can update the following values to match your application needs. 49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 50 | minSdkVersion flutter.minSdkVersion 51 | targetSdkVersion flutter.targetSdkVersion 52 | versionCode flutterVersionCode.toInteger() 53 | versionName flutterVersionName 54 | } 55 | 56 | buildTypes { 57 | release { 58 | // TODO: Add your own signing config for the release build. 59 | // Signing with the debug keys for now, so `flutter run --release` works. 60 | signingConfig signingConfigs.debug 61 | } 62 | } 63 | } 64 | 65 | flutter { 66 | source '../..' 67 | } 68 | 69 | dependencies { 70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 71 | } 72 | -------------------------------------------------------------------------------- /app_widget_platform_interface/lib/src/app_widget_platform.dart: -------------------------------------------------------------------------------- 1 | import 'package:plugin_platform_interface/plugin_platform_interface.dart'; 2 | 3 | abstract class AppWidgetPlatform extends PlatformInterface { 4 | /// Constructs a AppWidgetPlatform. 5 | AppWidgetPlatform() : super(token: _token); 6 | 7 | static final Object _token = Object(); 8 | 9 | // static late AppWidgetPlatform _instance = MethodChannelAppWidget(); 10 | // static AppWidgetPlatform _instance = MethodChannelAppWidget(); 11 | static late AppWidgetPlatform _instance; 12 | 13 | /// The default instance of [AppWidgetPlatform] to use. 14 | /// 15 | /// Defaults to [MethodChannelAppWidget]. 16 | static AppWidgetPlatform get instance => _instance; 17 | 18 | /// Platform-specific implementations should set this with their own 19 | /// platform-specific class that extends [AppWidgetPlatform] when 20 | /// they register themselves. 21 | static set instance(AppWidgetPlatform instance) { 22 | PlatformInterface.verifyToken(instance, _token); 23 | _instance = instance; 24 | } 25 | 26 | static const channel = 'tech.noxasch.flutter/app_widget_foreground'; 27 | 28 | Future cancelConfigureWidget() { 29 | throw UnimplementedError(); 30 | } 31 | 32 | // the params are not require in base interface as different platform will require differet parameters 33 | // we handle this in platform specific and plugin interface 34 | Future configureWidget({ 35 | String? androidPackageName, 36 | int? widgetId, 37 | int? layoutId, 38 | Map? textViews, 39 | String? payload, 40 | String? url, 41 | }) async { 42 | throw UnimplementedError(); 43 | } 44 | 45 | Future reloadWidgets({ 46 | String? androidPackageName, 47 | String? androidProviderName, 48 | }) async { 49 | throw UnimplementedError(); 50 | } 51 | 52 | Future updateWidget({ 53 | String? androidPackageName, 54 | int? widgetId, 55 | int? layoutId, 56 | Map? textViews, 57 | String? payload, 58 | String? url, 59 | }) async { 60 | throw UnimplementedError(); 61 | } 62 | 63 | /// get all widgets ids associated with the given provider 64 | /// will throw an error if the provider doesn't exist 65 | Future?> getWidgetIds({ 66 | String? androidPackageName, 67 | String? androidProviderName, 68 | }) async { 69 | throw UnimplementedError(); 70 | } 71 | 72 | Future widgetExist(int widgetId) async { 73 | throw UnimplementedError(); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /app_widget/example/android/app/build.gradle: -------------------------------------------------------------------------------- 1 | def localProperties = new Properties() 2 | def localPropertiesFile = rootProject.file('local.properties') 3 | if (localPropertiesFile.exists()) { 4 | localPropertiesFile.withReader('UTF-8') { reader -> 5 | localProperties.load(reader) 6 | } 7 | } 8 | 9 | def flutterRoot = localProperties.getProperty('flutter.sdk') 10 | if (flutterRoot == null) { 11 | throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.") 12 | } 13 | 14 | def flutterVersionCode = localProperties.getProperty('flutter.versionCode') 15 | if (flutterVersionCode == null) { 16 | flutterVersionCode = '1' 17 | } 18 | 19 | def flutterVersionName = localProperties.getProperty('flutter.versionName') 20 | if (flutterVersionName == null) { 21 | flutterVersionName = '1.0' 22 | } 23 | 24 | apply plugin: 'com.android.application' 25 | apply plugin: 'kotlin-android' 26 | apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle" 27 | 28 | android { 29 | compileSdkVersion flutter.compileSdkVersion 30 | ndkVersion flutter.ndkVersion 31 | 32 | compileOptions { 33 | sourceCompatibility JavaVersion.VERSION_1_8 34 | targetCompatibility JavaVersion.VERSION_1_8 35 | } 36 | 37 | kotlinOptions { 38 | jvmTarget = '1.8' 39 | } 40 | 41 | sourceSets { 42 | main.java.srcDirs += 'src/main/kotlin' 43 | } 44 | 45 | defaultConfig { 46 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). 47 | applicationId "tech.noxasch.app_widget_example" 48 | // You can update the following values to match your application needs. 49 | // For more information, see: https://docs.flutter.dev/deployment/android#reviewing-the-build-configuration. 50 | minSdkVersion flutter.minSdkVersion 51 | targetSdkVersion flutter.targetSdkVersion 52 | versionCode flutterVersionCode.toInteger() 53 | versionName flutterVersionName 54 | } 55 | 56 | buildTypes { 57 | release { 58 | // TODO: Add your own signing config for the release build. 59 | // Signing with the debug keys for now, so `flutter run --release` works. 60 | signingConfig signingConfigs.debug 61 | } 62 | } 63 | } 64 | 65 | flutter { 66 | source '../..' 67 | } 68 | 69 | dependencies { 70 | implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version" 71 | implementation 'com.google.code.gson:gson:2.9.1' 72 | } 73 | -------------------------------------------------------------------------------- /analysis_options.yaml: -------------------------------------------------------------------------------- 1 | linter: 2 | # The lint rules applied to this project can be customized in the 3 | # section below to disable rules from the `package:flutter_lints/flutter.yaml` 4 | # included above or to enable additional rules. A list of all available lints 5 | # and their documentation is published at 6 | # https://dart-lang.github.io/linter/lints/index.html. 7 | # 8 | # Instead of disabling a lint rule for the entire project in the 9 | # section below, it can also be suppressed for a single line of code 10 | # or a specific dart file by using the `// ignore: name_of_lint` and 11 | # `// ignore_for_file: name_of_lint` syntax on the line or in the file 12 | # producing the lint. 13 | rules: 14 | prefer_single_quotes: true 15 | eol_at_end_of_file: true 16 | directives_ordering: true 17 | require_trailing_commas: true 18 | unnecessary_await_in_return: true 19 | 20 | # Additional information about this file can be found at 21 | # https://dart.dev/guides/language/analysis-options 22 | analyzer: 23 | exclude: 24 | - lib/**/*.g.dart # freezed and drift 25 | - lib/firebase_options_*.dart 26 | - lib/**/*.freezed.dart # freezed 27 | errors: 28 | invalid_annotation_target: ignore 29 | missing_required_param: warning 30 | missing_return: warning 31 | todo: ignore 32 | language: 33 | strict-casts: true 34 | strict-inference: true 35 | strict-raw-types: true 36 | # high CPU usage issue 37 | # https://github.com/dart-code-checker/dart-code-metrics/issues/568 38 | # plugins: 39 | # - dart_code_metrics 40 | 41 | dart_code_metrics: 42 | anti-patterns: 43 | - long-method: 44 | severity: warning 45 | - long-parameter-list: 46 | severity: warning 47 | metrics: 48 | cyclomatic-complexity: 20 49 | maximum-nesting-level: 5 50 | number-of-parameters: 4 51 | source-lines-of-code: 50 52 | # maintainability-index: 50 53 | metrics-exclude: 54 | - test/** 55 | - lib/**/*.g.dart 56 | - lib/firebase_options_*.dart 57 | rules: 58 | - newline-before-return: 59 | severity: style 60 | - no-boolean-literal-compare: 61 | severity: warning 62 | - prefer-async-await 63 | - no-empty-block 64 | - prefer-enums-by-name 65 | - prefer-trailing-comma: 66 | severity: style 67 | - prefer-conditional-expressions 68 | - no-equal-then-else 69 | - avoid-non-ascii-symbols 70 | - double-literal-format 71 | - no-magic-number 72 | - avoid-returning-widgets 73 | - avoid-unnecessary-setstate 74 | - avoid-use-expanded-as-spacer 75 | - avoid-wrapping-in-padding 76 | - prefer-correct-edge-insets-constructor 77 | - prefer-extracting-callbacks 78 | - avoid-nested-conditional-expressions: 79 | acceptable-level: 2 80 | - member-ordering: 81 | # alphabetize: true 82 | order: 83 | - constructors 84 | - public-fields 85 | - private-fields 86 | -------------------------------------------------------------------------------- /app_widget/example/android/app/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 3 | 7 | 15 | 19 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 47 | 48 | 50 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /.github/workflows/main.yaml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | workflow_dispatch: 4 | branches: 5 | - master 6 | pull_request: 7 | branches: ['master'] 8 | paths: ['lib/**', 'android/**', 'app_widget/test/**', 'app_widget/example/**'] 9 | push: 10 | branches: ['master'] 11 | paths: ['lib/**', 'android/**', 'app_widget/test/**', 'app_widget/example/**'] 12 | jobs: 13 | analysis: 14 | runs-on: ubuntu-latest 15 | defaults: 16 | run: 17 | working-directory: ./app_widget 18 | strategy: 19 | matrix: 20 | os: [ubuntu-latest] 21 | steps: 22 | - uses: actions/checkout@v3 # checkout to current dir 23 | - uses: subosito/flutter-action@v2 24 | with: 25 | channel: 'stable' 26 | cache: true 27 | cache-key: ${{ runner.os }}-flutter-install-cache 28 | cache-path: ${{ runner.tool_cache }}/flutter 29 | - run: flutter pub get 30 | - name: Static Analysis 31 | run: flutter analyze 32 | - name: pub publishable 33 | run: flutter pub publish --dry-run 34 | - name: unit test 35 | run: flutter test 36 | # never finish on CI, but run fine on local 37 | # build_android: 38 | # needs: analysis 39 | # strategy: 40 | # matrix: 41 | # api-level: [29, 30, 31, 32, 33] 42 | # runs-on: macos-latest 43 | # defaults: 44 | # run: 45 | # working-directory: ./app_widget/example 46 | # steps: 47 | # - uses: actions/checkout@v3 48 | # - uses: subosito/flutter-action@v2 49 | # with: 50 | # channel: 'stable' 51 | # cache: true 52 | # cache-key: ${{ runner.os }}-flutter-install-cache 53 | # cache-path: ${{ runner.tool_cache }}/flutter 54 | # - run: flutter pub get 55 | # - name: AVD cache 56 | # uses: actions/cache@v3 57 | # id: avd-cache 58 | # with: 59 | # path: | 60 | # ~/.android/avd/* 61 | # ~/.android/adb* 62 | # key: avd-${{ matrix.api-level }} 63 | # - name: create AVD and generate snapshot for caching 64 | # if: steps.avd-cache.outputs.cache-hit != 'true' 65 | # uses: reactivecircus/android-emulator-runner@v2 66 | # with: 67 | # api-level: ${{ matrix.api-level }} 68 | # arch: x86_64 69 | # profile: Nexus 6 70 | # target: playstore 71 | # force-avd-creation: false 72 | # emulator-options: -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none 73 | # disable-animations: false 74 | # script: echo "Generated AVD snapshot for caching." 75 | # - uses: actions/setup-java@v2 76 | # with: 77 | # distribution: 'adopt' 78 | # java-version: '11' 79 | # cache: 'gradle' 80 | # - name: Integration Test 81 | # uses: reactivecircus/android-emulator-runner@v2 82 | # with: 83 | # target: playstore 84 | # api-level: ${{ matrix.api-level }} 85 | # arch: x86_64 86 | # profile: Nexus 6 87 | # force-avd-creation: false 88 | # emulator-options: -no-snapshot-save -no-window -gpu swiftshader_indirect -noaudio -no-boot-anim -camera-back none 89 | # disable-animations: true 90 | # working-directory: ./app_widget/example 91 | # script: flutter test integration_test/android_test.dart && sleep 2 && flutter test integration_test/android_test_2.dart 92 | -------------------------------------------------------------------------------- /app_widget_android/example/pubspec.yaml: -------------------------------------------------------------------------------- 1 | name: app_widget_android_example 2 | description: Demonstrates how to use the app_widget_android plugin. 3 | 4 | # The following line prevents the package from being accidentally published to 5 | # pub.dev using `flutter pub publish`. This is preferred for private packages. 6 | publish_to: 'none' # Remove this line if you wish to publish to pub.dev 7 | 8 | environment: 9 | sdk: ">=2.17.6 <3.0.0" 10 | 11 | # Dependencies specify other packages that your package needs in order to work. 12 | # To automatically upgrade your package dependencies to the latest versions 13 | # consider running `flutter pub upgrade --major-versions`. Alternatively, 14 | # dependencies can be manually updated by changing the version numbers below to 15 | # the latest version available on pub.dev. To see which dependencies have newer 16 | # versions available, run `flutter pub outdated`. 17 | dependencies: 18 | flutter: 19 | sdk: flutter 20 | 21 | app_widget_android: 22 | # When depending on this package from a real application you should use: 23 | # app_widget_android: ^x.y.z 24 | # See https://dart.dev/tools/pub/dependencies#version-constraints 25 | # The example app is bundled with the plugin so we use a path dependency on 26 | # the parent directory to use the current plugin's version. 27 | path: ../ 28 | 29 | # The following adds the Cupertino Icons font to your application. 30 | # Use with the CupertinoIcons class for iOS style icons. 31 | cupertino_icons: ^1.0.2 32 | 33 | dev_dependencies: 34 | flutter_test: 35 | sdk: flutter 36 | 37 | # The "flutter_lints" package below contains a set of recommended lints to 38 | # encourage good coding practices. The lint set provided by the package is 39 | # activated in the `analysis_options.yaml` file located at the root of your 40 | # package. See that file for information about deactivating specific lint 41 | # rules and activating additional ones. 42 | flutter_lints: ^2.0.0 43 | 44 | # For information on the generic Dart part of this file, see the 45 | # following page: https://dart.dev/tools/pub/pubspec 46 | 47 | # The following section is specific to Flutter packages. 48 | flutter: 49 | 50 | # The following line ensures that the Material Icons font is 51 | # included with your application, so that you can use the icons in 52 | # the material Icons class. 53 | uses-material-design: true 54 | 55 | # To add assets to your application, add an assets section, like this: 56 | # assets: 57 | # - images/a_dot_burr.jpeg 58 | # - images/a_dot_ham.jpeg 59 | 60 | # An image asset can refer to one or more resolution-specific "variants", see 61 | # https://flutter.dev/assets-and-images/#resolution-aware 62 | 63 | # For details regarding adding assets from package dependencies, see 64 | # https://flutter.dev/assets-and-images/#from-packages 65 | 66 | # To add custom fonts to your application, add a fonts section here, 67 | # in this "flutter" section. Each entry in this list should have a 68 | # "family" key with the font family name, and a "fonts" key with a 69 | # list giving the asset and other descriptors for the font. For 70 | # example: 71 | # fonts: 72 | # - family: Schyler 73 | # fonts: 74 | # - asset: fonts/Schyler-Regular.ttf 75 | # - asset: fonts/Schyler-Italic.ttf 76 | # style: italic 77 | # - family: Trajan Pro 78 | # fonts: 79 | # - asset: fonts/TrajanPro.ttf 80 | # - asset: fonts/TrajanPro_Bold.ttf 81 | # weight: 700 82 | # 83 | # For details regarding fonts from package dependencies, 84 | # see https://flutter.dev/custom-fonts/#from-packages 85 | -------------------------------------------------------------------------------- /app_widget/test/app_widget_ios_test.dart: -------------------------------------------------------------------------------- 1 | import 'package:app_widget/app_widget.dart'; 2 | import 'package:app_widget_platform_interface/app_widget_platform_interface.dart'; 3 | import 'package:flutter/foundation.dart'; 4 | import 'package:flutter/services.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | 7 | void main() { 8 | TestWidgetsFlutterBinding.ensureInitialized(); 9 | 10 | const MethodChannel channel = MethodChannel(AppWidgetPlatform.channel); 11 | final List log = []; 12 | 13 | setUpAll(() { 14 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger 15 | .setMockMethodCallHandler(channel, (methodCall) async { 16 | log.add(methodCall); 17 | switch (methodCall.method) { 18 | case 'getPlatformVersion': 19 | return '42'; 20 | case 'configureWidget': 21 | return true; 22 | case 'cancelConfigureWidget': 23 | return true; 24 | case 'getWidgetIds': 25 | return []; 26 | case 'reloadWidgets': 27 | return true; 28 | case 'widgetExist': 29 | return true; 30 | default: 31 | return null; 32 | } 33 | }); 34 | }); 35 | 36 | group('iOS', () { 37 | setUp(() { 38 | debugDefaultTargetPlatformOverride = TargetPlatform.iOS; 39 | 40 | // AppWidgetPlatform.instance 41 | }); 42 | 43 | tearDownAll(() { 44 | log.clear(); 45 | }); 46 | 47 | test('cancelConfigureWidget', () async { 48 | final appWidgetPlugin = AppWidgetPlugin(); 49 | 50 | expect( 51 | appWidgetPlugin.cancelConfigureWidget(), 52 | throwsA( 53 | isA().having( 54 | (e) => e.toString().contains('LateInitializationError'), 55 | 'LateInitializationError', 56 | isTrue, 57 | ), 58 | ), 59 | ); 60 | }); 61 | 62 | test('configureWidget', () async { 63 | final appWidgetPlugin = AppWidgetPlugin(); 64 | 65 | expect( 66 | appWidgetPlugin.configureWidget(), 67 | throwsA( 68 | isA().having( 69 | (e) => e.toString().contains('LateInitializationError'), 70 | 'LateInitializationError', 71 | isTrue, 72 | ), 73 | ), 74 | ); 75 | }); 76 | 77 | test('getWidgetIds', () async { 78 | final appWidgetPlugin = AppWidgetPlugin(); 79 | 80 | expect( 81 | () => appWidgetPlugin.getWidgetIds( 82 | androidProviderName: 'TestProvider', 83 | ), 84 | throwsA( 85 | isA().having( 86 | (e) => e.toString().contains('LateInitializationError'), 87 | 'LateInitializationError', 88 | isTrue, 89 | ), 90 | ), 91 | ); 92 | }); 93 | 94 | test('reloadWidgets', () async { 95 | final appWidgetPlugin = AppWidgetPlugin(); 96 | 97 | expect( 98 | appWidgetPlugin.reloadWidgets(), 99 | throwsA( 100 | isA().having( 101 | (e) => e.toString().contains('LateInitializationError'), 102 | 'LateInitializationError', 103 | isTrue, 104 | ), 105 | ), 106 | ); 107 | }); 108 | 109 | test('updateWidget', () async { 110 | final appWidgetPlugin = AppWidgetPlugin(); 111 | 112 | expect( 113 | appWidgetPlugin.updateWidget(), 114 | throwsA( 115 | isA().having( 116 | (e) => e.toString().contains('LateInitializationError'), 117 | 'LateInitializationError', 118 | isTrue, 119 | ), 120 | ), 121 | ); 122 | }); 123 | 124 | test('widgetExist', () async { 125 | final appWidgetPlugin = AppWidgetPlugin(); 126 | 127 | expect( 128 | appWidgetPlugin.widgetExist(12), 129 | throwsA( 130 | isA().having( 131 | (e) => e.toString().contains('LateInitializationError'), 132 | 'LateInitializationError', 133 | isTrue, 134 | ), 135 | ), 136 | ); 137 | }); 138 | }); 139 | } 140 | -------------------------------------------------------------------------------- /app_widget_android/test/app_widget_android_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: inference_failure_on_collection_literal 2 | 3 | import 'package:app_widget_android/app_widget_android.dart'; 4 | import 'package:app_widget_platform_interface/app_widget_platform_interface.dart'; 5 | import 'package:flutter_test/flutter_test.dart'; 6 | import 'package:plugin_platform_interface/plugin_platform_interface.dart'; 7 | 8 | void main() { 9 | final AppWidgetPlatform initialPlatform = MockAppWidgetAndroidPlatform(); 10 | 11 | test('$AppWidgetAndroid is the default instance', () { 12 | expect(initialPlatform, isInstanceOf()); 13 | }); 14 | 15 | test('cancelConfigureWidget', () async { 16 | AppWidgetPlatform appWidgetAndroidPlugin = MockAppWidgetAndroidPlatform(); 17 | MockAppWidgetAndroidPlatform fakePlatform = MockAppWidgetAndroidPlatform(); 18 | AppWidgetPlatform.instance = fakePlatform; 19 | 20 | expect(await appWidgetAndroidPlugin.cancelConfigureWidget(), isTrue); 21 | }); 22 | 23 | test('configureWidget', () async { 24 | AppWidgetPlatform appWidgetAndroidPlugin = MockAppWidgetAndroidPlatform(); 25 | MockAppWidgetAndroidPlatform fakePlatform = MockAppWidgetAndroidPlatform(); 26 | AppWidgetPlatform.instance = fakePlatform; 27 | 28 | expect(await appWidgetAndroidPlugin.configureWidget(), isTrue); 29 | }); 30 | 31 | test('getWidgetIds', () async { 32 | AppWidgetPlatform appWidgetAndroidPlugin = MockAppWidgetAndroidPlatform(); 33 | MockAppWidgetAndroidPlatform fakePlatform = MockAppWidgetAndroidPlatform(); 34 | AppWidgetPlatform.instance = fakePlatform; 35 | 36 | expect( 37 | await appWidgetAndroidPlugin.getWidgetIds(androidProviderName: 'name'), 38 | [42], 39 | ); 40 | }); 41 | 42 | test('reloadWidgets', () async { 43 | AppWidgetPlatform appWidgetAndroidPlugin = MockAppWidgetAndroidPlatform(); 44 | MockAppWidgetAndroidPlatform fakePlatform = MockAppWidgetAndroidPlatform(); 45 | AppWidgetPlatform.instance = fakePlatform; 46 | 47 | expect(await appWidgetAndroidPlugin.reloadWidgets(), isTrue); 48 | }); 49 | 50 | test('updateWidget', () async { 51 | AppWidgetPlatform appWidgetAndroidPlugin = MockAppWidgetAndroidPlatform(); 52 | MockAppWidgetAndroidPlatform fakePlatform = MockAppWidgetAndroidPlatform(); 53 | AppWidgetPlatform.instance = fakePlatform; 54 | 55 | expect(await appWidgetAndroidPlugin.cancelConfigureWidget(), isTrue); 56 | }); 57 | 58 | test('widgetExist', () async { 59 | AppWidgetPlatform appWidgetAndroidPlugin = MockAppWidgetAndroidPlatform(); 60 | MockAppWidgetAndroidPlatform fakePlatform = MockAppWidgetAndroidPlatform(); 61 | AppWidgetPlatform.instance = fakePlatform; 62 | 63 | expect(await appWidgetAndroidPlugin.widgetExist(1), isTrue); 64 | }); 65 | } 66 | 67 | class MockAppWidgetAndroidPlatform 68 | with MockPlatformInterfaceMixin 69 | implements AppWidgetPlatform { 70 | @override 71 | Future cancelConfigureWidget() async { 72 | return true; 73 | } 74 | 75 | @override 76 | Future configureWidget({ 77 | String? androidPackageName, 78 | int? widgetId, 79 | int? layoutId, 80 | String? widgetLayout, 81 | String? widgetContainerName, 82 | Map? textViews, 83 | String? payload, 84 | String? url, 85 | }) async { 86 | return true; 87 | } 88 | 89 | @override 90 | Future reloadWidgets({ 91 | String? androidPackageName, 92 | String? androidProviderName, 93 | }) async { 94 | return true; 95 | } 96 | 97 | @override 98 | Future updateWidget({ 99 | String? androidPackageName, 100 | int? widgetId, 101 | int? layoutId, 102 | String? widgetLayout, 103 | Map? textViews, 104 | String? payload, 105 | String? url, 106 | }) async { 107 | return true; 108 | } 109 | 110 | @override 111 | Future widgetExist(int widgetId) async { 112 | return true; 113 | } 114 | 115 | @override 116 | Future?> getWidgetIds({ 117 | String? androidPackageName, 118 | String? androidProviderName, 119 | }) async { 120 | return [42]; 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /app_widget_android/lib/src/app_widget_android_plugin.dart: -------------------------------------------------------------------------------- 1 | import 'package:app_widget_android/src/app_widget_android_platform.dart'; 2 | import 'package:app_widget_platform_interface/app_widget_platform_interface.dart'; 3 | import 'package:flutter/services.dart'; 4 | 5 | const onConfigureWidgetCallback = 'onConfigureWidget'; 6 | const onClickWidgetCallback = 'onClickWidget'; 7 | const onUpdateWidgetsCallback = 'onUpdateWidgets'; 8 | const onDeletedWidgetsCallback = 'onDeletedWidgets'; 9 | 10 | const MethodChannel _methodChannel = MethodChannel(AppWidgetPlatform.channel); 11 | 12 | class AppWidgetAndroidPlugin extends AppWidgetAndroid { 13 | AppWidgetAndroidPlugin({ 14 | void Function(int widgetId, int layoutId, String layoutName)? 15 | onConfigureWidget, 16 | void Function(String? payload)? onClickWidget, 17 | }) : _onConfigureWidget = onConfigureWidget, 18 | _onClickWidget = onClickWidget { 19 | _methodChannel.setMethodCallHandler(handleMethod); 20 | } 21 | 22 | final void Function(int widgetId, int layoutId, String layoutName)? 23 | _onConfigureWidget; 24 | final void Function(String? payload)? _onClickWidget; 25 | 26 | Future handleMethod(MethodCall call) async { 27 | switch (call.method) { 28 | case onConfigureWidgetCallback: 29 | final widgetId = call.arguments['widgetId'] as int; 30 | final layoutId = call.arguments['layoutId'] as int; 31 | final layoutName = call.arguments['layoutName'] as String; 32 | return _onConfigureWidget?.call(widgetId, layoutId, layoutName); 33 | case onClickWidgetCallback: 34 | return _onClickWidget?.call(call.arguments['payload'] as String); 35 | default: 36 | throw UnimplementedError('Method ${call.method} is not implemented!'); 37 | } 38 | } 39 | 40 | @override 41 | Future cancelConfigureWidget() { 42 | return _methodChannel.invokeMethod('cancelConfigureWidget'); 43 | } 44 | 45 | @override 46 | Future configureWidget({ 47 | String? androidPackageName, 48 | int? widgetId, 49 | int? layoutId, 50 | Map? textViews = const {}, 51 | String? payload, 52 | String? url, 53 | }) { 54 | assert(widgetId != null, 'widgetId is required for android!'); 55 | assert(layoutId != null, 'layoutId is required for android!'); 56 | 57 | return _methodChannel.invokeMethod('configureWidget', { 58 | 'androidPackageName': androidPackageName, 59 | 'widgetId': widgetId, 60 | 'layoutId': layoutId, 61 | 'textViews': textViews, 62 | 'payload': payload, 63 | 'url': url, 64 | }); 65 | } 66 | 67 | @override 68 | Future?> getWidgetIds({ 69 | String? androidPackageName, 70 | String? androidProviderName, 71 | }) async { 72 | assert( 73 | androidProviderName != null, 74 | 'androidProviderName is required for android!', 75 | ); 76 | 77 | final widgetIds = 78 | await _methodChannel.invokeMethod?>('getWidgetIds', { 79 | if (androidPackageName != null) ...{ 80 | 'androidPackageName': androidPackageName, 81 | }, 82 | 'androidProviderName': androidProviderName, 83 | }); 84 | 85 | return widgetIds?.map((id) => id as int).toList(); 86 | } 87 | 88 | @override 89 | Future reloadWidgets({ 90 | String? androidPackageName, 91 | String? androidProviderName, 92 | }) { 93 | assert( 94 | androidProviderName != null, 95 | 'androidProviderName is required for android!', 96 | ); 97 | 98 | return _methodChannel.invokeMethod( 99 | 'reloadWidgets', 100 | { 101 | if (androidPackageName != null) ...{ 102 | 'androidPackageName': androidPackageName, 103 | }, 104 | 'androidProviderName': androidProviderName, 105 | }, 106 | ); 107 | } 108 | 109 | @override 110 | Future updateWidget({ 111 | String? androidPackageName, 112 | int? widgetId, 113 | int? layoutId, 114 | String? widgetLayout, 115 | Map? textViews = const {}, 116 | String? payload, 117 | String? url, 118 | }) { 119 | assert(widgetId != null, 'widgetId is required for android!'); 120 | assert(layoutId != null, 'layoutId is required for android!'); 121 | 122 | return _methodChannel.invokeMethod('updateWidget', { 123 | if (androidPackageName != null) ...{ 124 | 'androidPackageName': androidPackageName, 125 | }, 126 | 'widgetId': widgetId, 127 | 'layoutId': layoutId, 128 | 'textViews': textViews, 129 | 'payload': payload, 130 | 'url': url, 131 | }); 132 | } 133 | 134 | @override 135 | Future widgetExist(int widgetId) { 136 | return _methodChannel 137 | .invokeMethod('widgetExist', {'widgetId': widgetId}); 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /app_widget_android/android/src/main/kotlin/tech/noxasch/app_widget/AppWidgetPlugin.kt: -------------------------------------------------------------------------------- 1 | package tech.noxasch.app_widget 2 | 3 | import android.app.Activity 4 | import android.appwidget.AppWidgetManager 5 | import android.content.Context 6 | import android.content.Intent 7 | import androidx.annotation.Keep 8 | import androidx.annotation.NonNull 9 | import io.flutter.embedding.engine.plugins.FlutterPlugin 10 | import io.flutter.embedding.engine.plugins.activity.ActivityAware 11 | import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding 12 | import io.flutter.plugin.common.PluginRegistry 13 | 14 | 15 | @Keep 16 | class AppWidgetPlugin: FlutterPlugin, ActivityAware, 17 | PluginRegistry.NewIntentListener { 18 | 19 | private var activity: Activity? = null 20 | private var methodCallHandler: AppWidgetMethodCallHandler? = null 21 | 22 | /// namespacing constant that need to be access from outside. 23 | /// similar to android string constant pattern 24 | companion object { 25 | @JvmStatic 26 | val TAG = "APP_WIDGET_PLUGIN" 27 | @JvmStatic 28 | val CHANNEL = "tech.noxasch.flutter/app_widget_foreground" 29 | @JvmStatic 30 | val CONFIGURE_WIDGET_ACTION_CALLBACK = "tech.noxasch.flutter.CONFIGURE_CALLBACK" 31 | @JvmStatic 32 | val CLICK_WIDGET_ACTION = "tech.noxasch.flutter.CLICK_WIDGET" 33 | @JvmStatic 34 | val CLICK_WIDGET_ACTION_CALLBACK = "tech.noxasch.flutter.CLICK_CALLBACK" 35 | 36 | @JvmStatic 37 | val EXTRA_PAYLOAD = "dataPayload" 38 | @JvmStatic 39 | val EXTRA_APP_ITEM_ID = "appItemId" 40 | @JvmStatic 41 | val EXTRA_APP_STRING_UID = "appStringUid" 42 | 43 | @JvmStatic 44 | val ON_CONFIGURE_WIDGET_CALLBACK = "onConfigureWidget" 45 | @JvmStatic 46 | val ON_ClICK_WIDGET_CALLBACK = "onClickWidget" 47 | 48 | // this method rethrow intent with a different name 49 | // to pass intent to onNewIntent 50 | // calling methodChannel in other method is too early 51 | // since they are called before flutter Ui is displayed 52 | @JvmStatic 53 | fun handleWidgetAction(context: Context, intent: Intent) { 54 | when(intent.action) { 55 | AppWidgetManager.ACTION_APPWIDGET_CONFIGURE -> handleConfigureAction(context, intent) 56 | CLICK_WIDGET_ACTION -> handleOnClickAction(context, intent) 57 | } 58 | } 59 | 60 | @JvmStatic 61 | private fun handleOnClickAction(context : Context, intent: Intent) { 62 | val clickIntent = Intent(context, context.javaClass) 63 | clickIntent.action = CLICK_WIDGET_ACTION_CALLBACK 64 | 65 | clickIntent.putExtras(intent) 66 | context.startActivity(clickIntent) 67 | } 68 | 69 | @JvmStatic 70 | private fun handleConfigureAction(context : Context, intent: Intent) { 71 | val extras = intent.extras 72 | val widgetId: Int = extras?.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID) ?: return 73 | if (widgetId == 0) return 74 | 75 | val widgetManager = AppWidgetManager.getInstance(context) 76 | val appWidgetInfo = widgetManager.getAppWidgetInfo(widgetId) 77 | val layoutId = appWidgetInfo.initialLayout 78 | val layoutName = context.resources.getResourceName(layoutId) 79 | 80 | val configIntent = Intent(context, context.javaClass) 81 | configIntent.action = CONFIGURE_WIDGET_ACTION_CALLBACK 82 | configIntent.putExtra("widgetId", widgetId) 83 | configIntent.putExtra("layoutId", layoutId) 84 | configIntent.putExtra("layoutName", layoutName) 85 | context.startActivity(configIntent) 86 | } 87 | } 88 | 89 | override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { 90 | methodCallHandler = AppWidgetMethodCallHandler(flutterPluginBinding.applicationContext) 91 | methodCallHandler!!.open(flutterPluginBinding.binaryMessenger) 92 | } 93 | 94 | override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { 95 | methodCallHandler!!.close() 96 | } 97 | 98 | override fun onAttachedToActivity(binding: ActivityPluginBinding) { 99 | activity = binding.activity 100 | binding.addOnNewIntentListener(this) 101 | methodCallHandler!!.setActivity(activity) 102 | } 103 | 104 | override fun onDetachedFromActivityForConfigChanges() { 105 | activity = null 106 | } 107 | 108 | override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { 109 | activity = binding.activity 110 | binding.addOnNewIntentListener(this) 111 | } 112 | 113 | override fun onDetachedFromActivity() { 114 | activity = null 115 | } 116 | 117 | // this only called when the activity already started 118 | // we need to rethrow the intent here since we don't have access to onUiDisplayed 119 | override fun onNewIntent(intent: Intent): Boolean { 120 | if (intent.action != null) { 121 | when (intent.action) { 122 | CONFIGURE_WIDGET_ACTION_CALLBACK -> methodCallHandler!!.handleConfigureIntent(intent) 123 | CLICK_WIDGET_ACTION_CALLBACK -> methodCallHandler!!.handleClickIntent(intent) 124 | } 125 | } 126 | 127 | return false 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /app_widget_android/example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | app_widget_android: 5 | dependency: "direct main" 6 | description: 7 | path: ".." 8 | relative: true 9 | source: path 10 | version: "0.3.3" 11 | app_widget_platform_interface: 12 | dependency: transitive 13 | description: 14 | name: app_widget_platform_interface 15 | sha256: a288112ec826c25e7638ddc30c33a5e7279cfc2eadba89ccb33bc8d3ddbb589c 16 | url: "https://pub.dev" 17 | source: hosted 18 | version: "0.4.0" 19 | async: 20 | dependency: transitive 21 | description: 22 | name: async 23 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 24 | url: "https://pub.dev" 25 | source: hosted 26 | version: "2.11.0" 27 | boolean_selector: 28 | dependency: transitive 29 | description: 30 | name: boolean_selector 31 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 32 | url: "https://pub.dev" 33 | source: hosted 34 | version: "2.1.1" 35 | characters: 36 | dependency: transitive 37 | description: 38 | name: characters 39 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 40 | url: "https://pub.dev" 41 | source: hosted 42 | version: "1.3.0" 43 | clock: 44 | dependency: transitive 45 | description: 46 | name: clock 47 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 48 | url: "https://pub.dev" 49 | source: hosted 50 | version: "1.1.1" 51 | collection: 52 | dependency: transitive 53 | description: 54 | name: collection 55 | sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 56 | url: "https://pub.dev" 57 | source: hosted 58 | version: "1.17.2" 59 | cupertino_icons: 60 | dependency: "direct main" 61 | description: 62 | name: cupertino_icons 63 | sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be 64 | url: "https://pub.dev" 65 | source: hosted 66 | version: "1.0.5" 67 | fake_async: 68 | dependency: transitive 69 | description: 70 | name: fake_async 71 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 72 | url: "https://pub.dev" 73 | source: hosted 74 | version: "1.3.1" 75 | flutter: 76 | dependency: "direct main" 77 | description: flutter 78 | source: sdk 79 | version: "0.0.0" 80 | flutter_lints: 81 | dependency: "direct dev" 82 | description: 83 | name: flutter_lints 84 | sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c 85 | url: "https://pub.dev" 86 | source: hosted 87 | version: "2.0.1" 88 | flutter_test: 89 | dependency: "direct dev" 90 | description: flutter 91 | source: sdk 92 | version: "0.0.0" 93 | lints: 94 | dependency: transitive 95 | description: 96 | name: lints 97 | sha256: "5cfd6509652ff5e7fe149b6df4859e687fca9048437857cb2e65c8d780f396e3" 98 | url: "https://pub.dev" 99 | source: hosted 100 | version: "2.0.0" 101 | matcher: 102 | dependency: transitive 103 | description: 104 | name: matcher 105 | sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" 106 | url: "https://pub.dev" 107 | source: hosted 108 | version: "0.12.16" 109 | material_color_utilities: 110 | dependency: transitive 111 | description: 112 | name: material_color_utilities 113 | sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" 114 | url: "https://pub.dev" 115 | source: hosted 116 | version: "0.5.0" 117 | meta: 118 | dependency: transitive 119 | description: 120 | name: meta 121 | sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" 122 | url: "https://pub.dev" 123 | source: hosted 124 | version: "1.9.1" 125 | path: 126 | dependency: transitive 127 | description: 128 | name: path 129 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 130 | url: "https://pub.dev" 131 | source: hosted 132 | version: "1.8.3" 133 | plugin_platform_interface: 134 | dependency: transitive 135 | description: 136 | name: plugin_platform_interface 137 | sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 138 | url: "https://pub.dev" 139 | source: hosted 140 | version: "2.1.7" 141 | sky_engine: 142 | dependency: transitive 143 | description: flutter 144 | source: sdk 145 | version: "0.0.99" 146 | source_span: 147 | dependency: transitive 148 | description: 149 | name: source_span 150 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 151 | url: "https://pub.dev" 152 | source: hosted 153 | version: "1.10.0" 154 | stack_trace: 155 | dependency: transitive 156 | description: 157 | name: stack_trace 158 | sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 159 | url: "https://pub.dev" 160 | source: hosted 161 | version: "1.11.0" 162 | stream_channel: 163 | dependency: transitive 164 | description: 165 | name: stream_channel 166 | sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" 167 | url: "https://pub.dev" 168 | source: hosted 169 | version: "2.1.1" 170 | string_scanner: 171 | dependency: transitive 172 | description: 173 | name: string_scanner 174 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 175 | url: "https://pub.dev" 176 | source: hosted 177 | version: "1.2.0" 178 | term_glyph: 179 | dependency: transitive 180 | description: 181 | name: term_glyph 182 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 183 | url: "https://pub.dev" 184 | source: hosted 185 | version: "1.2.1" 186 | test_api: 187 | dependency: transitive 188 | description: 189 | name: test_api 190 | sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" 191 | url: "https://pub.dev" 192 | source: hosted 193 | version: "0.6.0" 194 | vector_math: 195 | dependency: transitive 196 | description: 197 | name: vector_math 198 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 199 | url: "https://pub.dev" 200 | source: hosted 201 | version: "2.1.4" 202 | web: 203 | dependency: transitive 204 | description: 205 | name: web 206 | sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 207 | url: "https://pub.dev" 208 | source: hosted 209 | version: "0.1.4-beta" 210 | sdks: 211 | dart: ">=3.1.0-185.0.dev <4.0.0" 212 | flutter: ">=2.5.0" 213 | -------------------------------------------------------------------------------- /app_widget/lib/src/app_widget_plugin.dart: -------------------------------------------------------------------------------- 1 | import 'package:app_widget_android/app_widget_android.dart'; 2 | import 'package:app_widget_platform_interface/app_widget_platform_interface.dart'; 3 | import 'package:flutter/foundation.dart'; 4 | 5 | /// Instantiate plugin instance and register callback optional callback 6 | /// 7 | /// Accept an optional [androidPackageName] string which use to properly works for flavor that have different package name 8 | /// than the packageName for MainActivity
9 | /// Accept [onConfigureWidget] callback method - optional
10 | /// Accept [onClickWidget] callback method - optional
11 | /// 12 | /// onClickWidget payload: 13 | /// 14 | /// ```dart 15 | /// { 16 | /// "widgetId" : 23, 17 | /// "itemId": 232, // (if set during update/configure otherwise it is null) 18 | /// "stringUid": "dadas", // (if set during update/configure otherwise it is null) 19 | /// } 20 | /// ``` 21 | /// 22 | class AppWidgetPlugin { 23 | factory AppWidgetPlugin({ 24 | String? androidPackageName, 25 | 26 | /// callback function when the widget is first created 27 | void Function(int widgetId, int layoutId, String layoutName)? 28 | onConfigureWidget, 29 | void Function(String? payload)? onClickWidget, 30 | }) { 31 | if (instance != null) return instance!; 32 | instance = AppWidgetPlugin._( 33 | androidPackageName: androidPackageName, 34 | onConfigureWidget: onConfigureWidget, 35 | onClickWidget: onClickWidget, 36 | ); 37 | 38 | return instance!; 39 | } 40 | 41 | AppWidgetPlugin._({ 42 | required String? androidPackageName, 43 | required void Function(int widgetId, int layoutId, String layoutName)? 44 | onConfigureWidget, 45 | required void Function(String? payload)? onClickWidget, 46 | }) : _onConfigureWidget = onConfigureWidget, 47 | _onClickWidget = onClickWidget, 48 | _androidPackageName = androidPackageName { 49 | if (kIsWeb) { 50 | return; 51 | } 52 | if (defaultTargetPlatform == TargetPlatform.android) { 53 | AppWidgetPlatform.instance = AppWidgetAndroidPlugin( 54 | onConfigureWidget: _onConfigureWidget, 55 | onClickWidget: _onClickWidget, 56 | ); 57 | } else if (defaultTargetPlatform == TargetPlatform.iOS) { 58 | return; 59 | } 60 | } 61 | 62 | static AppWidgetPlugin? instance; 63 | 64 | final String? _androidPackageName; 65 | 66 | /// callback function when the widget is first created 67 | final void Function(int widgetId, int layoutId, String layoutName)? 68 | _onConfigureWidget; 69 | 70 | /// payload keys: 71 | /// - itemId 72 | /// - stringUid 73 | /// - widgetId 74 | final void Function(String? payload)? _onClickWidget; 75 | 76 | /// Cancel widget configuration 77 | /// 78 | /// this will send Activity.RESULT_CANCELLED on android 79 | Future cancelConfigureWidget() async { 80 | return AppWidgetPlatform.instance.cancelConfigureWidget(); 81 | } 82 | 83 | /// Configure Widget for the first time 84 | /// 85 | /// [androidPackageName] should be the app package name. 86 | /// eg: com.example.myapp 87 | /// 88 | /// [widgetLayout] is the layout filename without extension 89 | /// 90 | /// Get the [WidgetId] from [onConfigureWidget] callback. 91 | /// 92 | /// [textViews] is the id defined in layout ` configureWidget({ 105 | int? widgetId, 106 | int? layoutId, 107 | Map? textViews = const {}, 108 | String? payload, 109 | String? url, 110 | String? androidPackageName, 111 | }) async { 112 | return AppWidgetPlatform.instance.configureWidget( 113 | androidPackageName: androidPackageName ?? _androidPackageName, 114 | widgetId: widgetId, 115 | layoutId: layoutId, 116 | textViews: textViews, 117 | payload: payload, 118 | url: url, 119 | ); 120 | } 121 | 122 | /// 123 | /// Get all widgetId associated with a AppWidgetProvider 124 | /// 125 | /// [androidProviderName] is the provider class name which also it's filename
126 | /// eg: `AppWidgetExampleProvider` 127 | /// 128 | Future?> getWidgetIds({ 129 | required String androidProviderName, 130 | String? androidPackageName, 131 | }) { 132 | return AppWidgetPlatform.instance.getWidgetIds( 133 | androidPackageName: androidPackageName ?? _androidPackageName, 134 | androidProviderName: androidProviderName, 135 | ); 136 | } 137 | 138 | /// Force reload all widgets 139 | /// 140 | /// This is a convenient method to force reload all widgets from dart side. 141 | /// 142 | /// This will trigger onUpdate method on android side. 143 | /// Use this if you handle widget update from `AppWidgetProvider` directly 144 | /// otherwise this method is useless. 145 | /// 146 | /// [androidProviderName] is the provider class name which also it's filename
147 | /// eg: `AppWidgetExampleProvider` 148 | /// 149 | Future reloadWidgets({ 150 | String? androidProviderName, 151 | String? androidPackageName, 152 | }) async { 153 | return AppWidgetPlatform.instance.reloadWidgets( 154 | androidPackageName: androidPackageName ?? _androidPackageName, 155 | androidProviderName: androidProviderName, 156 | ); 157 | } 158 | 159 | /// Update widget view manually 160 | /// 161 | /// [androidPackageName] should be the app package name. 162 | /// eg: com.example.myapp 163 | /// 164 | /// [widgetLayout] is the layout filename without extension 165 | /// 166 | /// Get the [WidgetId] from [onConfigureWidget] callback when the widget 167 | /// is created for the first time. 168 | /// 169 | /// [textViews] is the id defined in layout ` updateWidget({ 182 | int? widgetId, 183 | int? layoutId, 184 | String? widgetLayout, 185 | Map? textViews = const {}, 186 | String? payload, 187 | String? url, 188 | String? androidPackageName, 189 | }) async { 190 | return AppWidgetPlatform.instance.updateWidget( 191 | androidPackageName: androidPackageName ?? _androidPackageName, 192 | widgetId: widgetId, 193 | layoutId: layoutId, 194 | textViews: textViews, 195 | payload: payload, 196 | url: url, 197 | ); 198 | } 199 | 200 | /// return [true] if a widget with given [widgetId] exist 201 | Future widgetExist(int widgetId) async { 202 | return AppWidgetPlatform.instance.widgetExist(widgetId); 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /app_widget/test/app_widget_test.dart: -------------------------------------------------------------------------------- 1 | // ignore_for_file: inference_failure_on_collection_literal 2 | 3 | import 'package:app_widget/app_widget.dart'; 4 | import 'package:app_widget_platform_interface/app_widget_platform_interface.dart'; 5 | import 'package:flutter/foundation.dart'; 6 | import 'package:flutter/services.dart'; 7 | import 'package:flutter_test/flutter_test.dart'; 8 | 9 | // NOTE: this merely test platform specific (android) interface 10 | void main() { 11 | TestWidgetsFlutterBinding.ensureInitialized(); 12 | 13 | const MethodChannel channel = MethodChannel(AppWidgetPlatform.channel); 14 | final List log = []; 15 | 16 | setUpAll(() { 17 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger 18 | .setMockMethodCallHandler(channel, (methodCall) async { 19 | log.add(methodCall); 20 | switch (methodCall.method) { 21 | case 'getPlatformVersion': 22 | return '42'; 23 | case 'configureWidget': 24 | return true; 25 | case 'cancelConfigureWidget': 26 | return true; 27 | case 'getWidgetIds': 28 | return [42]; 29 | case 'reloadWidgets': 30 | return true; 31 | case 'updateWidget': 32 | return true; 33 | case 'widgetExist': 34 | return true; 35 | default: 36 | return false; 37 | } 38 | }); 39 | }); 40 | 41 | group('Android', () { 42 | setUp(() { 43 | debugDefaultTargetPlatformOverride = TargetPlatform.android; 44 | }); 45 | 46 | tearDown(() { 47 | log.clear(); 48 | }); 49 | 50 | test('configureWidget', () async { 51 | final appWidgetPlugin = AppWidgetPlugin( 52 | androidPackageName: 'appname', 53 | ); 54 | 55 | expect( 56 | appWidgetPlugin.configureWidget( 57 | widgetId: 1, 58 | layoutId: 1, 59 | payload: '{"itemId": 1, "stringUid": "uid"}', 60 | url: 'https://google.come', 61 | ), 62 | completion(true), 63 | ); 64 | 65 | expect(log, [ 66 | isMethodCall( 67 | 'configureWidget', 68 | arguments: { 69 | 'androidPackageName': 'appname', 70 | 'widgetId': 1, 71 | 'layoutId': 1, 72 | 'textViews': {}, 73 | 'payload': '{"itemId": 1, "stringUid": "uid"}', 74 | 'url': 'https://google.come', 75 | }, 76 | ), 77 | ]); 78 | }); 79 | 80 | test('configureWidget diff package name', () async { 81 | final appWidgetPlugin = AppWidgetPlugin( 82 | androidPackageName: 'appname', 83 | ); 84 | 85 | expect( 86 | appWidgetPlugin.configureWidget( 87 | widgetId: 1, 88 | layoutId: 1, 89 | payload: '{"itemId": 1, "stringUid": "uid"}', 90 | url: 'https://google.come', 91 | androidPackageName: 'appname2', 92 | ), 93 | completion(true), 94 | ); 95 | 96 | expect(log, [ 97 | isMethodCall( 98 | 'configureWidget', 99 | arguments: { 100 | 'androidPackageName': 'appname2', 101 | 'widgetId': 1, 102 | 'layoutId': 1, 103 | 'textViews': {}, 104 | 'payload': '{"itemId": 1, "stringUid": "uid"}', 105 | 'url': 'https://google.come', 106 | }, 107 | ), 108 | ]); 109 | }); 110 | 111 | test('updateWidget', () async { 112 | final appWidgetPlugin = AppWidgetPlugin( 113 | androidPackageName: 'appname', 114 | ); 115 | 116 | expect( 117 | appWidgetPlugin.updateWidget( 118 | widgetId: 1, 119 | layoutId: 1, 120 | payload: '{"itemId": 1, "stringUid": "uid"}', 121 | url: 'https://google.come', 122 | ), 123 | completion(true), 124 | ); 125 | 126 | expect(log, [ 127 | isMethodCall( 128 | 'updateWidget', 129 | arguments: { 130 | 'androidPackageName': 'appname', 131 | 'widgetId': 1, 132 | 'layoutId': 1, 133 | 'textViews': {}, 134 | 'payload': '{"itemId": 1, "stringUid": "uid"}', 135 | 'url': 'https://google.come', 136 | }, 137 | ), 138 | ]); 139 | }); 140 | 141 | test('updateWidget different widget package name', () async { 142 | final appWidgetPlugin = AppWidgetPlugin( 143 | androidPackageName: 'appname', 144 | ); 145 | 146 | expect( 147 | appWidgetPlugin.updateWidget( 148 | androidPackageName: 'appname2', 149 | widgetId: 1, 150 | layoutId: 1, 151 | payload: '{"itemId": 1, "stringUid": "uid"}', 152 | url: 'https://google.come', 153 | ), 154 | completion(true), 155 | ); 156 | 157 | expect(log, [ 158 | isMethodCall( 159 | 'updateWidget', 160 | arguments: { 161 | 'androidPackageName': 'appname2', 162 | 'widgetId': 1, 163 | 'layoutId': 1, 164 | 'textViews': {}, 165 | 'payload': '{"itemId": 1, "stringUid": "uid"}', 166 | 'url': 'https://google.come', 167 | }, 168 | ), 169 | ]); 170 | }); 171 | 172 | test('cancelConfigureWidget', () async { 173 | final appWidgetPlugin = AppWidgetPlugin(); 174 | 175 | expect( 176 | appWidgetPlugin.cancelConfigureWidget(), 177 | completion(true), 178 | ); 179 | 180 | expect(log, [ 181 | isMethodCall( 182 | 'cancelConfigureWidget', 183 | arguments: null, 184 | ), 185 | ]); 186 | }); 187 | 188 | test('getWidgetIds', () async { 189 | final appWidgetPlugin = AppWidgetPlugin(); 190 | 191 | expect( 192 | appWidgetPlugin.getWidgetIds( 193 | androidProviderName: 'TestProvider', 194 | ), 195 | completion([42]), 196 | ); 197 | 198 | expect(log, [ 199 | isMethodCall( 200 | 'getWidgetIds', 201 | arguments: { 202 | 'androidProviderName': 'TestProvider', 203 | 'androidPackageName': 'appname', 204 | }, 205 | ), 206 | ]); 207 | }); 208 | 209 | test('getWidgetIds diff package name', () async { 210 | final appWidgetPlugin = AppWidgetPlugin(); 211 | 212 | expect( 213 | appWidgetPlugin.getWidgetIds( 214 | androidProviderName: 'TestProvider', 215 | androidPackageName: 'appname2', 216 | ), 217 | completion([42]), 218 | ); 219 | 220 | expect(log, [ 221 | isMethodCall( 222 | 'getWidgetIds', 223 | arguments: { 224 | 'androidProviderName': 'TestProvider', 225 | 'androidPackageName': 'appname2', 226 | }, 227 | ), 228 | ]); 229 | }); 230 | 231 | test('reloadWidgets', () async { 232 | final appWidgetPlugin = AppWidgetPlugin(); 233 | 234 | expect( 235 | appWidgetPlugin.reloadWidgets( 236 | androidProviderName: 'TestProvider', 237 | ), 238 | completion(true), 239 | ); 240 | 241 | expect(log, [ 242 | isMethodCall( 243 | 'reloadWidgets', 244 | arguments: { 245 | 'androidProviderName': 'TestProvider', 246 | 'androidPackageName': 'appname', 247 | }, 248 | ), 249 | ]); 250 | }); 251 | 252 | test('reloadWidgets diff package name', () async { 253 | final appWidgetPlugin = AppWidgetPlugin(); 254 | 255 | expect( 256 | appWidgetPlugin.reloadWidgets( 257 | androidProviderName: 'TestProvider', 258 | androidPackageName: 'appname2', 259 | ), 260 | completion(true), 261 | ); 262 | 263 | expect(log, [ 264 | isMethodCall( 265 | 'reloadWidgets', 266 | arguments: { 267 | 'androidProviderName': 'TestProvider', 268 | 'androidPackageName': 'appname2', 269 | }, 270 | ), 271 | ]); 272 | }); 273 | 274 | test('widgetExist', () async { 275 | final appWidgetPlugin = AppWidgetPlugin(); 276 | 277 | expect( 278 | appWidgetPlugin.widgetExist(12), 279 | completion(true), 280 | ); 281 | 282 | expect(log, [ 283 | isMethodCall( 284 | 'widgetExist', 285 | arguments: { 286 | 'widgetId': 12, 287 | }, 288 | ), 289 | ]); 290 | }); 291 | }); 292 | } 293 | -------------------------------------------------------------------------------- /app_widget/example/pubspec.lock: -------------------------------------------------------------------------------- 1 | # Generated by pub 2 | # See https://dart.dev/tools/pub/glossary#lockfile 3 | packages: 4 | app_widget: 5 | dependency: "direct main" 6 | description: 7 | path: ".." 8 | relative: true 9 | source: path 10 | version: "0.3.1" 11 | app_widget_android: 12 | dependency: transitive 13 | description: 14 | name: app_widget_android 15 | sha256: "9a0aa193b373bf2e548d5af721e021b166f607ad932fc144334ce55b4ff40320" 16 | url: "https://pub.dev" 17 | source: hosted 18 | version: "0.4.0" 19 | app_widget_platform_interface: 20 | dependency: transitive 21 | description: 22 | name: app_widget_platform_interface 23 | sha256: a288112ec826c25e7638ddc30c33a5e7279cfc2eadba89ccb33bc8d3ddbb589c 24 | url: "https://pub.dev" 25 | source: hosted 26 | version: "0.4.0" 27 | async: 28 | dependency: transitive 29 | description: 30 | name: async 31 | sha256: "947bfcf187f74dbc5e146c9eb9c0f10c9f8b30743e341481c1e2ed3ecc18c20c" 32 | url: "https://pub.dev" 33 | source: hosted 34 | version: "2.11.0" 35 | boolean_selector: 36 | dependency: transitive 37 | description: 38 | name: boolean_selector 39 | sha256: "6cfb5af12253eaf2b368f07bacc5a80d1301a071c73360d746b7f2e32d762c66" 40 | url: "https://pub.dev" 41 | source: hosted 42 | version: "2.1.1" 43 | characters: 44 | dependency: transitive 45 | description: 46 | name: characters 47 | sha256: "04a925763edad70e8443c99234dc3328f442e811f1d8fd1a72f1c8ad0f69a605" 48 | url: "https://pub.dev" 49 | source: hosted 50 | version: "1.3.0" 51 | clock: 52 | dependency: transitive 53 | description: 54 | name: clock 55 | sha256: cb6d7f03e1de671e34607e909a7213e31d7752be4fb66a86d29fe1eb14bfb5cf 56 | url: "https://pub.dev" 57 | source: hosted 58 | version: "1.1.1" 59 | collection: 60 | dependency: transitive 61 | description: 62 | name: collection 63 | sha256: f092b211a4319e98e5ff58223576de6c2803db36221657b46c82574721240687 64 | url: "https://pub.dev" 65 | source: hosted 66 | version: "1.17.2" 67 | cupertino_icons: 68 | dependency: "direct main" 69 | description: 70 | name: cupertino_icons 71 | sha256: e35129dc44c9118cee2a5603506d823bab99c68393879edb440e0090d07586be 72 | url: "https://pub.dev" 73 | source: hosted 74 | version: "1.0.5" 75 | fake_async: 76 | dependency: transitive 77 | description: 78 | name: fake_async 79 | sha256: "511392330127add0b769b75a987850d136345d9227c6b94c96a04cf4a391bf78" 80 | url: "https://pub.dev" 81 | source: hosted 82 | version: "1.3.1" 83 | file: 84 | dependency: transitive 85 | description: 86 | name: file 87 | sha256: "1b92bec4fc2a72f59a8e15af5f52cd441e4a7860b49499d69dfa817af20e925d" 88 | url: "https://pub.dev" 89 | source: hosted 90 | version: "6.1.4" 91 | flutter: 92 | dependency: "direct main" 93 | description: flutter 94 | source: sdk 95 | version: "0.0.0" 96 | flutter_driver: 97 | dependency: "direct dev" 98 | description: flutter 99 | source: sdk 100 | version: "0.0.0" 101 | flutter_lints: 102 | dependency: "direct dev" 103 | description: 104 | name: flutter_lints 105 | sha256: aeb0b80a8b3709709c9cc496cdc027c5b3216796bc0af0ce1007eaf24464fd4c 106 | url: "https://pub.dev" 107 | source: hosted 108 | version: "2.0.1" 109 | flutter_test: 110 | dependency: "direct dev" 111 | description: flutter 112 | source: sdk 113 | version: "0.0.0" 114 | fuchsia_remote_debug_protocol: 115 | dependency: transitive 116 | description: flutter 117 | source: sdk 118 | version: "0.0.0" 119 | integration_test: 120 | dependency: "direct dev" 121 | description: flutter 122 | source: sdk 123 | version: "0.0.0" 124 | lints: 125 | dependency: transitive 126 | description: 127 | name: lints 128 | sha256: "5cfd6509652ff5e7fe149b6df4859e687fca9048437857cb2e65c8d780f396e3" 129 | url: "https://pub.dev" 130 | source: hosted 131 | version: "2.0.0" 132 | matcher: 133 | dependency: transitive 134 | description: 135 | name: matcher 136 | sha256: "1803e76e6653768d64ed8ff2e1e67bea3ad4b923eb5c56a295c3e634bad5960e" 137 | url: "https://pub.dev" 138 | source: hosted 139 | version: "0.12.16" 140 | material_color_utilities: 141 | dependency: transitive 142 | description: 143 | name: material_color_utilities 144 | sha256: "9528f2f296073ff54cb9fee677df673ace1218163c3bc7628093e7eed5203d41" 145 | url: "https://pub.dev" 146 | source: hosted 147 | version: "0.5.0" 148 | meta: 149 | dependency: transitive 150 | description: 151 | name: meta 152 | sha256: "3c74dbf8763d36539f114c799d8a2d87343b5067e9d796ca22b5eb8437090ee3" 153 | url: "https://pub.dev" 154 | source: hosted 155 | version: "1.9.1" 156 | path: 157 | dependency: transitive 158 | description: 159 | name: path 160 | sha256: "8829d8a55c13fc0e37127c29fedf290c102f4e40ae94ada574091fe0ff96c917" 161 | url: "https://pub.dev" 162 | source: hosted 163 | version: "1.8.3" 164 | platform: 165 | dependency: transitive 166 | description: 167 | name: platform 168 | sha256: "4a451831508d7d6ca779f7ac6e212b4023dd5a7d08a27a63da33756410e32b76" 169 | url: "https://pub.dev" 170 | source: hosted 171 | version: "3.1.0" 172 | plugin_platform_interface: 173 | dependency: transitive 174 | description: 175 | name: plugin_platform_interface 176 | sha256: f4f88d4a900933e7267e2b353594774fc0d07fb072b47eedcd5b54e1ea3269f8 177 | url: "https://pub.dev" 178 | source: hosted 179 | version: "2.1.7" 180 | process: 181 | dependency: transitive 182 | description: 183 | name: process 184 | sha256: "53fd8db9cec1d37b0574e12f07520d582019cb6c44abf5479a01505099a34a09" 185 | url: "https://pub.dev" 186 | source: hosted 187 | version: "4.2.4" 188 | sky_engine: 189 | dependency: transitive 190 | description: flutter 191 | source: sdk 192 | version: "0.0.99" 193 | source_span: 194 | dependency: transitive 195 | description: 196 | name: source_span 197 | sha256: "53e943d4206a5e30df338fd4c6e7a077e02254531b138a15aec3bd143c1a8b3c" 198 | url: "https://pub.dev" 199 | source: hosted 200 | version: "1.10.0" 201 | stack_trace: 202 | dependency: transitive 203 | description: 204 | name: stack_trace 205 | sha256: c3c7d8edb15bee7f0f74debd4b9c5f3c2ea86766fe4178eb2a18eb30a0bdaed5 206 | url: "https://pub.dev" 207 | source: hosted 208 | version: "1.11.0" 209 | stream_channel: 210 | dependency: transitive 211 | description: 212 | name: stream_channel 213 | sha256: "83615bee9045c1d322bbbd1ba209b7a749c2cbcdcb3fdd1df8eb488b3279c1c8" 214 | url: "https://pub.dev" 215 | source: hosted 216 | version: "2.1.1" 217 | string_scanner: 218 | dependency: transitive 219 | description: 220 | name: string_scanner 221 | sha256: "556692adab6cfa87322a115640c11f13cb77b3f076ddcc5d6ae3c20242bedcde" 222 | url: "https://pub.dev" 223 | source: hosted 224 | version: "1.2.0" 225 | sync_http: 226 | dependency: transitive 227 | description: 228 | name: sync_http 229 | sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" 230 | url: "https://pub.dev" 231 | source: hosted 232 | version: "0.3.1" 233 | term_glyph: 234 | dependency: transitive 235 | description: 236 | name: term_glyph 237 | sha256: a29248a84fbb7c79282b40b8c72a1209db169a2e0542bce341da992fe1bc7e84 238 | url: "https://pub.dev" 239 | source: hosted 240 | version: "1.2.1" 241 | test_api: 242 | dependency: transitive 243 | description: 244 | name: test_api 245 | sha256: "75760ffd7786fffdfb9597c35c5b27eaeec82be8edfb6d71d32651128ed7aab8" 246 | url: "https://pub.dev" 247 | source: hosted 248 | version: "0.6.0" 249 | vector_math: 250 | dependency: transitive 251 | description: 252 | name: vector_math 253 | sha256: "80b3257d1492ce4d091729e3a67a60407d227c27241d6927be0130c98e741803" 254 | url: "https://pub.dev" 255 | source: hosted 256 | version: "2.1.4" 257 | vm_service: 258 | dependency: transitive 259 | description: 260 | name: vm_service 261 | sha256: c620a6f783fa22436da68e42db7ebbf18b8c44b9a46ab911f666ff09ffd9153f 262 | url: "https://pub.dev" 263 | source: hosted 264 | version: "11.7.1" 265 | web: 266 | dependency: transitive 267 | description: 268 | name: web 269 | sha256: dc8ccd225a2005c1be616fe02951e2e342092edf968cf0844220383757ef8f10 270 | url: "https://pub.dev" 271 | source: hosted 272 | version: "0.1.4-beta" 273 | webdriver: 274 | dependency: transitive 275 | description: 276 | name: webdriver 277 | sha256: "3c923e918918feeb90c4c9fdf1fe39220fa4c0e8e2c0fffaded174498ef86c49" 278 | url: "https://pub.dev" 279 | source: hosted 280 | version: "3.0.2" 281 | sdks: 282 | dart: ">=3.1.0-185.0.dev <4.0.0" 283 | flutter: ">=2.5.0" 284 | -------------------------------------------------------------------------------- /app_widget/example/lib/main.dart: -------------------------------------------------------------------------------- 1 | import 'dart:convert'; 2 | import 'dart:math'; 3 | 4 | import 'package:flutter/material.dart'; 5 | 6 | import 'package:app_widget/app_widget.dart'; 7 | 8 | void onClickWidget(String? payload) { 9 | // print('onClick Widget: $payload'); 10 | } 11 | 12 | void main() { 13 | WidgetsFlutterBinding.ensureInitialized(); 14 | 15 | runApp(const MyApp()); 16 | } 17 | 18 | class MyApp extends StatefulWidget { 19 | const MyApp({Key? key}) : super(key: key); 20 | 21 | @override 22 | State createState() => _MyAppState(); 23 | } 24 | 25 | class _MyAppState extends State { 26 | late final AppWidgetPlugin _appWidgetPlugin; 27 | late final TextEditingController _widgetIdcontroller; 28 | late final TextEditingController _layoutIdcontroller; 29 | late final TextEditingController _layoutNamecontroller; 30 | int? _widgetId; 31 | int? _layoutId; 32 | // ignore: unused_field 33 | String? _layoutName; 34 | 35 | @override 36 | void initState() { 37 | super.initState(); 38 | _appWidgetPlugin = AppWidgetPlugin( 39 | // androidPackageName: 'tech.noxasch.app_widget_example', 40 | onConfigureWidget: onConfigureWidget, 41 | onClickWidget: onClickWidget, 42 | ); 43 | _widgetIdcontroller = TextEditingController(); 44 | _layoutIdcontroller = TextEditingController(); 45 | _layoutNamecontroller = TextEditingController(); 46 | } 47 | 48 | @override 49 | void dispose() { 50 | _widgetIdcontroller.dispose(); 51 | _layoutIdcontroller.dispose(); 52 | _layoutNamecontroller.dispose(); 53 | super.dispose(); 54 | } 55 | 56 | void onConfigureWidget(int widgetId, int layoutId, String layoutName) { 57 | setState(() { 58 | _widgetId = widgetId; 59 | _layoutId = layoutId; 60 | _layoutName = layoutName; 61 | }); 62 | _widgetIdcontroller.text = widgetId.toString(); 63 | _layoutIdcontroller.text = layoutId.toString(); 64 | _layoutNamecontroller.text = layoutName.toString(); 65 | // do something 66 | } 67 | 68 | @override 69 | Widget build(BuildContext context) { 70 | return MaterialApp( 71 | home: Scaffold( 72 | appBar: AppBar( 73 | title: const Text('AppWidgetPlugin example app'), 74 | ), 75 | body: Center( 76 | child: SingleChildScrollView( 77 | child: Column( 78 | mainAxisAlignment: MainAxisAlignment.center, 79 | children: [ 80 | Padding( 81 | padding: const EdgeInsets.all(30.0), 82 | child: TextField( 83 | decoration: const InputDecoration(label: Text('Widget Id')), 84 | controller: _widgetIdcontroller, 85 | keyboardType: TextInputType.number, 86 | ), 87 | ), 88 | const SizedBox( 89 | height: 10, 90 | ), 91 | Padding( 92 | padding: const EdgeInsets.all(30.0), 93 | child: TextField( 94 | decoration: const InputDecoration(label: Text('Layout Id')), 95 | controller: _layoutIdcontroller, 96 | readOnly: true, 97 | ), 98 | ), 99 | const SizedBox( 100 | height: 10, 101 | ), 102 | Padding( 103 | padding: const EdgeInsets.all(30.0), 104 | child: TextField( 105 | decoration: 106 | const InputDecoration(label: Text('Layout Name')), 107 | controller: _layoutNamecontroller, 108 | readOnly: true, 109 | ), 110 | ), 111 | const SizedBox( 112 | height: 10, 113 | ), 114 | ConfigureButton( 115 | widgetId: _widgetId, 116 | layoutId: _layoutId, 117 | appWidgetPlugin: _appWidgetPlugin), 118 | const SizedBox( 119 | height: 10, 120 | ), 121 | WidgetExistButton( 122 | controller: _widgetIdcontroller, 123 | appWidgetPlugin: _appWidgetPlugin, 124 | ), 125 | const SizedBox( 126 | height: 10, 127 | ), 128 | ReloadWidgetButton(appWidgetPlugin: _appWidgetPlugin), 129 | const SizedBox( 130 | height: 10, 131 | ), 132 | UpdateWidgetButton( 133 | controller: _widgetIdcontroller, 134 | appWidgetPlugin: _appWidgetPlugin), 135 | const SizedBox( 136 | height: 10, 137 | ), 138 | GetWidgetIdsButton(appWidgetPlugin: _appWidgetPlugin), 139 | ], 140 | ), 141 | ), 142 | ), 143 | ), 144 | ); 145 | } 146 | } 147 | 148 | class UpdateWidgetButton extends StatelessWidget { 149 | const UpdateWidgetButton({ 150 | Key? key, 151 | required TextEditingController controller, 152 | required AppWidgetPlugin appWidgetPlugin, 153 | }) : _controller = controller, 154 | _appWidgetPlugin = appWidgetPlugin, 155 | super(key: key); 156 | 157 | final TextEditingController _controller; 158 | final AppWidgetPlugin _appWidgetPlugin; 159 | 160 | @override 161 | Widget build(BuildContext context) { 162 | return ElevatedButton( 163 | onPressed: () async { 164 | if (_controller.text.isNotEmpty) { 165 | // this means the app is started by the widget config event 166 | final widgetId = int.parse(_controller.text); 167 | 168 | // send configure 169 | await _appWidgetPlugin.updateWidget( 170 | widgetId: widgetId, 171 | widgetLayout: 'example_layout', 172 | textViews: { 173 | 'widget_title': 'App Widget', 174 | 'widget_message': 'Updated in flutter' 175 | }, 176 | payload: jsonEncode({'number': Random.secure().nextInt(10)}), 177 | ); 178 | } 179 | }, 180 | child: const Text('Update Widget'), 181 | ); 182 | } 183 | } 184 | 185 | class GetWidgetIdsButton extends StatelessWidget { 186 | const GetWidgetIdsButton({ 187 | Key? key, 188 | required AppWidgetPlugin appWidgetPlugin, 189 | }) : _appWidgetPlugin = appWidgetPlugin, 190 | super(key: key); 191 | 192 | final AppWidgetPlugin _appWidgetPlugin; 193 | 194 | @override 195 | Widget build(BuildContext context) { 196 | return ElevatedButton( 197 | onPressed: () async { 198 | final messenger = ScaffoldMessenger.of(context); 199 | final widgetIds = await _appWidgetPlugin.getWidgetIds( 200 | androidProviderName: 'AppWidgetExampleProvider'); 201 | messenger.showSnackBar( 202 | SnackBar( 203 | content: Text('widget ids: ${widgetIds ?? ""}'), 204 | ), 205 | ); 206 | }, 207 | child: const Text('Get WidgetIds'), 208 | ); 209 | } 210 | } 211 | 212 | class ReloadWidgetButton extends StatelessWidget { 213 | const ReloadWidgetButton({ 214 | Key? key, 215 | required AppWidgetPlugin appWidgetPlugin, 216 | }) : _appWidgetPlugin = appWidgetPlugin, 217 | super(key: key); 218 | 219 | final AppWidgetPlugin _appWidgetPlugin; 220 | 221 | @override 222 | Widget build(BuildContext context) { 223 | return ElevatedButton( 224 | onPressed: () async { 225 | final messenger = ScaffoldMessenger.of(context); 226 | await _appWidgetPlugin.reloadWidgets( 227 | androidProviderName: 'AppWidgetExampleProvider', 228 | ); 229 | messenger.showSnackBar( 230 | const SnackBar( 231 | content: 232 | Text('Reload broadcast has been sent check Android debug log'), 233 | ), 234 | ); 235 | }, 236 | child: const Text('Reload Widgets'), 237 | ); 238 | } 239 | } 240 | 241 | class WidgetExistButton extends StatelessWidget { 242 | const WidgetExistButton({ 243 | Key? key, 244 | required TextEditingController controller, 245 | required AppWidgetPlugin appWidgetPlugin, 246 | }) : _controller = controller, 247 | _appWidgetPlugin = appWidgetPlugin, 248 | super(key: key); 249 | 250 | final TextEditingController _controller; 251 | final AppWidgetPlugin _appWidgetPlugin; 252 | 253 | @override 254 | Widget build(BuildContext context) { 255 | return ElevatedButton( 256 | onPressed: () async { 257 | if (_controller.text.isNotEmpty) { 258 | final messenger = ScaffoldMessenger.of(context); 259 | 260 | final widgetId = int.parse(_controller.text); 261 | final exist = (await _appWidgetPlugin.widgetExist(widgetId))!; 262 | if (exist) { 263 | messenger.showSnackBar( 264 | const SnackBar( 265 | content: Text('This widget exist.'), 266 | ), 267 | ); 268 | } else { 269 | messenger.showSnackBar( 270 | SnackBar( 271 | content: Text('Widget with id $widgetId does not exist.'), 272 | ), 273 | ); 274 | } 275 | } 276 | }, 277 | child: const Text('check if Widget Exist')); 278 | } 279 | } 280 | 281 | class ConfigureButton extends StatelessWidget { 282 | const ConfigureButton({ 283 | Key? key, 284 | required int? widgetId, 285 | required int? layoutId, 286 | required AppWidgetPlugin appWidgetPlugin, 287 | }) : _widgetId = widgetId, 288 | _layoutId = layoutId, 289 | _appWidgetPlugin = appWidgetPlugin, 290 | super(key: key); 291 | 292 | final int? _widgetId; 293 | final int? _layoutId; 294 | final AppWidgetPlugin _appWidgetPlugin; 295 | 296 | @override 297 | Widget build(BuildContext context) { 298 | return ElevatedButton( 299 | onPressed: () async { 300 | if (_widgetId != null) { 301 | // this means the app is started by the widget config event 302 | final messenger = ScaffoldMessenger.of(context); 303 | 304 | // send configure 305 | await _appWidgetPlugin.configureWidget( 306 | widgetId: _widgetId!, 307 | layoutId: _layoutId!, 308 | textViews: { 309 | 'widget_title': 'App Widget', 310 | 'widget_message': 'Configured in flutter' 311 | }, 312 | payload: jsonEncode({'number': Random().nextInt(10)}), 313 | ); 314 | messenger.showSnackBar( 315 | const SnackBar(content: Text('Widget has been configured!'))); 316 | } else { 317 | ScaffoldMessenger.of(context).showSnackBar(const SnackBar( 318 | content: 319 | Text('Opps, no widget id from WIDGET_CONFIGURE event'))); 320 | } 321 | }, 322 | child: const Text('Configure Widget')); 323 | } 324 | } 325 | -------------------------------------------------------------------------------- /app_widget_android/android/src/main/kotlin/tech/noxasch/app_widget/AppWidgetMethodCallHandler.kt: -------------------------------------------------------------------------------- 1 | package tech.noxasch.app_widget 2 | 3 | import android.app.Activity 4 | import android.app.PendingIntent 5 | import android.appwidget.AppWidgetManager 6 | import android.content.ComponentName 7 | import android.content.Context 8 | import android.content.Intent 9 | import android.net.Uri 10 | import android.os.Build 11 | import android.widget.RemoteViews 12 | import androidx.annotation.NonNull 13 | import io.flutter.plugin.common.BinaryMessenger 14 | import io.flutter.plugin.common.MethodCall 15 | import io.flutter.plugin.common.MethodChannel 16 | 17 | class AppWidgetMethodCallHandler(private val context: Context, ) 18 | : MethodChannel.MethodCallHandler { 19 | 20 | private var channel: MethodChannel? = null 21 | private var activity: Activity? = null 22 | 23 | fun open(binaryMessenger: BinaryMessenger) { 24 | channel = MethodChannel(binaryMessenger, AppWidgetPlugin.CHANNEL) 25 | channel!!.setMethodCallHandler(this) 26 | } 27 | 28 | fun setActivity(_activity: Activity?) { 29 | activity = _activity 30 | } 31 | 32 | fun close() { 33 | if (channel == null) return 34 | 35 | channel!!.setMethodCallHandler(null) 36 | channel = null 37 | activity = null 38 | } 39 | 40 | fun handleConfigureIntent(intent: Intent): Boolean { 41 | val widgetId = intent.extras!!.getInt("widgetId") 42 | val layoutId = intent.extras!!.getInt("layoutId") 43 | val layoutName = intent.extras!!.getString("layoutName") 44 | channel!!.invokeMethod(AppWidgetPlugin.ON_CONFIGURE_WIDGET_CALLBACK, 45 | mapOf( 46 | "widgetId" to widgetId, 47 | "layoutId" to layoutId, 48 | "layoutName" to layoutName 49 | ) 50 | ) 51 | return true 52 | } 53 | 54 | 55 | fun handleClickIntent(intent: Intent): Boolean { 56 | val payload = intent.extras?.getString(AppWidgetPlugin.EXTRA_PAYLOAD) 57 | 58 | channel!!.invokeMethod(AppWidgetPlugin.ON_ClICK_WIDGET_CALLBACK, mapOf( 59 | "payload" to payload 60 | )) 61 | return true 62 | } 63 | 64 | override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) { 65 | when (call.method) { 66 | "cancelConfigureWidget" -> cancelConfigureWidget(result) 67 | "configureWidget" -> configureWidget(call, result) 68 | "getWidgetIds" -> getWidgetIds(call, result) 69 | "reloadWidgets" -> reloadWidgets(call, result) 70 | "updateWidget" -> updateWidget(call, result) 71 | "widgetExist" -> widgetExist(call, result) 72 | else -> { 73 | result.notImplemented() 74 | } 75 | } 76 | } 77 | 78 | private fun getWidgetIds(call: MethodCall, result: MethodChannel.Result) { 79 | val androidPackageName = call.argument("androidPackageName") 80 | ?: context.packageName 81 | val widgetProviderName = call.argument("androidProviderName") ?: return result.error( 82 | "-1", 83 | "widgetProviderName is required!", 84 | null 85 | ) 86 | 87 | return try { 88 | val widgetProviderClass = Class.forName("$androidPackageName.$widgetProviderName") 89 | val widgetProvider = ComponentName(context, widgetProviderClass) 90 | val widgetManager = AppWidgetManager.getInstance(context) 91 | val widgetIds = widgetManager.getAppWidgetIds(widgetProvider) 92 | 93 | result.success(widgetIds) 94 | } catch (exception: Exception) { 95 | result.error("-2", exception.message, exception) 96 | } 97 | } 98 | 99 | private fun cancelConfigureWidget(result: MethodChannel.Result) { 100 | return try { 101 | activity!!.setResult(Activity.RESULT_CANCELED) 102 | result.success(true) 103 | } catch (exception: Exception) { 104 | result.error("-2", exception.message, exception) 105 | } 106 | } 107 | 108 | 109 | 110 | /// This should be called when configuring individual widgets 111 | private fun configureWidget(call: MethodCall, result: MethodChannel.Result) { 112 | return try { 113 | if (activity == null) return result.error("-2", "Not attached to any activity!", null) 114 | 115 | val androidPackageName = call.argument("androidPackageName") 116 | ?: context.packageName 117 | val widgetId = call.argument("widgetId") 118 | ?: return result.error("-1", "widgetId is required!", null) 119 | val layoutId = call.argument("layoutId") 120 | ?: return result.error("-1", "layoutId is required!", null) 121 | val payload = call.argument("payload") 122 | val url = call.argument("url") 123 | val activityClass = Class.forName("${context.packageName}.MainActivity") 124 | val appWidgetManager = AppWidgetManager.getInstance(context) 125 | val pendingIntent = createPendingClickIntent(activityClass, widgetId, payload, url) 126 | val textViewsMap = call.argument>("textViews") 127 | 128 | if (textViewsMap != null) { 129 | val views : RemoteViews = RemoteViews(context.packageName, layoutId).apply { 130 | for ((key, value) in textViewsMap) { 131 | val textViewId: Int = 132 | context.resources.getIdentifier(key, "id", context.packageName) 133 | if (textViewId == 0) throw Exception("Id $key does not exist!") 134 | setTextViewText(textViewId, value) 135 | setOnClickPendingIntent(textViewId, pendingIntent) 136 | } 137 | } 138 | appWidgetManager.updateAppWidget(widgetId, views) 139 | } 140 | 141 | // This is important to confirm the widget 142 | // otherwise it's considered cancelled and widget will be removed 143 | val resultValue = Intent().putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) 144 | activity!!.setResult(Activity.RESULT_OK, resultValue) 145 | activity!!.finish() 146 | result.success(true) 147 | } catch (exception: Exception) { 148 | result.error("-2", exception.message, exception) 149 | } 150 | } 151 | 152 | private fun widgetExist(call: MethodCall, result: MethodChannel.Result) { 153 | val widgetId = call.argument("widgetId") ?: return result.success(false) 154 | return try { 155 | val widgetManager = AppWidgetManager.getInstance(context) 156 | widgetManager.getAppWidgetInfo(widgetId) ?: return result.success(false) 157 | 158 | result.success(true) 159 | } catch (exception: Exception) { 160 | result.error("-2", exception.message, exception) 161 | } 162 | } 163 | 164 | // This should only be called after the widget has been configure for the first time 165 | private fun updateWidget(call: MethodCall, result: MethodChannel.Result) { 166 | return try { 167 | val androidPackageName = call.argument("androidPackageName") 168 | ?: context.packageName 169 | val widgetId = call.argument("widgetId") 170 | ?: return result.error("-1", "widgetId is required!", null) 171 | val layoutId = call.argument("layoutId") 172 | ?: return result.error("-1", "layoutId is required!", null) 173 | 174 | val payload = call.argument("payload") 175 | val url = call.argument("url") 176 | val activityClass = Class.forName("${context.packageName}.MainActivity") 177 | val appWidgetManager = AppWidgetManager.getInstance(context) 178 | val pendingIntent = createPendingClickIntent(activityClass, widgetId, payload, url) 179 | val textViewsMap = call.argument>("textViews") 180 | 181 | if (textViewsMap != null) { 182 | val views = RemoteViews(context.packageName, layoutId) 183 | 184 | for ((key, value) in textViewsMap) { 185 | val textViewId: Int = 186 | context.resources.getIdentifier(key, "id", context.packageName) 187 | if (textViewId == 0) throw Exception("Id $key does not exist!") 188 | 189 | // only work if widget is blank - so we have to clear it first 190 | views.setTextViewText(textViewId, "") 191 | views.setTextViewText(textViewId, value) 192 | views.setOnClickPendingIntent(textViewId, pendingIntent) 193 | } 194 | appWidgetManager.partiallyUpdateAppWidget(widgetId, views) 195 | } 196 | 197 | result.success(true) 198 | } catch (exception: Exception) { 199 | result.error("-2", exception.message, exception) 200 | } 201 | } 202 | 203 | /// Create click intent on a widget 204 | /// 205 | /// when clicked the intent will received by the broadcast AppWidgetBroadcastReceiver 206 | /// the receiver will expose the click event to dart callback 207 | /// 208 | /// by default will use widgetId as requestCode to make sure the intent doesn't replace existing 209 | /// widget intent. 210 | /// The callback will return widgetId, itemId (if supplied) and stringUid (if supplied) 211 | /// This parameters can be use on app side to easily fetch the data from database or API 212 | /// without storing in sharedPrefs. 213 | /// 214 | /// 215 | private fun createPendingClickIntent( 216 | activityClass: Class<*>, 217 | widgetId: Int, 218 | payload: String?, 219 | url: String? 220 | ): PendingIntent { 221 | val clickIntent = Intent(context, activityClass) 222 | clickIntent.action = AppWidgetPlugin.CLICK_WIDGET_ACTION 223 | clickIntent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK) 224 | clickIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, widgetId) 225 | clickIntent.putExtra(AppWidgetPlugin.EXTRA_PAYLOAD, payload) 226 | if (url != null) clickIntent.data = (Uri.parse(url)) 227 | 228 | var pendingIntentFlag = PendingIntent.FLAG_UPDATE_CURRENT 229 | if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { 230 | pendingIntentFlag = pendingIntentFlag or PendingIntent.FLAG_IMMUTABLE 231 | } 232 | 233 | return PendingIntent.getActivity(context, widgetId, clickIntent, pendingIntentFlag) 234 | } 235 | 236 | /// force reload the widget and this will trigger onUpdate in broadcast receiver 237 | private fun reloadWidgets(call: MethodCall, result: MethodChannel.Result) { 238 | val androidPackageName = call.argument("androidPackageName") 239 | ?: context.packageName 240 | val widgetProviderName = call.argument("androidProviderName") 241 | ?: return result.error( 242 | "-1", 243 | "widgetProviderName is required!", 244 | null 245 | ) 246 | 247 | return try { 248 | val widgetClass = Class.forName("$androidPackageName.$widgetProviderName") 249 | val widgetProvider = ComponentName(context, widgetClass) 250 | val widgetManager = AppWidgetManager.getInstance(context) 251 | val widgetIds = widgetManager.getAppWidgetIds(widgetProvider) 252 | 253 | val reloadIntent = Intent() 254 | reloadIntent.action = AppWidgetManager.ACTION_APPWIDGET_UPDATE 255 | reloadIntent.putExtra(AppWidgetManager.EXTRA_APPWIDGET_IDS, widgetIds) 256 | context.sendBroadcast(reloadIntent) 257 | result.success(true) 258 | } catch (exception: Exception) { 259 | result.error("-2", exception.message, exception) 260 | } 261 | } 262 | 263 | } 264 | -------------------------------------------------------------------------------- /app_widget/README.md: -------------------------------------------------------------------------------- 1 | # App Widget 2 | 3 | [![platform-interface](https://github.com/noxasch/flutter_app_widget/actions/workflows/interface.yaml/badge.svg)](https://github.com/noxasch/flutter_app_widget/actions/workflows/interface.yaml) 4 | [![android](https://github.com/noxasch/flutter_app_widget/actions/workflows/android.yaml/badge.svg)](https://github.com/noxasch/flutter_app_widget/actions/workflows/android.yaml) 5 | [![build](https://github.com/noxasch/flutter_app_widget/actions/workflows/main.yaml/badge.svg)](https://github.com/noxasch/flutter_app_widget/actions/workflows/main.yaml) 6 | 7 | This plugin attempt to exposed as much useful API and callback to flutter to reduce 8 | going back and forth to native and make building app widget / home screen widget easier 9 | and can be manage fully from flutter side keeping app codebase logic in flutter. 10 | 11 | 12 | | | | 13 | | --- | --- | 14 | |![screen_shot](assets/screen_shot.webp) | ![gif](assets/example_app.gif) | 15 | 16 | ## Note 17 | - Please see the changelogs for breaking changes 18 | - Every minor version update might introduce a breaking changes as this plugin is still considered alpha 19 | 20 | ## Caveats 21 | 22 | Configuring or opening a screen from the widget is slower (unless the app is still active in the background) 23 | compare to native because we need to wait for flutter engine to start. Hence as you can see from the gif there 24 | is some delay and without the launch screen we can notice this delay. Howver on Android, most likely your 25 | app start time will going to improve over time except during the first time user open it after an update. 26 | So this shouldn't be an issue. Although we can notice significant delay in old phone and in debug mode. 27 | 28 | ## Plaform Support 29 | 30 | As of current state I have no capacity to support for iOS, but help is welcome. 31 | 32 | | Android | iOS | 33 | | :-----: | :-: | 34 | | ✔️ | | 35 | 36 | ## Using this package 37 | 38 | ### Widget Storage and Caching 39 | This plugin doesn't dictate on how to handle widget update/storage/caching. 40 | It simply provide api to manage the widget from flutter. 41 | 42 | ### Table Of Content 43 | 44 | Platform Setup 45 | - [Android]() 46 | - [Android Setup](#android-setup) 47 | - [Plugin Api Usage and callbaks](#in-app-usage-and-dartflutter-api) 48 | - [Update using flutter workmanager](#handling-widget-update-using-in-flutter-workmanger) 49 | - [Update widget using widget provider in kotlin](#handling-widget-update-using-appwidgetprovider-in-kotlin) 50 | 51 | ### Platform setup 52 | 53 | #### Android 54 | 55 | > Note: It is advisable to do this setup using Android Studio since it help you design the widget layout and proper linting and import in kotlin file. 56 | 57 | There are multiple ways you can update the widget on Android: 58 | 1. Using AppWidgetProvider / BroadcastReceiver 59 | 2. Using workmanager 60 | 3. Using alarm manager 61 | 4. Using android service 62 | 63 | Which by you can handle this in flutter using: 64 | 1. [Flutter Workmanager](https://pub.dev/packages/workmanager) 65 | 2. [android_alarm_manager_plus](https://pub.dev/packages/android_alarm_manager_plus) 66 | 3. Natively using `AppWidgetProvider` 67 | 68 | ##### Android Flavor support 69 | 70 | simply include main android package name use by the MainActivity without flavor prefix. 71 | Otherwise just omit the packageName as it will use your default package name. 72 | 73 | ```dart 74 | final appWidgetPlugin = AppWidgetPlugin( 75 | androidPackageName: 'tech.noxasch.app_widget_example', 76 | ); 77 | ``` 78 | 79 | ##### Android Setup 80 | 81 | 82 | 1. Add widget layout in `android/app/src/main/res/layout/example_layout.xml` 83 | 84 | ```xml 85 | 86 | 87 | 97 | 98 | 105 | 106 | 112 | 113 | ``` 114 | 115 | 2. Add `appwidget-provider` info `android/app/src/main/res/xml/my-widget-provider-info` 116 | 117 | ```xml 118 | 119 | 129 | 130 | 135 | ``` 136 | 137 | 3. Update Android manifest `android/app/src/main/AndroidManifest` 138 | 139 | - add intent-filter to the `MainActivity` activity block if you want to support widget initial configuration 140 | 141 | ```xml 142 | 145 | ... 146 | 147 | 148 | 149 | 150 | 151 | ``` 152 | 153 | - add receiver for widget provider to listen to widget event (after Activity block) 154 | 155 | ```xml 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 165 | 166 | ``` 167 | 4. Create the widget provider in `android/app/src/main/kotlin/your/domain/path/MyWidgetExampleProvider.kt` 168 | Inherit from Android `AppWidgetProvider` and implement the required method if needed. Since the plugin already provide interface to update widget, we can leave it empty and handle it on dart/flutter side. 169 | 170 | Probably you want to implement `onDeleted` or `onDisabled` method to handle cleanup like removing the widget Id from sharedPrefences allow user to add multiple widget. 171 | 172 | ```kotlin 173 | package com.example.my_app 174 | 175 | class MyWidgetExampleProvider : AppWidgetProvider() 176 | ``` 177 | 178 | 5. Update MainActivity to handle `onConfigure` intent 179 | 180 | ```kotlin 181 | package com.example.my_app 182 | 183 | import android.appwidget.AppWidgetManager 184 | import io.flutter.embedding.android.FlutterActivity 185 | import io.flutter.embedding.engine.FlutterEngine 186 | import tech.noxasch.app_widget.AppWidgetPlugin 187 | 188 | class MainActivity: FlutterActivity() { 189 | override fun onFlutterUiDisplayed() { 190 | super.onFlutterUiDisplayed() 191 | 192 | AppWidgetPlugin.Companion.handleWidgetAction(context, intent) 193 | } 194 | } 195 | ``` 196 | 197 | 6. By now you should be able to add a widget. Next step is to configure it from flutter side 198 | and make sure the widget configured. 199 | 200 | 201 | ### In App Usage and Dart/Flutter Api 202 | 203 | This section shows how to use the exposed api by the plugin in your app. 204 | 205 | ```dart 206 | // instantiate appWidgetPlugin 207 | // recommended to include your app package name for android 208 | // this help to resolves flavored version of the app 209 | final appWidgetPlugin = AppWidgetPlugin( 210 | androidPackageName: 'tech.noxasch.app_widget_example', 211 | ); 212 | await appWidgetPlugin.configureWidget(...) 213 | ``` 214 | 215 | #### handling onConfigureWidget 216 | 217 | ```dart 218 | // this method can be declare as a top level function or inside a widget as a member function 219 | @pragma('vm:entry-point') 220 | void onConfigureWidget(int widgetId, int layoutId, String layoutName) async { 221 | // handle widget configuration 222 | // eg: 223 | // redirect or use launchUrl and deeplink redirect to configuration page 224 | // store widgetId, layoutId and layoutName in sharedPref 225 | // use layoutName to build proper payload 226 | 227 | // layoutName: tech.noxasch.app_widget_example:layout/example_layout 228 | } 229 | 230 | // onConfigureWidget callback are optional 231 | // without this it will use default value that you set 232 | final appWidgetPlugin = AppWidgetPlugin( 233 | onConfigureWidget: onConfigureWidget 234 | ); 235 | 236 | // this changes will reflect on the widget 237 | // only use this method in widget configuration screen as 238 | // it method will close the app which require to signal the widget config completion 239 | await appWidgetPlugin.configureWidget( 240 | // change to androidPackageName - we needed as param since there is no standard on how long the domain name can be 241 | widgetId: _widgetId!, 242 | layoutId: _layoutId!, 243 | textViews: { 244 | 'widget_title': 'MY WIDGET', 245 | 'widget_message': 'This is my widget message' 246 | }, 247 | payload: '{"itemId": 1, "stringUid": "uid"}', 248 | url: 'deeplink or url' 249 | ); 250 | ``` 251 | #### Cancelling 252 | Call this method to properly cancel widget first time configuration 253 | 254 | ```dart 255 | await appWidgetPlugin.cancelConfigure() 256 | ``` 257 | 258 | #### handling onClickWidget 259 | 260 | ```dart 261 | // this method can be declare as a top level function or inside a widget 262 | void onClickWidget(String? payload) { 263 | // handle click widget event 264 | // eg: 265 | // redirect to item page 266 | // use launchUrl and deeplink redirect 267 | } 268 | 269 | // onClickWidget callback are optional 270 | final appWidgetPlugin = AppWidgetPlugin( 271 | onConfigureWidget: onConfigureWidget, 272 | onClickWidget: onClickWidget 273 | ); 274 | ``` 275 | 276 | #### updateWidget 277 | Make sure you store the `widgetId` and `layoutId` during widget configuration. 278 | 279 | Tips: Store `layoutName` to easily manage payload textViews for multiple layout 280 | 281 | Most of the time you'll want to update widget via workmanager. See [below](#handling-widget-update-using-in-flutter-workmanger) 282 | how to use the plugin in workmanager. 283 | 284 | ```dart 285 | await appWidgetPlugin.updateWidget( 286 | widgetId: _widgetId!, 287 | layoutId: _layoutId!, 288 | textViews: { 289 | 'widget_title': 'MY WIDGET', 290 | 'widget_message': 'This is my widget message' 291 | }, 292 | payload: '{"itemId": 1, "stringUid": "uid"}', 293 | url: 'deeplink or url' 294 | ); 295 | ``` 296 | 297 | #### reloadWidgets 298 | - use this method if you handle update in your widget provider want want to trigger force 299 | reload widgets from flutter 300 | - this will trigger `onUpdate` intent in your widget provider 301 | 302 | ```dart 303 | await appWidgetPlugin.reloadWidgets( 304 | androidProviderName: 'AppWidgetExampleProvider', 305 | }); 306 | ``` 307 | 308 | #### widgetExist 309 | - check if widget is exist 310 | - on android this utilize `appWidgetManager.getAppWidgetInfo` 311 | 312 | ```dart 313 | final widgetId = 12; 314 | 315 | if (await appWidgetPlugin.widgetExist(widgetId)) { 316 | // do something if widget exist 317 | } 318 | ``` 319 | 320 | ### getWidgetIds 321 | - return widgetIds which utilized `appWidgetManager.getAppWidgetIds` on android 322 | - might be unreliable. if you have a problem see this [issue](https://stackoverflow.com/questions/12462696/appwidgetmanager-getappwidgetids-returning-old-widget-ids) 323 | 324 | ```dart 325 | await appWidgetPlugin.getWidgetIds( 326 | androidProviderName: 'AppWidgetExampleProvider' 327 | ); 328 | ``` 329 | 330 | ## Handling Widget update using in Flutter Workmanger 331 | - there is a bug in android that cause your widget to flash and become blank. 332 | - To make sure this bug doesn't affect your widget udpate, you'll need to register 333 | another task that longer maybe than your update widget task, and then cancel it 334 | inside the callback. 335 | - to avoid this use workmanager periodicTask instead 336 | 337 | ```dart 338 | // Using workmanager chained OneOffTask 339 | @pragma('vm:entry-point') 340 | void onConfigureWidget(int widgetId, int layoutId) async { 341 | final sharedPrefs = await SharedPreferences.getInstance(); 342 | await sharedPrefs.setInt('widget_id', widgetId); 343 | await sharedPrefs.setInt('layout_id', layoutId); 344 | // register task druing configure event in onConfigure callback 345 | await Workmanager().registerOneOffTask( 346 | 'UpdateMyWidget', 347 | 'updateWidget', 348 | tag: 'WIDGET_PLUGIN', 349 | existingWorkPolicy: ExistingWorkPolicy.keep, 350 | initialDelay: const Duration(minutes: 5), 351 | ); 352 | // register a dummy task 353 | // dummy task is required to fix flickering bug 354 | // https://stackoverflow.com/questions/71603702/in-android-glance-widgets-are-flickering-during-every-update-even-if-there-i 355 | await Workmanager().registerOneOffTask( 356 | 'DUMMY_TASK', 357 | 'dummyTask', 358 | tag: 'DUMMY_TASKS', 359 | existingWorkPolicy: ExistingWorkPolicy.keep, 360 | initialDelay: const Duration(days: 365), 361 | ); 362 | } 363 | 364 | // Using workmanager PeriodicTask 365 | @pragma('vm:entry-point') 366 | void onConfigureWidget(int widgetId) async { 367 | final sharedPrefs = await SharedPreferences.getInstance(); 368 | await sharedPrefs.setInt('widget_id', widgetId); 369 | await sharedPrefs.setInt('layout_id', layoutId); 370 | // register task druing configure event in onConfigure callback 371 | await Workmanager().registerPeriodicTask( 372 | '$kUpdateWidgetTask-$widgetId', 373 | kUpdateWidgetTask, 374 | tag: kUpdateWidgetTag, 375 | frequency: kWidgetUpdateIntervalDuration, 376 | existingWorkPolicy: ExistingWorkPolicy.replace, 377 | backoffPolicy: BackoffPolicy.exponential, 378 | backoffPolicyDelay: const Duration( 379 | seconds: 10, 380 | ), 381 | initialDelay: const Duration(minutes: kWidgetUpdateIntervalInMinutes), 382 | inputData: { 383 | 'widgetId': widgetId, 384 | 'layoutId': layoutId, 385 | 'payload': payload, 386 | }, 387 | ); 388 | } 389 | 390 | // in callbackDipatcher or some other file 391 | final worksMapper = {'updateWidget': updateWidgetWorker}; 392 | 393 | @pragma('vm:entry-point') 394 | void callbackDipatcher() async { 395 | Workmanager().executeTask((taskName, inputData) async { 396 | try { 397 | if (taskName == 'updateWidget') { 398 | await updateWidgetWorker() 399 | } 400 | } catch (err) { 401 | return false; 402 | } 403 | return true; 404 | }); 405 | } 406 | 407 | 408 | @pragma('vm:entry-point') 409 | Future updateWidgetWorker() async { 410 | final sharedPrefs = await SharedPreferences.getInstance(); 411 | final appWidgetPlugin = AppWidgetPlugin( 412 | androidPackageName: 'tech.noxasch.app_widget_example', 413 | onConfigureWidget: onConfigureWidget, 414 | ); 415 | 416 | // Ssqlite database using drift to support multiple connection 417 | final connection = await _openConnection(); 418 | final db = AppDatabase.connect(connection); 419 | final repo = db.todosRepository; 420 | 421 | final widgetId = sharedPrefs.getInt('widget_id'); 422 | final layoutId = sharedPrefs.getInt('layout_id'); 423 | 424 | if (widgetId != null) { 425 | await appWidgetPlugin.updateWidget( 426 | widgetId: widgetId!, 427 | layoutId: layoutId, 428 | textViews: { 429 | 'widget_title': 'MY WIDGET', 430 | 'widget_message': 'This is my widget message' 431 | }); 432 | await Workmanager().cancelByUniqueName('DUMMY_TASK'); 433 | } 434 | } 435 | ``` 436 | 437 | ## Handling Widget update using AppWidgetProvider in Kotlin 438 | 439 | ```kotlin 440 | class AppWidgetExampleProvider : AppWidgetProvider() { 441 | override fun onUpdate( 442 | context: Context?, 443 | appWidgetManager: AppWidgetManager?, 444 | appWidgetIds: IntArray? 445 | ) { 446 | super.onUpdate(context, appWidgetManager, appWidgetIds) 447 | 448 | // check if widgetId store sharedPreferences 449 | // fetch data from sharedPreferences 450 | // then update 451 | for (widgetId in appWidgetIds!!) { 452 | val remoteViews = RemoteViews(context!!.packageName, R.layout.example_layout).apply() { 453 | setTextViewText(R.id.widget_title, "Widget Title") 454 | setTextViewText(R.id.widget_message, "This is my message") 455 | } 456 | 457 | appWidgetManager!!.partiallyUpdateAppWidget(widgetId, remoteViews) 458 | } 459 | } 460 | } 461 | ``` 462 | 463 | 464 | 473 | 474 | #### Testing 475 | You can test this plugin by mocking the required methodChannel directly and set 476 | debugDefaultTargetPlatformOverride to your preferred platform if needed. 477 | 478 | ```dart 479 | void main() { 480 | TestWidgetsFlutterBinding.ensureInitialized(); 481 | 482 | const MethodChannel channel = MethodChannel(AppWidgetPlatform.channel); 483 | final List log = []; 484 | 485 | 486 | TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger 487 | .setMockMethodCallHandler(channel, (methodCall) async { 488 | log.add(methodCall); 489 | switch (methodCall.method) { 490 | case 'getPlatformVersion': 491 | return '42'; 492 | case 'configureWidget': 493 | return true; 494 | case 'cancelConfigureWidget': 495 | return true; 496 | case 'getWidgetIds': 497 | return []; 498 | case 'reloadWidgets': 499 | return true; 500 | case 'widgetExist': 501 | return true; 502 | default: 503 | return null; 504 | } 505 | }); 506 | 507 | setUp(() { 508 | debugDefaultTargetPlatformOverride = TargetPlatform.android; 509 | }); 510 | 511 | tearDown(() { 512 | log.clear(); 513 | }); 514 | 515 | test('', () async { 516 | final appwidgetPlugin = AppWidgetPlugin(); 517 | 518 | expect(await appwidgetPlugin.configureWidget( 519 | ... 520 | ), isTrue); 521 | 522 | // testing if your method that call configureWidget sending the expected arguments - interface level only 523 | expect(log, [ 524 | isMethodCall( 525 | 'configureWidget', 526 | arguments: { 527 | 'androidPackageName': 'appname', // androidPackageName is included behind the scene 528 | 'widgetId': 1, 529 | 'layoutId': 1, 530 | 'textViews': {}, 531 | 'payload': '{"itemId": 1, "stringUid": "uid"}' 532 | }, 533 | ) 534 | ]); 535 | }); 536 | ``` 537 | 538 | #### References 539 | - [Android developers - App Widgets](https://developer.android.com/develop/ui/views/appwidgets) 540 | - [Android developers - Updating widget and creating widget preview](https://developer.android.com/develop/ui/views/appwidgets/advanced) 541 | 542 | ## Checklist 543 | - [x] Unit Test 544 | - [x] update documentation to cover api usage 545 | - [x] Test example 546 | - [x] Update example app 547 | - [x] Github Action Workflow (CI) 548 | - [x] Update Screenshot 549 | - [ ] iOS support 550 | --------------------------------------------------------------------------------