├── ios
├── Assets
│ └── .gitkeep
├── .gitignore
├── Classes
│ ├── internal
│ │ ├── BGTaskHandler.h
│ │ ├── utils.m
│ │ ├── BGTaskMgrDelegate.h
│ │ ├── utils.h
│ │ ├── BGTaskHandler.m
│ │ └── BGTaskMgrDelegate.m
│ ├── FltWorkerPlugin.h
│ └── FltWorkerPlugin.m
└── flt_worker.podspec
├── android
├── settings.gradle
├── .gitignore
├── gradle.properties
├── src
│ ├── main
│ │ ├── AndroidManifest.xml
│ │ └── java
│ │ │ └── dev
│ │ │ └── thinkng
│ │ │ └── flt_worker
│ │ │ ├── internal
│ │ │ ├── BackgroundWorker.java
│ │ │ ├── MethodCallFuture.java
│ │ │ ├── BackgroundWorkerPlugin.java
│ │ │ ├── WorkRequests.java
│ │ │ └── AbsWorkerPlugin.java
│ │ │ └── FltWorkerPlugin.java
│ └── test
│ │ └── java
│ │ └── dev
│ │ └── thinkng
│ │ └── flt_worker
│ │ └── internal
│ │ └── WorkRequestsTest.java
├── .idea
│ ├── codeStyles
│ │ ├── codeStyleConfig.xml
│ │ └── Project.xml
│ └── inspectionProfiles
│ │ └── Project_Default.xml
├── gradle
│ └── wrapper
│ │ └── gradle-wrapper.properties
└── build.gradle
├── analysis_options.yaml
├── example
├── ios
│ ├── Runner
│ │ ├── Runner-Bridging-Header.h
│ │ ├── AppDelegate.h
│ │ ├── Assets.xcassets
│ │ │ ├── LaunchImage.imageset
│ │ │ │ ├── LaunchImage.png
│ │ │ │ ├── LaunchImage@2x.png
│ │ │ │ ├── LaunchImage@3x.png
│ │ │ │ ├── README.md
│ │ │ │ └── Contents.json
│ │ │ └── AppIcon.appiconset
│ │ │ │ ├── Icon-App-20x20@1x.png
│ │ │ │ ├── Icon-App-20x20@2x.png
│ │ │ │ ├── Icon-App-20x20@3x.png
│ │ │ │ ├── Icon-App-29x29@1x.png
│ │ │ │ ├── Icon-App-29x29@2x.png
│ │ │ │ ├── Icon-App-29x29@3x.png
│ │ │ │ ├── Icon-App-40x40@1x.png
│ │ │ │ ├── Icon-App-40x40@2x.png
│ │ │ │ ├── Icon-App-40x40@3x.png
│ │ │ │ ├── Icon-App-60x60@2x.png
│ │ │ │ ├── Icon-App-60x60@3x.png
│ │ │ │ ├── Icon-App-76x76@1x.png
│ │ │ │ ├── Icon-App-76x76@2x.png
│ │ │ │ ├── Icon-App-1024x1024@1x.png
│ │ │ │ ├── Icon-App-83.5x83.5@2x.png
│ │ │ │ └── Contents.json
│ │ ├── main.m
│ │ ├── AppDelegate.swift
│ │ ├── AppDelegate.m
│ │ ├── Base.lproj
│ │ │ ├── Main.storyboard
│ │ │ └── LaunchScreen.storyboard
│ │ └── Info.plist
│ ├── Flutter
│ │ ├── Debug.xcconfig
│ │ ├── Release.xcconfig
│ │ └── AppFrameworkInfo.plist
│ ├── Runner.xcodeproj
│ │ ├── project.xcworkspace
│ │ │ ├── contents.xcworkspacedata
│ │ │ └── xcshareddata
│ │ │ │ ├── WorkspaceSettings.xcsettings
│ │ │ │ └── IDEWorkspaceChecks.plist
│ │ └── xcshareddata
│ │ │ └── xcschemes
│ │ │ └── Runner.xcscheme
│ ├── Runner.xcworkspace
│ │ ├── contents.xcworkspacedata
│ │ └── xcshareddata
│ │ │ ├── WorkspaceSettings.xcsettings
│ │ │ └── IDEWorkspaceChecks.plist
│ ├── .gitignore
│ ├── Podfile.lock
│ └── Podfile
├── android
│ ├── gradle.properties
│ ├── .gitignore
│ ├── 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
│ │ │ │ │ └── values
│ │ │ │ │ │ └── styles.xml
│ │ │ │ ├── java
│ │ │ │ │ └── dev
│ │ │ │ │ │ └── thinkng
│ │ │ │ │ │ └── flt_worker_example
│ │ │ │ │ │ └── MainActivity.java
│ │ │ │ └── AndroidManifest.xml
│ │ │ ├── debug
│ │ │ │ └── AndroidManifest.xml
│ │ │ └── profile
│ │ │ │ └── AndroidManifest.xml
│ │ └── build.gradle
│ ├── gradle
│ │ └── wrapper
│ │ │ └── gradle-wrapper.properties
│ ├── settings.gradle
│ └── build.gradle
├── .metadata
├── .gitignore
├── test
│ └── widget_test.dart
├── lib
│ ├── worker.dart
│ ├── counter_file.dart
│ ├── btc_price_file.dart
│ ├── background_tasks_counter.dart
│ ├── counter.dart
│ ├── work_manager_counter.dart
│ ├── background_tasks_btc_prices.dart
│ ├── work_manager_btc_prices.dart
│ ├── btc_prices.dart
│ ├── rest.dart
│ └── main.dart
├── pubspec.yaml
├── README.md
└── pubspec.lock
├── CHANGELOG.md
├── test
├── flt_worker_test.dart
├── ios_delegate_test.dart
└── android_delegate_test.dart
├── .idea
├── codeStyles
│ ├── codeStyleConfig.xml
│ └── Project.xml
├── dictionaries
│ └── ywu.xml
└── runConfigurations
│ └── example_lib_main_dart.xml
├── lib
├── android.dart
├── ios.dart
├── src
│ ├── utils.dart
│ ├── background_tasks
│ │ ├── background_tasks.dart
│ │ ├── delegate.dart
│ │ └── models.dart
│ ├── work_manager
│ │ ├── delegate.dart
│ │ ├── work_manager.dart
│ │ └── models.dart
│ ├── functions.dart
│ ├── callback_dispatcher.dart
│ ├── constraints.dart
│ └── models.dart
└── flt_worker.dart
├── .metadata
├── .gitignore
├── pubspec.yaml
├── .github
└── workflows
│ ├── publish.yaml
│ └── check.yaml
├── flt_worker.iml
├── LICENSE
├── pubspec.lock
└── README.md
/ios/Assets/.gitkeep:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/android/settings.gradle:
--------------------------------------------------------------------------------
1 | rootProject.name = 'flt_worker'
2 |
--------------------------------------------------------------------------------
/analysis_options.yaml:
--------------------------------------------------------------------------------
1 | include: package:pedantic/analysis_options.yaml
2 |
--------------------------------------------------------------------------------
/example/ios/Runner/Runner-Bridging-Header.h:
--------------------------------------------------------------------------------
1 | #import "GeneratedPluginRegistrant.h"
2 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | :point_right: [Release History](https://github.com/xinthink/flt_worker/releases)
2 |
--------------------------------------------------------------------------------
/android/.gitignore:
--------------------------------------------------------------------------------
1 | *.iml
2 | .gradle
3 | /local.properties
4 | /.idea/workspace.xml
5 | /.idea/libraries
6 | .DS_Store
7 | /build
8 | /captures
9 |
--------------------------------------------------------------------------------
/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.enableR8=true
3 | android.useAndroidX=true
4 | android.enableJetifier=true
5 |
--------------------------------------------------------------------------------
/example/ios/Flutter/Debug.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.debug.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/android/gradle.properties:
--------------------------------------------------------------------------------
1 | org.gradle.jvmargs=-Xmx1536M
2 | android.enableR8=true
3 | android.useAndroidX=true
4 | android.enableJetifier=true
5 |
--------------------------------------------------------------------------------
/example/ios/Flutter/Release.xcconfig:
--------------------------------------------------------------------------------
1 | #include "Pods/Target Support Files/Pods-Runner/Pods-Runner.release.xcconfig"
2 | #include "Generated.xcconfig"
3 |
--------------------------------------------------------------------------------
/example/ios/Runner/AppDelegate.h:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 |
4 | @interface AppDelegate : FlutterAppDelegate
5 |
6 | @end
7 |
--------------------------------------------------------------------------------
/android/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
--------------------------------------------------------------------------------
/example/android/.gitignore:
--------------------------------------------------------------------------------
1 | gradle-wrapper.jar
2 | /.gradle
3 | /captures/
4 | /gradlew
5 | /gradlew.bat
6 | /local.properties
7 | GeneratedPluginRegistrant.java
8 |
--------------------------------------------------------------------------------
/test/flt_worker_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter_test/flutter_test.dart';
2 |
3 | void main() {
4 | test('temaple', () {
5 | expect(1, 1);
6 | });
7 | }
8 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
--------------------------------------------------------------------------------
/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage.png
--------------------------------------------------------------------------------
/android/.idea/codeStyles/codeStyleConfig.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/LaunchImage@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-20x20@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-29x29@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-40x40@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-60x60@3x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-76x76@2x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-1024x1024@1x.png
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/xinthink/flt_worker/HEAD/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Icon-App-83.5x83.5@2x.png
--------------------------------------------------------------------------------
/.idea/dictionaries/ywu.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | backoff
5 | cupertino
6 | unmetered
7 |
8 |
9 |
--------------------------------------------------------------------------------
/lib/android.dart:
--------------------------------------------------------------------------------
1 | /// The low level api specific for the Android platform, mapping to the `WorkManager` library.
2 | library work_manager;
3 |
4 | export 'flt_worker.dart' show initializeWorker;
5 | export 'src/work_manager/work_manager.dart';
6 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcodeproj/project.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/lib/ios.dart:
--------------------------------------------------------------------------------
1 | /// The low level api specific for the iOS platform, mapping to the `BackgroundTasks` framework.
2 | library background_tasks;
3 |
4 | export 'flt_worker.dart' show initializeWorker;
5 | export 'src/background_tasks/background_tasks.dart';
6 |
--------------------------------------------------------------------------------
/android/gradle/wrapper/gradle-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionBase=GRADLE_USER_HOME
2 | distributionPath=wrapper/dists
3 | zipStoreBase=GRADLE_USER_HOME
4 | zipStorePath=wrapper/dists
5 | distributionUrl=https\://services.gradle.org/distributions/gradle-5.6.2-all.zip
6 |
--------------------------------------------------------------------------------
/example/ios/Runner/main.m:
--------------------------------------------------------------------------------
1 | #import
2 | #import
3 | #import "AppDelegate.h"
4 |
5 | int main(int argc, char* argv[]) {
6 | @autoreleasepool {
7 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class]));
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/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-5.6.2-all.zip
7 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcworkspace/contents.xcworkspacedata:
--------------------------------------------------------------------------------
1 |
2 |
4 |
6 |
7 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/WorkspaceSettings.xcsettings:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PreviewsEnabled
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcodeproj/project.xcworkspace/xcshareddata/IDEWorkspaceChecks.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | IDEDidComputeMac32BitWarning
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/.idea/runConfigurations/example_lib_main_dart.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
--------------------------------------------------------------------------------
/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 67826bdce54505760fe83b7ead70bdb5af6fe9f2
8 | channel: dev
9 |
10 | project_type: plugin
11 |
--------------------------------------------------------------------------------
/example/.metadata:
--------------------------------------------------------------------------------
1 | # This file tracks properties of this Flutter project.
2 | # Used by Flutter tool to assess capabilities and perform upgrades etc.
3 | #
4 | # This file should be version controlled and should not be manually edited.
5 |
6 | version:
7 | revision: 67826bdce54505760fe83b7ead70bdb5af6fe9f2
8 | channel: dev
9 |
10 | project_type: app
11 |
--------------------------------------------------------------------------------
/example/android/app/src/debug/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/android/app/src/profile/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/README.md:
--------------------------------------------------------------------------------
1 | # Launch Screen Assets
2 |
3 | You can customize the launch screen with your own desired assets by replacing the image files in this directory.
4 |
5 | You can also do it by opening your Flutter project's Xcode project with `open ios/Runner.xcworkspace`, selecting `Runner/Assets.xcassets` in the Project Navigator and dropping in the desired images.
--------------------------------------------------------------------------------
/example/ios/Runner/AppDelegate.swift:
--------------------------------------------------------------------------------
1 | import UIKit
2 | import Flutter
3 |
4 | @UIApplicationMain
5 | @objc class AppDelegate: FlutterAppDelegate {
6 | override func application(
7 | _ application: UIApplication,
8 | didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
9 | ) -> Bool {
10 | GeneratedPluginRegistrant.register(with: self)
11 | return super.application(application, didFinishLaunchingWithOptions: launchOptions)
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/drawable/launch_background.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
12 |
13 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/LaunchImage.imageset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "idiom" : "universal",
5 | "filename" : "LaunchImage.png",
6 | "scale" : "1x"
7 | },
8 | {
9 | "idiom" : "universal",
10 | "filename" : "LaunchImage@2x.png",
11 | "scale" : "2x"
12 | },
13 | {
14 | "idiom" : "universal",
15 | "filename" : "LaunchImage@3x.png",
16 | "scale" : "3x"
17 | }
18 | ],
19 | "info" : {
20 | "version" : 1,
21 | "author" : "xcode"
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/example/android/settings.gradle:
--------------------------------------------------------------------------------
1 | include ':app'
2 |
3 | def flutterProjectRoot = rootProject.projectDir.parentFile.toPath()
4 |
5 | def plugins = new Properties()
6 | def pluginsFile = new File(flutterProjectRoot.toFile(), '.flutter-plugins')
7 | if (pluginsFile.exists()) {
8 | pluginsFile.withReader('UTF-8') { reader -> plugins.load(reader) }
9 | }
10 |
11 | plugins.each { name, path ->
12 | def pluginDirectory = flutterProjectRoot.resolve(path).resolve('android').toFile()
13 | include ":$name"
14 | project(":$name").projectDir = pluginDirectory
15 | }
16 |
--------------------------------------------------------------------------------
/ios/.gitignore:
--------------------------------------------------------------------------------
1 | .idea/
2 | .vagrant/
3 | .sconsign.dblite
4 | .svn/
5 |
6 | .DS_Store
7 | *.swp
8 | profile
9 |
10 | DerivedData/
11 | build/
12 | GeneratedPluginRegistrant.h
13 | GeneratedPluginRegistrant.m
14 |
15 | .generated/
16 |
17 | *.pbxuser
18 | *.mode1v3
19 | *.mode2v3
20 | *.perspectivev3
21 |
22 | !default.pbxuser
23 | !default.mode1v3
24 | !default.mode2v3
25 | !default.perspectivev3
26 |
27 | xcuserdata
28 |
29 | *.moved-aside
30 |
31 | *.pyc
32 | *sync/
33 | Icon?
34 | .tags*
35 |
36 | /Flutter/Generated.xcconfig
37 | /Flutter/flutter_export_environment.sh
--------------------------------------------------------------------------------
/ios/Classes/internal/BGTaskHandler.h:
--------------------------------------------------------------------------------
1 | //
2 | // BGTaskHandler.h
3 | // Pods
4 | //
5 | // Created by Yingxin Wu on 2020/3/13.
6 | //
7 |
8 | #ifndef BGTaskHandler_h
9 | #define BGTaskHandler_h
10 |
11 | #import "utils.h"
12 | #import
13 |
14 | @interface BGTaskHandler : NSObject
15 |
16 | @property (class, readonly) BGTaskHandler * _Nonnull instance;
17 | @property (class, nonatomic) FuncRegisterPlugins _Nullable registerPlugins;
18 |
19 | - (void)handleBGTask:(BGTask * _Nonnull)task API_AVAILABLE(ios(13.0));
20 |
21 | @end
22 |
23 | #endif /* BGTaskHandler_h */
24 |
--------------------------------------------------------------------------------
/example/android/build.gradle:
--------------------------------------------------------------------------------
1 | buildscript {
2 | repositories {
3 | google()
4 | jcenter()
5 | }
6 |
7 | dependencies {
8 | classpath 'com.android.tools.build:gradle:3.5.3'
9 | }
10 | }
11 |
12 | allprojects {
13 | repositories {
14 | google()
15 | jcenter()
16 | }
17 | }
18 |
19 | rootProject.buildDir = '../build'
20 | subprojects {
21 | project.buildDir = "${rootProject.buildDir}/${project.name}"
22 | }
23 | subprojects {
24 | project.evaluationDependsOn(':app')
25 | }
26 |
27 | task clean(type: Delete) {
28 | delete rootProject.buildDir
29 | }
30 |
--------------------------------------------------------------------------------
/android/.idea/inspectionProfiles/Project_Default.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/ios/Classes/FltWorkerPlugin.h:
--------------------------------------------------------------------------------
1 | #import "utils.h"
2 | #import
3 |
4 |
5 | @interface FltWorkerPlugin : NSObject
6 |
7 | /**
8 | * Provides a callback to register needed plugins for the headless isolate.
9 | *
10 | * Example:
11 | * ```
12 | * - (BOOL)application:(UIApplication *)application
13 | * didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
14 | * FltWorkerPlugin.registerPlugins = ^(NSObject *registry) {
15 | * [GeneratedPluginRegistrant registerWithRegistry:registry];
16 | * };
17 | * }
18 | * ```
19 | */
20 | @property (class, nonatomic) FuncRegisterPlugins registerPlugins;
21 | @end
22 |
--------------------------------------------------------------------------------
/example/ios/.gitignore:
--------------------------------------------------------------------------------
1 | *.mode1v3
2 | *.mode2v3
3 | *.moved-aside
4 | *.pbxuser
5 | *.perspectivev3
6 | **/*sync/
7 | .sconsign.dblite
8 | .tags*
9 | **/.vagrant/
10 | **/DerivedData/
11 | Icon?
12 | **/Pods/
13 | **/.symlinks/
14 | profile
15 | xcuserdata
16 | **/.generated/
17 | Flutter/App.framework
18 | Flutter/Flutter.framework
19 | Flutter/Flutter.podspec
20 | Flutter/Generated.xcconfig
21 | Flutter/app.flx
22 | Flutter/app.zip
23 | Flutter/flutter_assets/
24 | Flutter/flutter_export_environment.sh
25 | ServiceDefinitions.json
26 | Runner/GeneratedPluginRegistrant.*
27 |
28 | # Exceptions to above rules.
29 | !default.mode1v3
30 | !default.mode2v3
31 | !default.pbxuser
32 | !default.perspectivev3
33 |
--------------------------------------------------------------------------------
/ios/Classes/internal/utils.m:
--------------------------------------------------------------------------------
1 | //
2 | // utils.m
3 | // flt_worker
4 | //
5 | // Created by Yingxin Wu on 2020/3/13.
6 | //
7 |
8 | #import "utils.h"
9 |
10 | const NSString *_lock = @"";
11 |
12 | NSUserDefaults *_workerDefaults = nil;
13 | NSUserDefaults* workerDefaults() {
14 | @synchronized (_lock) {
15 | if (_workerDefaults == nil) {
16 | _workerDefaults = [[NSUserDefaults alloc] init];
17 | }
18 | return _workerDefaults;
19 | }
20 | }
21 |
22 | int64_t dispatcherHandle() {
23 | return [[workerDefaults() objectForKey:@DISPATCHER_KEY] longValue] ?: 0;
24 | }
25 |
26 | int64_t workerHandle() {
27 | return [[workerDefaults() objectForKey:@WORKER_KEY] longValue] ?: 0;
28 | }
29 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | .dart_tool/
3 |
4 | .packages
5 | .pub/
6 |
7 | build/
8 | doc/
9 |
10 | # IntelliJ related
11 | *.iml
12 | *.ipr
13 | *.iws
14 | # .idea/
15 | **/.idea/.name
16 | **/.idea/assetWizardSettings.xml
17 | **/.idea/caches
18 | # .idea/dictionaries
19 | **/.idea/gradle.xml
20 | **/.idea/jarRepositories.xml
21 | **/.idea/jsLibraryMappings.xml
22 | **/.idea/libraries
23 | **/.idea/misc.xml
24 | **/.idea/modules.xml
25 | **/.idea/navEditor.xml
26 | **/.idea/runConfigurations.xml
27 | **/.idea/tasks.xml
28 | **/.idea/vcs.xml
29 | **/.idea/workspace.xml
30 |
31 | # The .vscode folder contains launch configuration and tasks you configure in
32 | # VS Code which you may wish to be included in version control, so this line
33 | # is commented out by default.
34 | .vscode/
35 |
--------------------------------------------------------------------------------
/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: flt_worker
2 | description: The flt_worker plugin allows you to schedule and execute Dart background tasks, based on the WorkManager and the BackgroundTasks APIs, for Android and iOS 13+ respectively.
3 | version: 0.1.0
4 | homepage: https://github.com/xinthink/flt_worker/
5 | repository: https://github.com/xinthink/flt_worker/
6 |
7 | environment:
8 | sdk: ">=2.3.0 <3.0.0"
9 | flutter: "^1.10.0"
10 |
11 | dependencies:
12 | flutter:
13 | sdk: flutter
14 |
15 | dev_dependencies:
16 | flutter_test:
17 | sdk: flutter
18 | pedantic: ^1.8.0
19 |
20 | flutter:
21 | plugin:
22 | platforms:
23 | android:
24 | package: dev.thinkng.flt_worker
25 | pluginClass: FltWorkerPlugin
26 | ios:
27 | pluginClass: FltWorkerPlugin
28 |
--------------------------------------------------------------------------------
/lib/src/utils.dart:
--------------------------------------------------------------------------------
1 | import 'dart:ui';
2 |
3 | import 'package:flutter/services.dart';
4 |
5 | import 'models.dart';
6 |
7 | const CHANNEL_NAME = 'dev.thinkng.flt_worker';
8 | const METHOD_PREFIX = 'FltWorkerPlugin';
9 |
10 | /// Typedef of a worker function.
11 | typedef WorkerFn = Future Function(WorkPayload payload);
12 |
13 | /// The shared method channel for api calls
14 | const apiChannel = MethodChannel(CHANNEL_NAME);
15 |
16 | /// Returns the raw handle of the [callback] function, throws if it doesn't exist.
17 | int ensureRawHandle(Function callback) {
18 | final handle = PluginUtilities.getCallbackHandle(callback)?.toRawHandle();
19 | if (handle == null) {
20 | throw Exception('CallbackHandle not found for the specified function. Make sure to use a top level function');
21 | }
22 | return handle;
23 | }
24 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yaml:
--------------------------------------------------------------------------------
1 | name: publish
2 |
3 | on:
4 | release:
5 | types: published
6 |
7 | jobs:
8 | publish:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v1
12 | - uses: subosito/flutter-action@v1
13 | - name: Format
14 | run: |
15 | $FLUTTER_HOME/bin/cache/dart-sdk/bin/dartfmt -l 80 -w .
16 | - name: Publish
17 | run: |
18 | mkdir ~/.pub-cache/
19 | echo "${{ secrets.PUB_CREDENTIALS }}" | base64 --decode > ~/.pub-cache/credentials.json
20 | pub publish --force
21 | - name: notification
22 | if: cancelled() == false
23 | uses: xinthink/action-telegram@v1.1
24 | with:
25 | botToken: ${{ secrets.TelegramBotToken }}
26 | chatId: ${{ secrets.TelegramTarget }}
27 | jobStatus: ${{ job.status }}
28 |
--------------------------------------------------------------------------------
/example/.gitignore:
--------------------------------------------------------------------------------
1 | # Miscellaneous
2 | *.class
3 | *.log
4 | *.pyc
5 | *.swp
6 | .DS_Store
7 | .atom/
8 | .buildlog/
9 | .history
10 | .svn/
11 |
12 | # IntelliJ related
13 | *.iml
14 | *.ipr
15 | *.iws
16 | .idea/
17 |
18 | # The .vscode folder contains launch configuration and tasks you configure in
19 | # VS Code which you may wish to be included in version control, so this line
20 | # is commented out by default.
21 | #.vscode/
22 |
23 | # Flutter/Dart/Pub related
24 | **/doc/api/
25 | .dart_tool/
26 | .flutter-plugins
27 | .flutter-plugins-dependencies
28 | .packages
29 | .pub-cache/
30 | .pub/
31 | /build/
32 |
33 | # Web related
34 | lib/generated_plugin_registrant.dart
35 |
36 | # Symbolication related
37 | app.*.symbols
38 |
39 | # Exceptions to above rules.
40 | !/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
41 |
--------------------------------------------------------------------------------
/example/android/app/src/main/java/dev/thinkng/flt_worker_example/MainActivity.java:
--------------------------------------------------------------------------------
1 | package dev.thinkng.flt_worker_example;
2 |
3 | import androidx.annotation.NonNull;
4 |
5 | import dev.thinkng.flt_worker.FltWorkerPlugin;
6 | import io.flutter.embedding.android.FlutterActivity;
7 | import io.flutter.embedding.engine.FlutterEngine;
8 | import io.flutter.plugins.GeneratedPluginRegistrant;
9 |
10 | public class MainActivity extends FlutterActivity {
11 | @Override
12 | public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
13 | GeneratedPluginRegistrant.registerWith(flutterEngine);
14 | FltWorkerPlugin.registerPluginsForWorkers = registry -> {
15 | io.flutter.plugins.pathprovider.PathProviderPlugin.registerWith(
16 | registry.registrarFor("io.flutter.plugins.pathprovider.PathProviderPlugin"));
17 | return null;
18 | };
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/example/ios/Flutter/AppFrameworkInfo.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | CFBundleDevelopmentRegion
6 | $(DEVELOPMENT_LANGUAGE)
7 | CFBundleExecutable
8 | App
9 | CFBundleIdentifier
10 | io.flutter.flutter.app
11 | CFBundleInfoDictionaryVersion
12 | 6.0
13 | CFBundleName
14 | App
15 | CFBundlePackageType
16 | FMWK
17 | CFBundleShortVersionString
18 | 1.0
19 | CFBundleSignature
20 | ????
21 | CFBundleVersion
22 | 1.0
23 | MinimumOSVersion
24 | 8.0
25 |
26 |
27 |
--------------------------------------------------------------------------------
/android/src/main/java/dev/thinkng/flt_worker/internal/BackgroundWorker.java:
--------------------------------------------------------------------------------
1 | package dev.thinkng.flt_worker.internal;
2 |
3 | import android.content.Context;
4 | import android.util.Log;
5 |
6 | import androidx.annotation.Keep;
7 | import androidx.annotation.NonNull;
8 | import androidx.work.Worker;
9 | import androidx.work.WorkerParameters;
10 |
11 | @Keep
12 | public class BackgroundWorker extends Worker {
13 |
14 | public BackgroundWorker(@NonNull Context context, @NonNull WorkerParameters params) {
15 | super(context, params);
16 | }
17 |
18 | @NonNull
19 | @Override
20 | public Result doWork() {
21 | try {
22 | BackgroundWorkerPlugin.getInstance(getApplicationContext())
23 | .doWork(this)
24 | .get();
25 | return Result.success();
26 | } catch (Throwable e) {
27 | Log.e(AbsWorkerPlugin.TAG, "worker execution failure", e);
28 | return Result.failure();
29 | }
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/android/build.gradle:
--------------------------------------------------------------------------------
1 | group 'dev.thinkng.flt_worker'
2 | version '1.0'
3 |
4 | buildscript {
5 | repositories {
6 | google()
7 | jcenter()
8 | }
9 |
10 | dependencies {
11 | classpath 'com.android.tools.build:gradle:3.5.3'
12 | }
13 | }
14 |
15 | rootProject.allprojects {
16 | repositories {
17 | google()
18 | jcenter()
19 | }
20 | }
21 |
22 | apply plugin: 'com.android.library'
23 |
24 | android {
25 | compileSdkVersion 28
26 |
27 | defaultConfig {
28 | minSdkVersion 16
29 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
30 | }
31 | lintOptions {
32 | disable 'InvalidPackage'
33 | }
34 | }
35 |
36 | dependencies {
37 | def work_version = "2.3.2"
38 |
39 | implementation "androidx.work:work-runtime:$work_version"
40 | // implementation "androidx.work:work-rxjava2:$work_version"
41 |
42 | testCompile 'junit:junit:4.12'
43 | }
44 |
--------------------------------------------------------------------------------
/ios/Classes/internal/BGTaskMgrDelegate.h:
--------------------------------------------------------------------------------
1 | //
2 | // BGTaskMgrDelegate.h
3 | // flt_worker
4 | //
5 | // Created by Yingxin Wu on 2020/3/12.
6 | //
7 |
8 | #ifndef BGTaskMgrDelegate_h
9 | #define BGTaskMgrDelegate_h
10 |
11 | #import
12 |
13 | @interface BGTaskMgrDelegate : NSObject
14 |
15 | @property (readonly, nonatomic) FlutterMethodChannel *methodChannel;
16 |
17 | /** Register background task indentifiers. */
18 | + (void)registerBGTaskHandler;
19 |
20 | - (instancetype)initWithRegistrar:(NSObject*)registrar;
21 |
22 | - (instancetype)initWithEngine:(FlutterEngine*)engine;
23 |
24 | - (BOOL)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result;
25 |
26 | /** Save dispatcher & worker handles for later use */
27 | - (void)saveHandles:(NSArray*)args;
28 |
29 | /** Makes a payload dict used as input of the dart worker */
30 | - (NSDictionary*)packPayloadForTask:(NSString*)identifier;
31 | @end
32 |
33 | #endif /* BGTaskMgrDelegate_h */
34 |
--------------------------------------------------------------------------------
/example/ios/Runner/AppDelegate.m:
--------------------------------------------------------------------------------
1 | #import "AppDelegate.h"
2 | #import "GeneratedPluginRegistrant.h"
3 | #import
4 | #import
5 |
6 | @implementation AppDelegate
7 |
8 | - (BOOL)application:(UIApplication *)application
9 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
10 |
11 | // set a callback to register all plugins to a headless engine instance
12 | FltWorkerPlugin.registerPlugins = ^(NSObject *registry) {
13 | [FLTPathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTPathProviderPlugin"]];
14 | };
15 |
16 | [GeneratedPluginRegistrant registerWithRegistry:self];
17 | // Override point for customization after application launch.
18 | return [super application:application didFinishLaunchingWithOptions:launchOptions];
19 | }
20 |
21 | - (void)applicationDidEnterBackground:(UIApplication *)application {
22 | NSLog(@"app did enter background…");
23 | }
24 |
25 | @end
26 |
--------------------------------------------------------------------------------
/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 that Flutter provides. 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:flt_worker_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(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/check.yaml:
--------------------------------------------------------------------------------
1 | name: check
2 |
3 | on: [push, pull_request]
4 |
5 | jobs:
6 | check:
7 | runs-on: ubuntu-latest
8 | steps:
9 | - uses: actions/checkout@v1
10 | - uses: subosito/flutter-action@v1
11 | - name: Check
12 | run: |
13 | cd example && flutter analyze && cd ..
14 | flutter test
15 | - name: notification
16 | if: cancelled() == false
17 | uses: xinthink/action-telegram@v1.1
18 | with:
19 | botToken: ${{ secrets.TelegramBotToken }}
20 | chatId: ${{ secrets.TelegramTarget }}
21 | jobStatus: ${{ job.status }}
22 |
23 | # coverage:
24 | # runs-on: ubuntu-latest
25 | # container:
26 | # image: google/dart:dev
27 | # steps:
28 | # - uses: actions/checkout@v1
29 | # - run: pub get
30 | # - name: Code Coverage
31 | # run: |
32 | # pub global activate test_coverage
33 | # pub global run test_coverage
34 | # - uses: codecov/codecov-action@v1.0.0
35 | # with:
36 | # token: ${{ secrets.CODECOV_TOKEN }}
37 |
--------------------------------------------------------------------------------
/example/android/app/src/main/res/values/styles.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
9 |
15 |
18 |
19 |
--------------------------------------------------------------------------------
/ios/flt_worker.podspec:
--------------------------------------------------------------------------------
1 | #
2 | # To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
3 | # Run `pod lib lint flt_worker.podspec' to validate before publishing.
4 | #
5 | Pod::Spec.new do |s|
6 | s.name = 'flt_worker'
7 | s.version = '0.0.1'
8 | s.summary = 'A new flutter plugin project.'
9 | s.description = <<-DESC
10 | A new flutter plugin project.
11 | DESC
12 | s.homepage = 'http://example.com'
13 | s.license = { :file => '../LICENSE' }
14 | s.author = { 'Your Company' => 'email@example.com' }
15 | s.source = { :path => '.' }
16 | s.source_files = 'Classes/**/*'
17 | s.public_header_files = 'Classes/**/*.h'
18 | s.dependency 'Flutter'
19 | s.weak_framework = 'BackgroundTasks'
20 | s.platform = :ios, '8.0'
21 |
22 | # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported.
23 | s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
24 | end
25 |
--------------------------------------------------------------------------------
/lib/src/background_tasks/background_tasks.dart:
--------------------------------------------------------------------------------
1 | /// The low level api specific for the iOS platform, mapping to the `BackgroundTasks` framework.
2 | library background_tasks;
3 |
4 | import 'models.dart';
5 | import '../utils.dart';
6 |
7 | export 'models.dart';
8 |
9 | /// Schedules a previously registered background task for execution.
10 | Future submitTaskRequest(BGTaskRequest request)
11 | => apiChannel.invokeMethod('$METHOD_PREFIX#submitTaskRequest', request.toJson());
12 |
13 | /// Cancels a scheduled task request with the [identifier].
14 | Future cancelTaskRequest(String identifier)
15 | => apiChannel.invokeMethod('$METHOD_PREFIX#cancelTaskRequest', identifier);
16 |
17 | /// Cancels all scheduled task requests.
18 | Future cancelAllTaskRequests()
19 | => apiChannel.invokeMethod('$METHOD_PREFIX#cancelAllTaskRequests');
20 |
21 | /// Simulate launch BGTask with the given [identifier], **debugging only**
22 | Future simulateLaunchTask(String identifier)
23 | => apiChannel.invokeMethod('$METHOD_PREFIX#simulateLaunchTask', identifier);
24 |
--------------------------------------------------------------------------------
/lib/src/work_manager/delegate.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 |
3 | import 'work_manager.dart';
4 | import '../models.dart';
5 |
6 | /// Enqueues a request to work in the background.
7 | Future enqueueWorkIntent(WorkIntent intent) =>
8 | enqueueWorkRequest(parseWorkIntent(intent));
9 |
10 | Future cancelWork(String id) => cancelAllWorkByTag(id);
11 |
12 | Future wmCancelAllWork() => cancelAllWork();
13 |
14 | @visibleForTesting
15 | WorkRequest parseWorkIntent(WorkIntent intent) {
16 | final tags = [intent.identifier] + (intent.tags ?? []);
17 |
18 | return intent.repeatInterval != null
19 | ? PeriodicWorkRequest(
20 | tags: tags,
21 | input: intent.input,
22 | initialDelay: intent.initialDelay,
23 | constraints: intent.constraints,
24 | repeatInterval: intent.repeatInterval,
25 | flexInterval: intent.flexInterval,
26 | )
27 | : OneTimeWorkRequest(
28 | tags: tags,
29 | input: intent.input,
30 | initialDelay: intent.initialDelay,
31 | constraints: intent.constraints,
32 | );
33 | }
34 |
--------------------------------------------------------------------------------
/lib/src/functions.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:io';
3 |
4 | import 'background_tasks/delegate.dart' as bg;
5 | import 'models.dart';
6 | import 'work_manager/delegate.dart' as wm;
7 |
8 | /// Enqueues a request to work in the background.
9 | final Future Function(WorkIntent intent) enqueueWorkIntent =
10 | Platform.isAndroid ? wm.enqueueWorkIntent : bg.enqueueWorkIntent;
11 |
12 | /// Cancels all unfinished work with the given [identifier].
13 | ///
14 | /// Note that cancellation is a best-effort policy and work that is already executing may continue to run.
15 | final Future Function(String id) cancelWork =
16 | Platform.isAndroid ? wm.cancelWork : bg.cancelWork;
17 |
18 | /// Cancels all unfinished work.
19 | ///
20 | /// **Use this method with extreme caution!**
21 | /// By invoking it, you will potentially affect other modules or libraries in your codebase.
22 | /// It is strongly recommended that you use one of the other cancellation methods at your disposal.
23 | final Future Function() cancelAllWork =
24 | Platform.isAndroid ? wm.wmCancelAllWork : bg.cancelAllWork;
25 |
--------------------------------------------------------------------------------
/ios/Classes/internal/utils.h:
--------------------------------------------------------------------------------
1 | //
2 | // utils.h
3 | // Pods
4 | //
5 | // Created by Yingxin Wu on 2020/3/13.
6 | //
7 |
8 | #ifndef utils_h
9 | #define utils_h
10 |
11 | #import
12 |
13 | #define PLUGIN_PKG "dev.thinkng.flt_worker"
14 | #define API_METHOD(NAME) "FltWorkerPlugin#"#NAME
15 | #define DISPATCHER_KEY "dev.thinkng.flt_worker/callback_dispatcher_handle"
16 | #define WORKER_KEY "dev.thinkng.flt_worker/worker_handle"
17 | #define IS_NONNULL(V) V && ![NSNull.null isEqual:V]
18 | #define TASK_KEY(id) [NSString stringWithFormat:@"dev.thinkng.flt_worker/tasks/%@", id]
19 | #define WORKER_DEFAULTS_LONG(K) [[workerDefaults() objectForKey:@K] longValue] ?: 0
20 |
21 | /** Retrieves the `UserDefaults` instance for FltWorkerPlugin. */
22 | NSUserDefaults* workerDefaults(void);
23 |
24 | /** Returns raw function handle of the callback dispatcher. */
25 | int64_t dispatcherHandle(void);
26 |
27 | /** Returns raw handle of the worker function. */
28 | int64_t workerHandle(void);
29 |
30 | typedef void (^FuncRegisterPlugins)(NSObject*registry);
31 |
32 | #endif /* utils_h */
33 |
--------------------------------------------------------------------------------
/example/ios/Podfile.lock:
--------------------------------------------------------------------------------
1 | PODS:
2 | - flt_worker (0.0.1):
3 | - Flutter
4 | - Flutter (1.0.0)
5 | - path_provider (0.0.1):
6 | - Flutter
7 | - path_provider_macos (0.0.1):
8 | - Flutter
9 |
10 | DEPENDENCIES:
11 | - flt_worker (from `.symlinks/plugins/flt_worker/ios`)
12 | - Flutter (from `Flutter`)
13 | - path_provider (from `.symlinks/plugins/path_provider/ios`)
14 | - path_provider_macos (from `.symlinks/plugins/path_provider_macos/ios`)
15 |
16 | EXTERNAL SOURCES:
17 | flt_worker:
18 | :path: ".symlinks/plugins/flt_worker/ios"
19 | Flutter:
20 | :path: Flutter
21 | path_provider:
22 | :path: ".symlinks/plugins/path_provider/ios"
23 | path_provider_macos:
24 | :path: ".symlinks/plugins/path_provider_macos/ios"
25 |
26 | SPEC CHECKSUMS:
27 | flt_worker: 09a85dc9bfc12106faa15f7c00b26f85cbccb3e6
28 | Flutter: 0e3d915762c693b495b44d77113d4970485de6ec
29 | path_provider: fb74bd0465e96b594bb3b5088ee4a4e7bb1f2a9d
30 | path_provider_macos: f760a3c5b04357c380e2fddb6f9db6f3015897e0
31 |
32 | PODFILE CHECKSUM: 083258d7f5e80b42ea9bfee905fe93049bc04c64
33 |
34 | COCOAPODS: 1.8.4
35 |
--------------------------------------------------------------------------------
/flt_worker.iml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 xinthink
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/lib/src/callback_dispatcher.dart:
--------------------------------------------------------------------------------
1 | import 'dart:async';
2 | import 'dart:ui';
3 |
4 | import 'package:flutter/services.dart';
5 | import 'package:flutter/widgets.dart';
6 |
7 | import 'models.dart';
8 | import 'utils.dart';
9 |
10 | /// Callback dispatcher, which is the entry of the isolate running background workers.
11 | void callbackDispatcher() {
12 | WidgetsFlutterBinding.ensureInitialized();
13 | // final channel = MethodChannel('$CHANNEL_NAME');
14 | // channel.setMethodCallHandler(_executeBackgroundTask);
15 | apiChannel.setMethodCallHandler(_executeBackgroundTask);
16 | }
17 |
18 | /// Run the specified function in the background isoloate.
19 | Future _executeBackgroundTask(MethodCall call) {
20 | final args = call.arguments;
21 | WorkerFn callback;
22 | Map payload;
23 |
24 | if (args.isNotEmpty) {
25 | final handle = CallbackHandle.fromRawHandle(args[0]);
26 | payload = Map.castFrom(args[1]);
27 | if (handle != null) {
28 | callback = PluginUtilities.getCallbackFromHandle(handle);
29 | }
30 | }
31 |
32 | if (callback != null) {
33 | return callback(WorkPayload.fromJson(payload));
34 | }
35 |
36 | debugPrint('Callback not found for method=${call.method} args=$args');
37 | return Future.value();
38 | }
39 |
--------------------------------------------------------------------------------
/example/lib/worker.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flt_worker/flt_worker.dart';
4 |
5 | import 'btc_price_file.dart';
6 | import 'counter_file.dart';
7 |
8 | /// Worker callback running in the background isolate.
9 | Future worker(WorkPayload payload) {
10 | if (payload.tags.contains(kTagCounterWork)) {
11 | return _increaseCounter(payload.input);
12 | } else if (payload.tags.contains(kTagBtcPricesWork)) {
13 | return _fetchBtcPrice();
14 | } else {
15 | return Future.value();
16 | }
17 | }
18 |
19 | /// The worker increasing the counter.
20 | Future _increaseCounter(Map input) =>
21 | writeCounter((input['counter'] ?? 0) + 1);
22 |
23 | /// Fetches the latest BTC price via CoinBase rest api.
24 | Future _fetchBtcPrice() async {
25 | try {
26 | await fetchBtcPrice();
27 | } finally {
28 | if (Platform.isIOS) {
29 | // periodic work is not supported natively on iOS,
30 | // so we have to schedule it again after the current one is marked as complete
31 | Future.delayed(Duration(milliseconds: 50), () =>
32 | enqueueWorkIntent(const WorkIntent(
33 | identifier: kTagBtcPricesWork,
34 | initialDelay: Duration(seconds: 60),
35 | ))
36 | );
37 | }
38 | }
39 | }
40 |
--------------------------------------------------------------------------------
/lib/src/background_tasks/delegate.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 |
3 | import 'background_tasks.dart';
4 | import '../models.dart';
5 |
6 | /// Enqueues a request to work in the background.
7 | Future enqueueWorkIntent(WorkIntent intent) =>
8 | submitTaskRequest(parseWorkIntent(intent));
9 |
10 | Future cancelWork(String id) => cancelTaskRequest(id).then((_) => true);
11 |
12 | Future cancelAllWork() => cancelAllTaskRequests().then((_) => true);
13 |
14 | @visibleForTesting
15 | BGTaskRequest parseWorkIntent(WorkIntent intent) {
16 | bool network;
17 | if (intent.constraints?.networkType != null) {
18 | network = intent.constraints.networkType != NetworkType.notRequired;
19 | }
20 |
21 | final earliestBeginDate = intent.initialDelay != null
22 | ? DateTime.now().add(intent.initialDelay) : null;
23 |
24 | return intent.isProcessingTask == true
25 | ? BGProcessingTaskRequest(
26 | intent.identifier,
27 | input: intent.input,
28 | earliestBeginDate: earliestBeginDate,
29 | requiresExternalPower: intent.constraints?.charging,
30 | requiresNetworkConnectivity: network,
31 | )
32 | : BGAppRefreshTaskRequest(
33 | intent.identifier,
34 | input: intent.input,
35 | earliestBeginDate: earliestBeginDate,
36 | );
37 | }
38 |
--------------------------------------------------------------------------------
/example/lib/counter_file.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flutter/foundation.dart';
4 | import 'package:path_provider/path_provider.dart';
5 | import 'package:watcher/watcher.dart';
6 |
7 | const kTagCounterWork = 'com.example.counter_task';
8 |
9 | /// Returns the counter file path.
10 | Future counterFile() async {
11 | final dir = (await getTemporaryDirectory()).path;
12 | final file = File('$dir/counter.txt');
13 | if (!(await file.exists())) {
14 | await file.writeAsString('0');
15 | }
16 | return file;
17 | }
18 |
19 | /// A stream of the updated counter values.
20 | Stream counterStream() async* {
21 | // yield the initial value
22 | yield await readCounter();
23 |
24 | // yield a value whenever the file is modified
25 | final path = (await counterFile()).path;
26 | final updates = (Platform.isAndroid
27 | ? PollingFileWatcher(path) : FileWatcher(path)).events;
28 | await for (final _ in updates) {
29 | yield await readCounter();
30 | }
31 | }
32 |
33 | /// Reads counter from a file.
34 | Future readCounter() async {
35 | try {
36 | final counterStr = await (await counterFile()).readAsString();
37 | return counterStr.isNotEmpty ? int.parse(counterStr) : 0;
38 | } catch (e) {
39 | debugPrint('read counter file failed: $e');
40 | return 0;
41 | }
42 | }
43 |
44 | /// The worker working on the counter.
45 | Future writeCounter(int count) async {
46 | debugPrint('--- updating counter file => $count');
47 | await (await counterFile()).writeAsString('$count');
48 | }
49 |
--------------------------------------------------------------------------------
/example/ios/Runner/Base.lproj/Main.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
--------------------------------------------------------------------------------
/example/lib/btc_price_file.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'dart:io';
3 |
4 | import 'package:flutter/foundation.dart';
5 | import 'package:path_provider/path_provider.dart';
6 | import 'package:watcher/watcher.dart';
7 |
8 | import 'rest.dart';
9 |
10 | const kTagBtcPricesWork = 'com.example.btc_prices_task';
11 |
12 | /// Returns the BTC price file path.
13 | Future btcPriceFile() async {
14 | final dir = (await getTemporaryDirectory()).path;
15 | final file = File('$dir/btc_price.json');
16 | if (!(await file.exists())) {
17 | await file.writeAsString('{}', flush: true);
18 | }
19 | return file;
20 | }
21 |
22 | /// A stream of updated BTC prices.
23 | Stream btcPriceStream() async* {
24 | // yield the initial value
25 | yield await readBtcPrice();
26 |
27 | // yield a value whenever the file is modified
28 | final path = (await btcPriceFile()).path;
29 | final updates = (Platform.isAndroid
30 | ? PollingFileWatcher(path) : FileWatcher(path)).events;
31 | await for (final _ in updates) {
32 | yield await readBtcPrice();
33 | }
34 | }
35 |
36 | /// Reads the price from a data file.
37 | Future readBtcPrice() async {
38 | try {
39 | final json = jsonDecode(await (await btcPriceFile()).readAsString());
40 | return json['amount'] != null ? json : null;
41 | } catch (e) {
42 | debugPrint('read data file failed: $e');
43 | return null;
44 | }
45 | }
46 |
47 | /// Fetches the latest BTC price via CoinBase rest api.
48 | Future fetchBtcPrice() async {
49 | debugPrint('--- fetching BTC price');
50 | final resp = await getJson('https://api.coinbase.com/v2/prices/spot?currency=USD');
51 | await (await btcPriceFile()).writeAsString('''{
52 | "amount": ${resp['data']['amount']},
53 | "time": ${DateTime.now().millisecondsSinceEpoch}
54 | }''');
55 | }
56 |
--------------------------------------------------------------------------------
/lib/src/work_manager/work_manager.dart:
--------------------------------------------------------------------------------
1 | /// The low level api specific for the Android platform, mapping to the `WorkManager` library.
2 | library work_manager;
3 |
4 | import 'models.dart';
5 | import '../utils.dart';
6 |
7 | export 'models.dart';
8 |
9 | /// Enqueues one item for background processing.
10 | Future enqueueWorkRequest(WorkRequest request)
11 | => enqueueWorkRequests([request]);
12 |
13 | /// Enqueues one or more items for background processing.
14 | Future enqueueWorkRequests(Iterable requests)
15 | => apiChannel.invokeMethod('$METHOD_PREFIX#enqueue',
16 | requests.map((r) => r.toJson()).toList(growable: false)
17 | );
18 |
19 | /// Cancels all unfinished work with the given [tag].
20 | ///
21 | /// Note that cancellation is a best-effort policy and work that is already executing may continue to run.
22 | Future cancelAllWorkByTag(String tag)
23 | => apiChannel.invokeMethod('$METHOD_PREFIX#cancelAllWorkByTag', tag);
24 |
25 | /// Cancels all unfinished work in the work chain with the given [name].
26 | ///
27 | /// Note that cancellation is a best-effort policy and work that is already executing may continue to run.
28 | Future cancelUniqueWork(String name)
29 | => apiChannel.invokeMethod('$METHOD_PREFIX#cancelUniqueWork', name);
30 |
31 | /// Cancels work with the given [uuid] if it isn't finished.
32 | ///
33 | /// Note that cancellation is a best-effort policy and work that is already executing may continue to run.
34 | Future cancelWorkById(String uuid)
35 | => apiChannel.invokeMethod('$METHOD_PREFIX#cancelWorkById', uuid);
36 |
37 | /// Cancels all unfinished work.
38 | ///
39 | /// **Use this method with extreme caution!**
40 | /// By invoking it, you will potentially affect other modules or libraries in your codebase.
41 | /// It is strongly recommended that you use one of the other cancellation methods at your disposal.
42 | Future cancelAllWork()
43 | => apiChannel.invokeMethod('$METHOD_PREFIX#cancelAllWork');
44 |
--------------------------------------------------------------------------------
/example/lib/background_tasks_counter.dart:
--------------------------------------------------------------------------------
1 | import 'package:flt_worker/ios.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | import 'counter_file.dart';
5 |
6 | /// BackgroundTasks api example for the iOS platform.
7 | class BackgroundTasksCounter extends StatelessWidget {
8 | @override
9 | Widget build(BuildContext context) => Scaffold(
10 | appBar: AppBar(
11 | title: const Text('Counter (BackgroundTasks)'),
12 | ),
13 | body: SingleChildScrollView(
14 | child: Container(
15 | alignment: Alignment.center,
16 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 48),
17 | child: _buildCounter(),
18 | ),
19 | ),
20 | );
21 |
22 | /// Renders the latest counter by watching a data file.
23 | Widget _buildCounter() => StreamBuilder(
24 | stream: counterStream(),
25 | builder: (_, snapshot) => Column(
26 | children: [
27 | RichText(
28 | textAlign: TextAlign.center,
29 | text: TextSpan(
30 | text: 'Increases the counter via a processing task\n',
31 | style: const TextStyle(
32 | color: Colors.black87,
33 | fontSize: 16,
34 | ),
35 | children: [
36 | TextSpan(
37 | text: snapshot.hasData ? '${snapshot.data}' : '',
38 | style: const TextStyle(
39 | color: Colors.blueAccent,
40 | fontSize: 48,
41 | height: 1.618,
42 | ),
43 | ),
44 | ],
45 | ),
46 | ),
47 | RaisedButton(
48 | child: const Text('Count'),
49 | onPressed: () => _increaseCounter(snapshot.data),
50 | ),
51 | ],
52 | ),
53 | );
54 |
55 | /// Submit a background task to update the counter.
56 | void _increaseCounter(int counter) {
57 | submitTaskRequest(BGProcessingTaskRequest(kTagCounterWork,
58 | input: {
59 | 'counter': counter,
60 | },
61 | ));
62 | }
63 | }
64 |
--------------------------------------------------------------------------------
/example/lib/counter.dart:
--------------------------------------------------------------------------------
1 | import 'package:flt_worker/flt_worker.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | import 'counter_file.dart';
5 |
6 | /// WorkManager api example for the Android platform.
7 | class Counter extends StatelessWidget {
8 | @override
9 | Widget build(BuildContext context) => Scaffold(
10 | appBar: AppBar(
11 | title: const Text('Counter'),
12 | ),
13 | body: SingleChildScrollView(
14 | child: Container(
15 | alignment: Alignment.center,
16 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 48),
17 | child: _buildCounter(),
18 | ),
19 | ),
20 | );
21 |
22 | /// Renders the latest counter by watching a data file.
23 | Widget _buildCounter() => StreamBuilder(
24 | stream: counterStream(),
25 | builder: (_, snapshot) => Column(
26 | children: [
27 | RichText(
28 | textAlign: TextAlign.center,
29 | text: TextSpan(
30 | text: 'Increases the counter via an unified API on both platforms.\n',
31 | style: const TextStyle(
32 | color: Colors.black87,
33 | fontSize: 16,
34 | ),
35 | children: [
36 | TextSpan(
37 | text: snapshot.hasData ? '${snapshot.data}' : '',
38 | style: const TextStyle(
39 | color: Colors.blueAccent,
40 | fontSize: 48,
41 | height: 1.618,
42 | ),
43 | ),
44 | ],
45 | ),
46 | ),
47 | RaisedButton(
48 | child: const Text('Count'),
49 | onPressed: () => _increaseCounter(snapshot.data),
50 | ),
51 | ],
52 | )
53 | );
54 |
55 | /// Enqueues a work request to update the counter.
56 | void _increaseCounter(int counter) {
57 | enqueueWorkIntent(WorkIntent(
58 | identifier: kTagCounterWork,
59 | input: {
60 | 'counter': counter,
61 | },
62 | isProcessingTask: true,
63 | ));
64 | }
65 | }
66 |
--------------------------------------------------------------------------------
/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 from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
26 |
27 | android {
28 | compileSdkVersion 28
29 |
30 | lintOptions {
31 | disable 'InvalidPackage'
32 | }
33 |
34 | defaultConfig {
35 | // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
36 | applicationId "dev.thinkng.flt_worker_example"
37 | minSdkVersion 16
38 | targetSdkVersion 28
39 | versionCode flutterVersionCode.toInteger()
40 | versionName flutterVersionName
41 | testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
42 | }
43 |
44 | buildTypes {
45 | release {
46 | // TODO: Add your own signing config for the release build.
47 | // Signing with the debug keys for now, so `flutter run --release` works.
48 | signingConfig signingConfigs.debug
49 | }
50 | }
51 | }
52 |
53 | flutter {
54 | source '../..'
55 | }
56 |
57 | dependencies {
58 | testImplementation 'junit:junit:4.12'
59 | androidTestImplementation 'androidx.test:runner:1.1.1'
60 | androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
61 | }
62 |
--------------------------------------------------------------------------------
/example/ios/Runner/Info.plist:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | BGTaskSchedulerPermittedIdentifiers
6 |
7 | com.example.task1
8 | dev.example.task2
9 | com.example.counter_task
10 | com.example.btc_prices_task
11 |
12 | CFBundleDevelopmentRegion
13 | $(DEVELOPMENT_LANGUAGE)
14 | CFBundleExecutable
15 | $(EXECUTABLE_NAME)
16 | CFBundleIdentifier
17 | $(PRODUCT_BUNDLE_IDENTIFIER)
18 | CFBundleInfoDictionaryVersion
19 | 6.0
20 | CFBundleName
21 | flt_worker_example
22 | CFBundlePackageType
23 | APPL
24 | CFBundleShortVersionString
25 | $(FLUTTER_BUILD_NAME)
26 | CFBundleSignature
27 | ????
28 | CFBundleVersion
29 | $(FLUTTER_BUILD_NUMBER)
30 | LSRequiresIPhoneOS
31 |
32 | UIBackgroundModes
33 |
34 | fetch
35 | processing
36 |
37 | UILaunchStoryboardName
38 | LaunchScreen
39 | UIMainStoryboardFile
40 | Main
41 | UISupportedInterfaceOrientations
42 |
43 | UIInterfaceOrientationPortrait
44 | UIInterfaceOrientationLandscapeLeft
45 | UIInterfaceOrientationLandscapeRight
46 |
47 | UISupportedInterfaceOrientations~ipad
48 |
49 | UIInterfaceOrientationPortrait
50 | UIInterfaceOrientationPortraitUpsideDown
51 | UIInterfaceOrientationLandscapeLeft
52 | UIInterfaceOrientationLandscapeRight
53 |
54 | UIViewControllerBasedStatusBarAppearance
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/example/lib/work_manager_counter.dart:
--------------------------------------------------------------------------------
1 | import 'package:flt_worker/android.dart';
2 | import 'package:flutter/material.dart';
3 |
4 | import 'counter_file.dart';
5 |
6 | /// WorkManager api example for the Android platform.
7 | class WorkManagerCounter extends StatelessWidget {
8 | @override
9 | Widget build(BuildContext context) => Scaffold(
10 | appBar: AppBar(
11 | title: const Text('Counter (WorkManager)'),
12 | ),
13 | body: SingleChildScrollView(
14 | child: Container(
15 | alignment: Alignment.center,
16 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 48),
17 | child: _buildCounter(),
18 | ),
19 | ),
20 | );
21 |
22 | /// Renders the latest counter by watching a data file.
23 | Widget _buildCounter() => StreamBuilder(
24 | stream: counterStream(),
25 | builder: (_, snapshot) => Column(
26 | children: [
27 | RichText(
28 | textAlign: TextAlign.center,
29 | text: TextSpan(
30 | text: 'Increases the counter via an one-off work\n',
31 | style: const TextStyle(
32 | color: Colors.black87,
33 | fontSize: 16,
34 | ),
35 | children: [
36 | TextSpan(
37 | text: snapshot.hasData ? '${snapshot.data}' : '',
38 | style: const TextStyle(
39 | color: Colors.blueAccent,
40 | fontSize: 48,
41 | height: 1.618,
42 | ),
43 | ),
44 | ],
45 | ),
46 | ),
47 | RaisedButton(
48 | child: const Text('Count'),
49 | onPressed: () => _increaseCounter(snapshot.data),
50 | ),
51 | ],
52 | )
53 | );
54 |
55 | /// Enqueues a work request to update the counter.
56 | void _increaseCounter(int counter) {
57 | enqueueWorkRequest(OneTimeWorkRequest(
58 | tags: [kTagCounterWork],
59 | constraints: WorkConstraints(
60 | networkType: NetworkType.notRequired,
61 | ),
62 | input: {
63 | 'counter': counter,
64 | },
65 | ));
66 | }
67 | }
68 |
--------------------------------------------------------------------------------
/test/ios_delegate_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flt_worker/flt_worker.dart';
2 | import 'package:flt_worker/ios.dart';
3 | import 'package:flt_worker/src/background_tasks/delegate.dart';
4 | import 'package:flutter_test/flutter_test.dart';
5 |
6 | void main() {
7 | test('tranform unified model to BackgroundTasks terms', () {
8 | var intent = WorkIntent(
9 | identifier: 'work1',
10 | );
11 | var req = parseWorkIntent(intent);
12 | expect(req, isA());
13 | expect(req.identifier, 'work1');
14 | expect(req.earliestBeginDate, null);
15 | expect(req.input, null);
16 |
17 | intent = WorkIntent(
18 | identifier: 'work2',
19 | isProcessingTask: true,
20 | );
21 | req = parseWorkIntent(intent);
22 | expect(req, isA());
23 | expect(req.identifier, 'work2');
24 | expect((req as BGProcessingTaskRequest).requiresNetworkConnectivity, isNull);
25 | expect((req as BGProcessingTaskRequest).requiresExternalPower, isNull);
26 | });
27 |
28 | test('parse BGProcessingTaskRequest with constraints', () {
29 | final now = DateTime.now();
30 | var intent = WorkIntent(
31 | identifier: 'work1',
32 | initialDelay: Duration(minutes: 11),
33 | isProcessingTask: true,
34 | constraints: WorkConstraints(
35 | networkType: NetworkType.notRoaming,
36 | charging: true,
37 | ),
38 | );
39 | var req = parseWorkIntent(intent) as BGProcessingTaskRequest;
40 | expect(
41 | (req.earliestBeginDate.millisecondsSinceEpoch - now.add(Duration(minutes: 11)).millisecondsSinceEpoch).abs(),
42 | lessThanOrEqualTo(1000));
43 | expect(req.requiresNetworkConnectivity, isTrue);
44 | expect(req.requiresExternalPower, isTrue);
45 |
46 | intent = WorkIntent(
47 | identifier: 'work1',
48 | isProcessingTask: true,
49 | constraints: WorkConstraints(
50 | networkType: NetworkType.notRequired,
51 | charging: false,
52 | ),
53 | );
54 | req = parseWorkIntent(intent) as BGProcessingTaskRequest;
55 | expect(req.requiresNetworkConnectivity, isFalse);
56 | expect(req.requiresExternalPower, isFalse);
57 | });
58 | }
59 |
--------------------------------------------------------------------------------
/example/pubspec.yaml:
--------------------------------------------------------------------------------
1 | name: flt_worker_example
2 | description: Demonstrates how to use the flt_worker plugin.
3 | publish_to: 'none'
4 | version: 1.0.0+1
5 |
6 | environment:
7 | sdk: ">=2.3.0 <3.0.0"
8 |
9 | dependencies:
10 | flutter:
11 | sdk: flutter
12 |
13 | # The following adds the Cupertino Icons font to your application.
14 | # Use with the CupertinoIcons class for iOS style icons.
15 | cupertino_icons: ^0.1.2
16 |
17 | path_provider: ^1.6.5
18 | watcher: ^0.9.7+14
19 | http: ^0.12.0+4
20 | intl: ^0.16.1
21 |
22 | dev_dependencies:
23 | flutter_test:
24 | sdk: flutter
25 |
26 | flt_worker:
27 | path: ../
28 |
29 | # For information on the generic Dart part of this file, see the
30 | # following page: https://dart.dev/tools/pub/pubspec
31 |
32 | # The following section is specific to Flutter.
33 | flutter:
34 |
35 | # The following line ensures that the Material Icons font is
36 | # included with your application, so that you can use the icons in
37 | # the material Icons class.
38 | uses-material-design: true
39 |
40 | # To add assets to your application, add an assets section, like this:
41 | # assets:
42 | # - images/a_dot_burr.jpeg
43 | # - images/a_dot_ham.jpeg
44 |
45 | # An image asset can refer to one or more resolution-specific "variants", see
46 | # https://flutter.dev/assets-and-images/#resolution-aware.
47 |
48 | # For details regarding adding assets from package dependencies, see
49 | # https://flutter.dev/assets-and-images/#from-packages
50 |
51 | # To add custom fonts to your application, add a fonts section here,
52 | # in this "flutter" section. Each entry in this list should have a
53 | # "family" key with the font family name, and a "fonts" key with a
54 | # list giving the asset and other descriptors for the font. For
55 | # example:
56 | # fonts:
57 | # - family: Schyler
58 | # fonts:
59 | # - asset: fonts/Schyler-Regular.ttf
60 | # - asset: fonts/Schyler-Italic.ttf
61 | # style: italic
62 | # - family: Trajan Pro
63 | # fonts:
64 | # - asset: fonts/TrajanPro.ttf
65 | # - asset: fonts/TrajanPro_Bold.ttf
66 | # weight: 700
67 | #
68 | # For details regarding fonts from package dependencies,
69 | # see https://flutter.dev/custom-fonts/#from-packages
70 |
--------------------------------------------------------------------------------
/example/README.md:
--------------------------------------------------------------------------------
1 | # flt_worker_example
2 |
3 | Demonstrates how to use the flt_worker plugin.
4 |
5 | ## Examples
6 |
7 | High-level API examples:
8 | - [counter.dart]: Counter demo the worker way
9 | - [btc_prices.dart]: refreshing Bitcoin price periodically in the background
10 | - [worker.dart]: the worker dispather example
11 |
12 | Android low-level API examples:
13 | - [work_manager_counter.dart]: Counter using an `OneTimeWorkRequest`
14 | - [work_manager_btc_prices.dart]: refreshing Bitcoin price periodically using a `PeriodicWorkRequest`
15 |
16 | iOS low-level API examples:
17 | - [background_tasks_counter.dart]: Counter using a `BGProcessingTaskRequest`
18 | - [background_tasks_btc_prices.dart]: refreshing Bitcoin price periodically using `BGAppRefreshTaskRequest`s
19 |
20 | ## Debugging on iOS
21 |
22 | For debugging your worker on an iOS device, you may want to force launch a `BGTaskRequest`.
23 |
24 | Please follow these steps:
25 | 1. Run your app using Xcode
26 | 2. Set a breakpoint at the last line of the `handleMethodCall` method in `flt_worker/ios/Classes/FltWorkerPlugin.m`
27 | 3. When the app pauses (after submission of a `BGTaskRequest`), execute the following line in the debugger, substituting your task identifier for `TASK_IDENTIFIER`, and resume the app.
28 |
29 | ```
30 | e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"TASK_IDENTIFIER"]
31 | ```
32 |
33 | Please find more details here: [Starting and Terminating Tasks During Development].
34 |
35 |
36 | [worker.dart]: https://github.com/xinthink/flt_worker/blob/master/example/lib/worker.dart
37 | [counter.dart]: https://github.com/xinthink/flt_worker/blob/master/example/lib/counter.dart
38 | [btc_prices.dart]: https://github.com/xinthink/flt_worker/blob/master/example/lib/btc_prices.dart
39 | [background_tasks_btc_prices.dart]: https://github.com/xinthink/flt_worker/blob/master/example/lib/background_tasks_btc_prices.dart
40 | [background_tasks_counter.dart]: https://github.com/xinthink/flt_worker/blob/master/example/lib/background_tasks_counter.dart
41 | [work_manager_btc_prices.dart]: https://github.com/xinthink/flt_worker/blob/master/example/lib/work_manager_btc_prices.dart
42 | [work_manager_counter.dart]: https://github.com/xinthink/flt_worker/blob/master/example/lib/work_manager_counter.dart
43 | [Starting and Terminating Tasks During Development]: https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development
44 |
--------------------------------------------------------------------------------
/example/ios/Runner/Base.lproj/LaunchScreen.storyboard:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
--------------------------------------------------------------------------------
/example/android/app/src/main/AndroidManifest.xml:
--------------------------------------------------------------------------------
1 |
3 |
8 |
12 |
19 |
23 |
27 |
32 |
36 |
37 |
38 |
39 |
40 |
41 |
43 |
46 |
47 |
48 |
--------------------------------------------------------------------------------
/android/src/main/java/dev/thinkng/flt_worker/internal/MethodCallFuture.java:
--------------------------------------------------------------------------------
1 | package dev.thinkng.flt_worker.internal;
2 |
3 | import androidx.annotation.Nullable;
4 | import androidx.annotation.UiThread;
5 |
6 | import java.util.concurrent.ExecutionException;
7 | import java.util.concurrent.Future;
8 | import java.util.concurrent.TimeUnit;
9 | import java.util.concurrent.TimeoutException;
10 |
11 | import io.flutter.plugin.common.MethodChannel;
12 |
13 | @SuppressWarnings("unchecked")
14 | public class MethodCallFuture implements MethodChannel.Result, Future {
15 | private final byte[] lock = new byte[0];
16 | private volatile boolean isComplete;
17 | private volatile Object result;
18 | private volatile Exception error;
19 |
20 | @UiThread
21 | @Override
22 | public void success(Object o) {
23 | synchronized (lock) {
24 | result = o;
25 | isComplete = true;
26 | lock.notifyAll();
27 | }
28 | }
29 |
30 | @UiThread
31 | @Override
32 | public void error(String errorCode, String errorMessage, Object errorDetails) {
33 | synchronized (lock) {
34 | error = new RuntimeException(error + " " + errorMessage + (errorDetails != null ? " " + errorDetails : ""));
35 | isComplete = true;
36 | lock.notifyAll();
37 | }
38 | }
39 |
40 | @UiThread
41 | @Override
42 | public void notImplemented() {
43 | synchronized (lock) {
44 | isComplete = true;
45 | lock.notifyAll();
46 | }
47 | }
48 |
49 | @Override
50 | public boolean cancel(boolean b) {
51 | return false;
52 | }
53 |
54 | @Override
55 | public boolean isCancelled() {
56 | return false;
57 | }
58 |
59 | @Override
60 | public boolean isDone() {
61 | return isComplete;
62 | }
63 |
64 | @Nullable
65 | @Override
66 | public T get() throws ExecutionException, InterruptedException {
67 | synchronized (lock) {
68 | while (!isComplete) {
69 | lock.wait();
70 | }
71 |
72 | if (error != null) {
73 | throw new ExecutionException("", error);
74 | }
75 | return (T) result;
76 | }
77 | }
78 |
79 | @Nullable
80 | @Override
81 | public T get(long timeout, TimeUnit timeUnit) throws ExecutionException, InterruptedException, TimeoutException {
82 | synchronized (lock) {
83 | long timeoutMillis = timeUnit.toMillis(timeout);
84 | long t = System.currentTimeMillis();
85 | while (!isComplete) {
86 | lock.wait(timeoutMillis);
87 | if (System.currentTimeMillis() - t >= timeoutMillis) {
88 | throw new TimeoutException("timed out waiting for the result to complete");
89 | }
90 | }
91 |
92 | if (error != null) {
93 | throw new ExecutionException("", error);
94 | }
95 | return (T) result;
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/test/android_delegate_test.dart:
--------------------------------------------------------------------------------
1 | import 'package:flt_worker/android.dart';
2 | import 'package:flt_worker/flt_worker.dart';
3 | import 'package:flt_worker/src/work_manager/delegate.dart';
4 | import 'package:flutter_test/flutter_test.dart';
5 |
6 | void main() {
7 | test('tranform unified model to WorkManager terms', () {
8 | var intent = WorkIntent(
9 | identifier: 'work1',
10 | );
11 | var req = parseWorkIntent(intent);
12 | expect(req, isA());
13 | expect(req.tags, ['work1']);
14 | expect(req.initialDelay, null);
15 | expect(req.input, null);
16 | expect(req.constraints, null);
17 | expect(req.backoffCriteria, null);
18 |
19 | intent = WorkIntent(
20 | identifier: 'work2',
21 | tags: ['periodic'],
22 | repeatInterval: Duration(hours: 4),
23 | );
24 | req = parseWorkIntent(intent);
25 | expect(req, isA());
26 | expect(req.tags, ['work2', 'periodic']);
27 | expect((req as PeriodicWorkRequest).repeatInterval, Duration(hours: 4));
28 |
29 | intent = WorkIntent(
30 | identifier: 'work2',
31 | tags: ['periodic'],
32 | repeatInterval: Duration(hours: 4),
33 | flexInterval: Duration(minutes: 1),
34 | );
35 | req = parseWorkIntent(intent);
36 | expect(req, isA());
37 | expect(req.tags, ['work2', 'periodic']);
38 | expect((req as PeriodicWorkRequest).repeatInterval, Duration(hours: 4));
39 | expect((req as PeriodicWorkRequest).flexInterval, Duration(minutes: 1));
40 | });
41 |
42 | test('model tranformation with constraints', () {
43 | // default constraints (empty)
44 | var intent = WorkIntent(
45 | identifier: 'work1',
46 | initialDelay: Duration(seconds: 59),
47 | constraints: WorkConstraints(),
48 | );
49 | var req = parseWorkIntent(intent);
50 | var constraints = req.constraints;
51 | expect(req.initialDelay, Duration(seconds: 59));
52 | expect(constraints, isNotNull);
53 | expect(constraints.batteryNotLow, isNull);
54 | expect(constraints.charging, isNull);
55 | expect(constraints.deviceIdle, isNull);
56 | expect(constraints.networkType, isNull);
57 | expect(constraints.storageNotLow, isNull);
58 |
59 | intent = WorkIntent(
60 | identifier: 'work1',
61 | constraints: WorkConstraints(
62 | batteryNotLow: true,
63 | charging: false,
64 | deviceIdle: true,
65 | networkType: NetworkType.metered,
66 | storageNotLow: false,
67 | ),
68 | );
69 | req = parseWorkIntent(intent);
70 | constraints = req.constraints;
71 | expect(constraints.batteryNotLow, isTrue);
72 | expect(constraints.charging, isFalse);
73 | expect(constraints.deviceIdle, isTrue);
74 | expect(constraints.networkType, NetworkType.metered);
75 | expect(constraints.storageNotLow, isFalse);
76 | });
77 | }
78 |
--------------------------------------------------------------------------------
/lib/src/background_tasks/models.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/foundation.dart';
4 |
5 | /// An abstract class for representing task requests.
6 | @immutable
7 | abstract class BGTaskRequest {
8 | /// The identifier of the task associated with the request.
9 | final String identifier;
10 |
11 | /// The earliest date and time at which to run the task.
12 | ///
13 | /// Specify `null` for no start delay.
14 | ///
15 | /// Setting the property indicates that the background task shouldn’t start any earlier than this date.
16 | /// However, the system doesn't guarantee launching the task at the specified date, but only that it won’t begin sooner.
17 | final DateTime earliestBeginDate;
18 |
19 | /// Input data of the task.
20 | final Map input;
21 |
22 | /// Initializes a [BGTaskRequest] instance with the given [identifier].
23 | ///
24 | /// Optional properties:
25 | /// - [earliestBeginDate]
26 | /// - [input]
27 | const BGTaskRequest(this.identifier, {
28 | this.earliestBeginDate,
29 | this.input,
30 | });
31 |
32 | Map toJson() => {
33 | 'type': (this is BGAppRefreshTaskRequest) ? 'AppRefresh' : 'Processing',
34 | 'identifier': identifier,
35 | 'earliestBeginDate': earliestBeginDate?.millisecondsSinceEpoch,
36 | 'input': jsonEncode(input ?? {}),
37 | };
38 | }
39 |
40 | /// A request to launch your app in the background to execute a short refresh task.
41 | @immutable
42 | class BGAppRefreshTaskRequest extends BGTaskRequest {
43 | /// Instantiates a [BGAppRefreshTaskRequest] with the task [identifier]
44 | /// and an optional [earliestBeginDate].
45 | const BGAppRefreshTaskRequest(String identifier, {
46 | DateTime earliestBeginDate,
47 | Map input,
48 | }) : super(identifier, earliestBeginDate: earliestBeginDate, input: input);
49 | }
50 |
51 | /// A request to launch your app in the background to execute a processing task that can take minutes to complete.
52 | @immutable
53 | class BGProcessingTaskRequest extends BGTaskRequest {
54 | /// Specifies if the processing task requires a device connected to power.
55 | final bool requiresExternalPower;
56 |
57 | /// Specifies if the processing task requires network connectivity.
58 | final bool requiresNetworkConnectivity;
59 |
60 | /// Instantiates a [BGProcessingTaskRequest] with the task [identifier].
61 | ///
62 | /// and an optional [earliestBeginDate].
63 | const BGProcessingTaskRequest(String identifier, {
64 | DateTime earliestBeginDate,
65 | Map input,
66 | this.requiresExternalPower,
67 | this.requiresNetworkConnectivity,
68 | }) : super(identifier, earliestBeginDate: earliestBeginDate, input: input);
69 |
70 | @override
71 | Map toJson() {
72 | final json = super.toJson();
73 | json['requiresExternalPower'] = requiresExternalPower;
74 | json['requiresNetworkConnectivity'] = requiresNetworkConnectivity;
75 | return json;
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/ios/Classes/FltWorkerPlugin.m:
--------------------------------------------------------------------------------
1 | #import "FltWorkerPlugin.h"
2 | #import "BGTaskMgrDelegate.h"
3 | #import "BGTaskHandler.h"
4 |
5 | @implementation FltWorkerPlugin {
6 | BGTaskMgrDelegate *_delegate;
7 | // FlutterEngine *_headlessEngine;
8 | // FlutterMethodChannel *_callbackChannel;
9 | // BOOL _isHeadlessEnginRegistered;
10 | // NSUserDefaults *_userDefaults;
11 | // NSDictionary *_workers;
12 | }
13 |
14 | static FltWorkerPlugin *instance = nil;
15 |
16 | + (void)registerWithRegistrar:(NSObject*)registrar {
17 | @synchronized (self) {
18 | if (instance == nil) {
19 | instance = [[FltWorkerPlugin alloc] initWithRegistrar:registrar];
20 | // [registrar addApplicationDelegate:instance];
21 | }
22 | }
23 |
24 | // channel for api calls should be registerd for both instances of engine,
25 | // so that it's available in the headless isolate
26 | // FlutterMethodChannel *channel = [FlutterMethodChannel methodChannelWithName:@PLUGIN_PKG
27 | // binaryMessenger:[registrar messenger]];
28 | // [registrar addMethodCallDelegate:instance channel:channel];
29 | }
30 |
31 | + (FuncRegisterPlugins) registerPlugins {
32 | return BGTaskHandler.registerPlugins;
33 | }
34 |
35 | + (void) setRegisterPlugins:(FuncRegisterPlugins)registerPlugins {
36 | BGTaskHandler.registerPlugins = registerPlugins;
37 | }
38 |
39 | - (instancetype)initWithRegistrar:(NSObject*)registrar {
40 | self = [super init];
41 | if (self) {
42 | _delegate = [[BGTaskMgrDelegate alloc] initWithRegistrar:registrar];
43 | [registrar addMethodCallDelegate:self channel:_delegate.methodChannel];
44 | // _userDefaults = [[NSUserDefaults alloc] init];
45 |
46 | // init a headless engine instance for callback
47 | // _headlessEngine = [[FlutterEngine alloc] initWithName:@"flt_worker_isolate"
48 | // project:nil
49 | // allowHeadlessExecution:YES];
50 | //
51 | // // channel for callbacks
52 | // FlutterMethodChannel *callbackChannel = [FlutterMethodChannel methodChannelWithName:@PLUGIN_PKG"/callback"
53 | // binaryMessenger:[_headlessEngine binaryMessenger]];
54 |
55 | // register BGTask identifiers
56 | [BGTaskMgrDelegate registerBGTaskHandler];
57 | }
58 | return self;
59 | }
60 |
61 | - (void)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
62 | // NSLog(@"--- handling method call: %@ args=%@", call.method, call.arguments);
63 | NSString *method = call.method;
64 | NSArray *args = call.arguments;
65 | if ([@API_METHOD(initialize) isEqualToString:method]) {
66 | [_delegate saveHandles:args];
67 | result(nil);
68 | } else if ([@API_METHOD(test) isEqualToString:method]) {
69 | // [BGTaskHandler.instance handleBGTask:nil];
70 | result(nil);
71 | } else if (![_delegate handleMethodCall:call result:result]) {
72 | result(FlutterMethodNotImplemented);
73 | }
74 | }
75 |
76 | @end
77 |
--------------------------------------------------------------------------------
/lib/src/constraints.dart:
--------------------------------------------------------------------------------
1 | import 'package:flutter/foundation.dart';
2 |
3 | /// Constraints for a [WorkIntent].
4 | @immutable
5 | class WorkConstraints {
6 | /// Whether the work requires a particular [NetworkType] to run.
7 | ///
8 | /// The default value is dependent on the native `WorkManager`,
9 | /// which should be [NetworkType.notRequired] according to
10 | /// the [documentation](https://developer.android.com/reference/androidx/work/Constraints.Builder?hl=en#setRequiredNetworkType(androidx.work.NetworkType)).
11 | final NetworkType networkType;
12 |
13 | /// Whether device battery should be at an acceptable level for the work to run.
14 | ///
15 | /// The default value is dependent on the native `WorkManager`,
16 | /// which should be `false` according to
17 | /// the [documentation](https://developer.android.com/reference/androidx/work/Constraints.Builder?hl=en#setRequiresBatteryNotLow(boolean)).
18 | final bool batteryNotLow;
19 |
20 | /// Whether device should be charging for the work to run.
21 | ///
22 | /// The default value is dependent on the native `WorkManager`,
23 | /// which should be `false` according to
24 | /// the [documentation](https://developer.android.com/reference/androidx/work/Constraints.Builder?hl=en#setRequiresCharging(boolean)).
25 | final bool charging;
26 |
27 | /// Whether device should be idle for the work to run.
28 | ///
29 | /// Requires Android SDK level 23+.
30 | /// The default value is dependent on the native `WorkManager`,
31 | /// which should be `false` according to
32 | /// the [documentation](https://developer.android.com/reference/androidx/work/Constraints.Builder?hl=en#setRequiresDeviceIdle(boolean)).
33 | final bool deviceIdle;
34 |
35 | /// Whether the work requires device's storage should be at an acceptable level.
36 | ///
37 | /// The default value is dependent on the native `WorkManager`,
38 | /// which should be `false` according to
39 | /// the [documentation](https://developer.android.com/reference/androidx/work/Constraints.Builder?hl=en#setRequiresStorageNotLow(boolean)).
40 | final bool storageNotLow;
41 |
42 | /// Creates constraints for a [WorkRequest].
43 | const WorkConstraints({
44 | this.networkType,
45 | this.batteryNotLow,
46 | this.charging,
47 | this.deviceIdle,
48 | this.storageNotLow,
49 | });
50 |
51 | /// Serializes this constraints into a json object.
52 | Map toJson() => {
53 | 'networkType': networkType?.index,
54 | 'batteryNotLow': batteryNotLow,
55 | 'charging': charging,
56 | 'deviceIdle': deviceIdle,
57 | 'storageNotLow': storageNotLow,
58 | };
59 | }
60 |
61 | /// An enumeration of various network types that can be used as [WorkConstraints].
62 | enum NetworkType {
63 | /// Any working network connection is required for this work.
64 | connected,
65 |
66 | /// A metered network connection is required for this work.
67 | metered,
68 |
69 | /// A network is not required for this work.
70 | notRequired,
71 |
72 | /// A non-roaming network connection is required for this work.
73 | notRoaming,
74 |
75 | /// An unmetered network connection is required for this work.
76 | unmetered,
77 | }
78 |
--------------------------------------------------------------------------------
/example/ios/Runner/Assets.xcassets/AppIcon.appiconset/Contents.json:
--------------------------------------------------------------------------------
1 | {
2 | "images" : [
3 | {
4 | "size" : "20x20",
5 | "idiom" : "iphone",
6 | "filename" : "Icon-App-20x20@2x.png",
7 | "scale" : "2x"
8 | },
9 | {
10 | "size" : "20x20",
11 | "idiom" : "iphone",
12 | "filename" : "Icon-App-20x20@3x.png",
13 | "scale" : "3x"
14 | },
15 | {
16 | "size" : "29x29",
17 | "idiom" : "iphone",
18 | "filename" : "Icon-App-29x29@1x.png",
19 | "scale" : "1x"
20 | },
21 | {
22 | "size" : "29x29",
23 | "idiom" : "iphone",
24 | "filename" : "Icon-App-29x29@2x.png",
25 | "scale" : "2x"
26 | },
27 | {
28 | "size" : "29x29",
29 | "idiom" : "iphone",
30 | "filename" : "Icon-App-29x29@3x.png",
31 | "scale" : "3x"
32 | },
33 | {
34 | "size" : "40x40",
35 | "idiom" : "iphone",
36 | "filename" : "Icon-App-40x40@2x.png",
37 | "scale" : "2x"
38 | },
39 | {
40 | "size" : "40x40",
41 | "idiom" : "iphone",
42 | "filename" : "Icon-App-40x40@3x.png",
43 | "scale" : "3x"
44 | },
45 | {
46 | "size" : "60x60",
47 | "idiom" : "iphone",
48 | "filename" : "Icon-App-60x60@2x.png",
49 | "scale" : "2x"
50 | },
51 | {
52 | "size" : "60x60",
53 | "idiom" : "iphone",
54 | "filename" : "Icon-App-60x60@3x.png",
55 | "scale" : "3x"
56 | },
57 | {
58 | "size" : "20x20",
59 | "idiom" : "ipad",
60 | "filename" : "Icon-App-20x20@1x.png",
61 | "scale" : "1x"
62 | },
63 | {
64 | "size" : "20x20",
65 | "idiom" : "ipad",
66 | "filename" : "Icon-App-20x20@2x.png",
67 | "scale" : "2x"
68 | },
69 | {
70 | "size" : "29x29",
71 | "idiom" : "ipad",
72 | "filename" : "Icon-App-29x29@1x.png",
73 | "scale" : "1x"
74 | },
75 | {
76 | "size" : "29x29",
77 | "idiom" : "ipad",
78 | "filename" : "Icon-App-29x29@2x.png",
79 | "scale" : "2x"
80 | },
81 | {
82 | "size" : "40x40",
83 | "idiom" : "ipad",
84 | "filename" : "Icon-App-40x40@1x.png",
85 | "scale" : "1x"
86 | },
87 | {
88 | "size" : "40x40",
89 | "idiom" : "ipad",
90 | "filename" : "Icon-App-40x40@2x.png",
91 | "scale" : "2x"
92 | },
93 | {
94 | "size" : "76x76",
95 | "idiom" : "ipad",
96 | "filename" : "Icon-App-76x76@1x.png",
97 | "scale" : "1x"
98 | },
99 | {
100 | "size" : "76x76",
101 | "idiom" : "ipad",
102 | "filename" : "Icon-App-76x76@2x.png",
103 | "scale" : "2x"
104 | },
105 | {
106 | "size" : "83.5x83.5",
107 | "idiom" : "ipad",
108 | "filename" : "Icon-App-83.5x83.5@2x.png",
109 | "scale" : "2x"
110 | },
111 | {
112 | "size" : "1024x1024",
113 | "idiom" : "ios-marketing",
114 | "filename" : "Icon-App-1024x1024@1x.png",
115 | "scale" : "1x"
116 | }
117 | ],
118 | "info" : {
119 | "version" : 1,
120 | "author" : "xcode"
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/example/lib/background_tasks_btc_prices.dart:
--------------------------------------------------------------------------------
1 | import 'package:flt_worker/ios.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:intl/intl.dart';
4 |
5 | import 'btc_price_file.dart';
6 |
7 | /// An example for using low level `WorkManager` api on the Android platform,
8 | /// which polls Bitcoin price periodically every 900 seconds.
9 | class BackgroundTasksBtcPrices extends StatefulWidget {
10 | @override
11 | State createState() => _BtcPricesState();
12 | }
13 |
14 | class _BtcPricesState extends State {
15 | @override
16 | void initState() {
17 | super.initState();
18 | _startPolling();
19 | }
20 |
21 | @override
22 | void dispose() {
23 | // Comments out this line to keep the work running in background
24 | cancelTaskRequest(kTagBtcPricesWork);
25 | super.dispose();
26 | }
27 |
28 | @override
29 | Widget build(BuildContext context) => Scaffold(
30 | appBar: AppBar(
31 | title: const Text('Bitcoin Price (BackgroundTasks)'),
32 | ),
33 | body: SingleChildScrollView(
34 | child: Container(
35 | alignment: Alignment.center,
36 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 48),
37 | child: _buildDashboard(),
38 | ),
39 | ),
40 | );
41 |
42 | /// Renders the latest Bitcoin price by watching a data file.
43 | Widget _buildDashboard() => StreamBuilder(
44 | stream: btcPriceStream(),
45 | builder: (_, snapshot) => Column(
46 | children: [
47 | RichText(
48 | textAlign: TextAlign.center,
49 | text: TextSpan(
50 | text: '₿1',
51 | style: TextStyle(
52 | color: Colors.lime.shade700,
53 | fontSize: 56,
54 | height: 1.618,
55 | ),
56 | children: [
57 | TextSpan(
58 | text: '\n=\n',
59 | style: const TextStyle(
60 | color: Colors.black45,
61 | fontSize: 36,
62 | height: null,
63 | ),
64 | ),
65 | TextSpan(
66 | text: snapshot.hasData
67 | ? NumberFormat.currency(symbol: '\$', decimalDigits: 2)
68 | .format(snapshot.data['amount'])
69 | : '',
70 | style: const TextStyle(
71 | color: Colors.blueAccent,
72 | ),
73 | ),
74 | TextSpan(
75 | text: snapshot.hasData
76 | ? '\nUpdated at: ${DateFormat('hh:mm a, yyyy MMM dd')
77 | .format(DateTime.fromMillisecondsSinceEpoch(snapshot.data['time']))}'
78 | : '',
79 | style: const TextStyle(
80 | color: Colors.black38,
81 | fontSize: 14,
82 | height: null,
83 | ),
84 | ),
85 | ],
86 | ),
87 | ),
88 | ],
89 | ),
90 | );
91 |
92 | /// Enqueues a work request to poll the price.
93 | void _startPolling() async {
94 | await cancelTaskRequest(kTagBtcPricesWork); // cancel the previous task
95 | await submitTaskRequest(BGAppRefreshTaskRequest(kTagBtcPricesWork));
96 | }
97 | }
98 |
--------------------------------------------------------------------------------
/lib/flt_worker.dart:
--------------------------------------------------------------------------------
1 | /// A unified and simplified API for scheduling general background tasks.
2 | ///
3 | /// The background tasks scheduler is based on
4 | /// the [BackgroundTasks](https://developer.apple.com/documentation/backgroundtasks) framework on iOS 13+,
5 | /// and the [WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager) APIs
6 | /// on Android.
7 | ///
8 | /// For more complex tasks, you may want to use the flatform-specific low-level
9 | /// [work_manager] and [background_tasks] APIs for Android and iOS devices respectively.
10 | library flt_worker;
11 |
12 | import 'dart:async';
13 |
14 | import 'src/callback_dispatcher.dart';
15 | import 'src/functions.dart' as impl;
16 | import 'src/models.dart';
17 | import 'src/utils.dart';
18 |
19 | export 'src/models.dart';
20 |
21 | /// Initializes the plugin by registering a [worker] callback.
22 | ///
23 | /// All background work will be dispatched to the [worker] function,
24 | /// which will run in a headless isolate.
25 | ///
26 | /// You may assign different tasks to other functions according to the [input][WorkPayload] of each work.
27 | /// For example:
28 | /// ```
29 | /// Future worker(WorkPayload payload) {
30 | /// final id = payload.tags.first;
31 | /// switch (id) {
32 | /// case 'task1':
33 | /// return onTask1();
34 | /// default:
35 | /// return Future.value();
36 | /// }
37 | /// }
38 | ///
39 | /// ...
40 | /// initializeWorker(worker);
41 | /// ```
42 | Future initializeWorker(WorkerFn worker) =>
43 | apiChannel.invokeMethod(
44 | '$METHOD_PREFIX#initialize',
45 | [
46 | ensureRawHandle(callbackDispatcher),
47 | ensureRawHandle(worker),
48 | ]);
49 |
50 | /// Enqueues a [intent] to work in the background.
51 | ///
52 | /// You can specify input data and constraints like network or battery status
53 | /// to the background work via the [WorkIntent].
54 | ///
55 | /// Example:
56 | /// ```
57 | /// enqueueWorkIntent(WorkIntent(
58 | /// identifier: 'task1',
59 | /// initialDelay: Duration(seconds: 59),
60 | /// constraints: WorkConstraints(
61 | /// networkType: NetworkType.connected,
62 | /// batteryNotLow: true,
63 | /// ),
64 | /// input: {
65 | /// 'counter': counter,
66 | /// },
67 | /// ));
68 | /// ```
69 | ///
70 | /// For the iOS platform, all `identifier`s must be registered in the `Info.plist` file,
71 | /// please see the [integration guide](https://github.com/xinthink/flt_worker#integration) for more details.
72 | ///
73 | /// The `identifier` will always be prepended to the `tags` properties,
74 | /// which you can retrieve from the `WorkPayload` when handling the work later.
75 | Future enqueueWorkIntent(WorkIntent intent) => impl.enqueueWorkIntent(intent);
76 |
77 | /// Cancels all unfinished work with the given [identifier].
78 | ///
79 | /// Note that cancellation is a best-effort policy and work that is already executing may continue to run.
80 | Future cancelWork(String identifier) => impl.cancelWork(identifier);
81 |
82 | /// Cancels all unfinished work.
83 | ///
84 | /// **Use this method with extreme caution!**
85 | /// By invoking it, you will potentially affect other modules or libraries in your codebase.
86 | /// It is strongly recommended that you use one of the other cancellation methods at your disposal.
87 | Future cancelAllWork() => impl.cancelAllWork();
88 |
--------------------------------------------------------------------------------
/example/lib/work_manager_btc_prices.dart:
--------------------------------------------------------------------------------
1 | import 'package:flt_worker/android.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:intl/intl.dart';
4 |
5 | import 'btc_price_file.dart';
6 |
7 | /// An example for using low level `WorkManager` api on the Android platform,
8 | /// which polls Bitcoin price periodically every 900 seconds.
9 | class WorkManagerBtcPrices extends StatefulWidget {
10 | @override
11 | State createState() => _BtcPricesState();
12 | }
13 |
14 | class _BtcPricesState extends State {
15 | @override
16 | void initState() {
17 | super.initState();
18 | _startPolling();
19 | }
20 |
21 | @override
22 | void dispose() {
23 | // Comments out this line to keep the work running in background
24 | cancelAllWorkByTag(kTagBtcPricesWork);
25 | super.dispose();
26 | }
27 |
28 | @override
29 | Widget build(BuildContext context) => Scaffold(
30 | appBar: AppBar(
31 | title: const Text('Bitcoin Price (WorkManager)'),
32 | ),
33 | body: SingleChildScrollView(
34 | child: Container(
35 | alignment: Alignment.center,
36 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 48),
37 | child: _buildDashboard(),
38 | ),
39 | ),
40 | );
41 |
42 | /// Renders the latest Bitcoin price by watching a data file.
43 | Widget _buildDashboard() => StreamBuilder(
44 | stream: btcPriceStream(),
45 | builder: (_, snapshot) => Column(
46 | children: [
47 | RichText(
48 | textAlign: TextAlign.center,
49 | text: TextSpan(
50 | text: '₿1',
51 | style: TextStyle(
52 | color: Colors.lime.shade700,
53 | fontSize: 56,
54 | height: 1.618,
55 | ),
56 | children: [
57 | TextSpan(
58 | text: '\n=\n',
59 | style: const TextStyle(
60 | color: Colors.black45,
61 | fontSize: 36,
62 | height: null,
63 | ),
64 | ),
65 | TextSpan(
66 | text: snapshot.hasData
67 | ? NumberFormat.currency(symbol: '\$', decimalDigits: 2)
68 | .format(snapshot.data['amount'])
69 | : '',
70 | style: const TextStyle(
71 | color: Colors.blueAccent,
72 | ),
73 | ),
74 | TextSpan(
75 | text: snapshot.hasData
76 | ? '\nUpdated at: ${DateFormat('hh:mm a, yyyy MMM dd')
77 | .format(DateTime.fromMillisecondsSinceEpoch(snapshot.data['time']))}'
78 | : '',
79 | style: const TextStyle(
80 | color: Colors.black38,
81 | fontSize: 14,
82 | height: null,
83 | ),
84 | ),
85 | ],
86 | ),
87 | ),
88 | ],
89 | ),
90 | );
91 |
92 | /// Enqueues a work request to poll the price.
93 | void _startPolling() async {
94 | await cancelAllWorkByTag(kTagBtcPricesWork); // cancel the previous work
95 | await enqueueWorkRequest(const PeriodicWorkRequest(
96 | repeatInterval: Duration(seconds: 30),
97 | tags: [kTagBtcPricesWork],
98 | constraints: WorkConstraints(
99 | networkType: NetworkType.connected,
100 | ),
101 | ));
102 | }
103 | }
104 |
--------------------------------------------------------------------------------
/example/lib/btc_prices.dart:
--------------------------------------------------------------------------------
1 | import 'package:flt_worker/flt_worker.dart';
2 | import 'package:flutter/material.dart';
3 | import 'package:intl/intl.dart';
4 |
5 | import 'btc_price_file.dart';
6 |
7 | /// An example for using low level `WorkManager` api on the Android platform,
8 | /// which polls Bitcoin price periodically every 900 seconds.
9 | class BtcPrices extends StatefulWidget {
10 | @override
11 | State createState() => _BtcPricesState();
12 | }
13 |
14 | class _BtcPricesState extends State {
15 | @override
16 | void initState() {
17 | super.initState();
18 | _startPolling();
19 | }
20 |
21 | @override
22 | void dispose() {
23 | // Comments out this line to keep the work running in background
24 | cancelWork(kTagBtcPricesWork);
25 | super.dispose();
26 | }
27 |
28 | @override
29 | Widget build(BuildContext context) => Scaffold(
30 | appBar: AppBar(
31 | title: const Text('Bitcoin Price'),
32 | ),
33 | body: SingleChildScrollView(
34 | child: Container(
35 | alignment: Alignment.center,
36 | padding: const EdgeInsets.symmetric(horizontal: 32, vertical: 48),
37 | child: _buildDashboard(),
38 | ),
39 | ),
40 | );
41 |
42 | /// Renders the latest Bitcoin price by watching a data file.
43 | Widget _buildDashboard() => StreamBuilder(
44 | stream: btcPriceStream(),
45 | builder: (_, snapshot) => Column(
46 | children: [
47 | RichText(
48 | textAlign: TextAlign.center,
49 | text: TextSpan(
50 | text: '₿1',
51 | style: TextStyle(
52 | color: Colors.lime.shade700,
53 | fontSize: 56,
54 | height: 1.618,
55 | ),
56 | children: [
57 | TextSpan(
58 | text: '\n=\n',
59 | style: const TextStyle(
60 | color: Colors.black45,
61 | fontSize: 36,
62 | height: null,
63 | ),
64 | ),
65 | TextSpan(
66 | text: snapshot.hasData
67 | ? NumberFormat.currency(symbol: '\$', decimalDigits: 2)
68 | .format(snapshot.data['amount'])
69 | : '',
70 | style: const TextStyle(
71 | color: Colors.blueAccent,
72 | ),
73 | ),
74 | TextSpan(
75 | text: snapshot.hasData
76 | ? '\nUpdated at: ${DateFormat('hh:mm a, yyyy MMM dd')
77 | .format(DateTime.fromMillisecondsSinceEpoch(snapshot.data['time']))}'
78 | : '',
79 | style: const TextStyle(
80 | color: Colors.black38,
81 | fontSize: 14,
82 | height: null,
83 | ),
84 | ),
85 | ],
86 | ),
87 | ),
88 | ],
89 | ),
90 | );
91 |
92 | /// Enqueues a work request to poll the price.
93 | void _startPolling() async {
94 | await cancelWork(kTagBtcPricesWork); // cancel the previous work
95 | await enqueueWorkIntent(const WorkIntent(
96 | identifier: kTagBtcPricesWork,
97 | repeatInterval: Duration(seconds: 60), // TODO minimum is 900 on Android
98 | constraints: WorkConstraints(
99 | networkType: NetworkType.connected,
100 | batteryNotLow: true,
101 | ),
102 | ));
103 | }
104 | }
105 |
--------------------------------------------------------------------------------
/example/ios/Runner.xcodeproj/xcshareddata/xcschemes/Runner.xcscheme:
--------------------------------------------------------------------------------
1 |
2 |
5 |
8 |
9 |
15 |
21 |
22 |
23 |
24 |
25 |
30 |
31 |
32 |
33 |
39 |
40 |
41 |
42 |
43 |
44 |
54 |
56 |
62 |
63 |
64 |
65 |
66 |
67 |
73 |
75 |
81 |
82 |
83 |
84 |
86 |
87 |
90 |
91 |
92 |
--------------------------------------------------------------------------------
/android/src/main/java/dev/thinkng/flt_worker/FltWorkerPlugin.java:
--------------------------------------------------------------------------------
1 | package dev.thinkng.flt_worker;
2 |
3 | import android.content.Context;
4 |
5 | import androidx.annotation.Keep;
6 | import androidx.annotation.NonNull;
7 | import androidx.arch.core.util.Function;
8 |
9 | import java.util.List;
10 |
11 | import dev.thinkng.flt_worker.internal.AbsWorkerPlugin;
12 | import dev.thinkng.flt_worker.internal.BackgroundWorkerPlugin;
13 | import io.flutter.plugin.common.MethodCall;
14 | import io.flutter.plugin.common.MethodChannel;
15 | import io.flutter.plugin.common.MethodChannel.Result;
16 | import io.flutter.plugin.common.PluginRegistry;
17 | import io.flutter.plugin.common.PluginRegistry.Registrar;
18 |
19 | /** Main entry of the FltWorkerPlugin, dedicated to main isolate. */
20 | @Keep
21 | public class FltWorkerPlugin extends AbsWorkerPlugin {
22 | /**
23 | * Provides a callback to register all needed plugins for background workers.
24 | *
25 | * Example:
26 | * {@code
27 | * @Override
28 | * public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
29 | * GeneratedPluginRegistrant.registerWith(flutterEngine);
30 | * FltWorkerPlugin.registerPluginsForWorkers = registry -> {
31 | * if (!registry.hasPlugin("XPlugin")) {
32 | * XPlugin.registerWith(registry.registrarFor("XPlugin"));
33 | * }
34 | * return null;
35 | * };
36 | * }
37 | * }
38 | *
39 | */
40 | public static Function registerPluginsForWorkers;
41 |
42 | @SuppressWarnings("unused")
43 | public FltWorkerPlugin() {
44 | super();
45 | }
46 |
47 | private FltWorkerPlugin(Context context) {
48 | super(context);
49 | }
50 |
51 | // This static function is optional and equivalent to onAttachedToEngine. It supports the old
52 | // pre-Flutter-1.12 Android projects. You are encouraged to continue supporting
53 | // plugin registration via this function while apps migrate to use the new Android APIs
54 | // post-flutter-1.12 via https://flutter.dev/go/android-project-migration.
55 | //
56 | // It is encouraged to share logic between onAttachedToEngine and registerWith to keep
57 | // them functionally equivalent. Only one of onAttachedToEngine or registerWith will be called
58 | // depending on the user's project. onAttachedToEngine or registerWith must both be defined
59 | // in the same class.
60 | public static void registerWith(Registrar registrar) {
61 | final MethodChannel channel = new MethodChannel(registrar.messenger(), CHANNEL_NAME);
62 | channel.setMethodCallHandler(new FltWorkerPlugin(registrar.activity()));
63 | }
64 |
65 | @Override
66 | public boolean handleMethodCall(@NonNull MethodCall call, @NonNull Result result) {
67 | boolean handled = true;
68 | String method = call.method;
69 | if (method.equals(METHOD_PREFIX + "initialize")) {
70 | List args = (List) call.arguments;
71 | if (args.size() > 1) {
72 | Long dispatcherHandler = (Long) args.get(0);
73 | Long workerHandler = (Long) args.get(1);
74 | getPrefs()
75 | .edit()
76 | .putLong("callback_dispatcher_handle", dispatcherHandler)
77 | .putLong("worker_handle", workerHandler)
78 | .apply();
79 | }
80 | result.success(null);
81 | } else if (method.equals(METHOD_PREFIX + "test")) {
82 | try {
83 | BackgroundWorkerPlugin.getInstance(context).doWork(null);
84 | result.success(null);
85 | } catch (Exception e) {
86 | result.error("E", "worker test failure: " + e.getMessage(), null);
87 | }
88 | } else {
89 | handled = super.handleMethodCall(call, result);
90 | }
91 | return handled;
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/lib/src/models.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 |
3 | import 'package:flutter/foundation.dart';
4 |
5 | import 'constraints.dart';
6 |
7 | export 'constraints.dart';
8 |
9 | /// Describes a work request.
10 | ///
11 | /// The name `WorkIntent` is chosen to avoid conflict with the term `WorkRequest` on the Android platform.
12 | @immutable
13 | class WorkIntent {
14 | /// The identifier of the work.
15 | ///
16 | /// Will be prepended to [tags] implicitly, which can be retrieved from [WorkPayload] when handling the work.
17 | ///
18 | /// For the iOS platform, all `identifier`s must be registered in the `Info.plist` file,
19 | /// please see the [integration guide](https://github.com/xinthink/flt_worker#integration) for more details.
20 | final String identifier;
21 |
22 | /// Tags for grouping work.
23 | ///
24 | /// Tags except [identifier] are only available on **Android**.
25 | final Iterable tags;
26 |
27 | /// Input data of the work.
28 | ///
29 | /// Please notice that on iOS, the input data is cached with the key of `identifier`,
30 | /// if you schedule a work before the previous one with the same `identifier` is complete,
31 | /// cached input of the key `identifier` will be overwritten.
32 | final Map input;
33 |
34 | /// The duration of initial delay of the work.
35 | final Duration initialDelay;
36 |
37 | /// Constraints for the work to run.
38 | final WorkConstraints constraints;
39 |
40 | /// **iOS** only, if `true`, requests to schedule a `BGProcessingTaskRequest`,
41 | /// otherwise defaults to `BGAppRefreshTaskRequest`.
42 | final bool isProcessingTask;
43 |
44 | /// **Android** only. The repeat interval of a periodic work request.
45 | final Duration repeatInterval;
46 |
47 | /// **Android** only. The duration for which the work repeats from the end of the [repeatInterval].
48 | ///
49 | /// Note that flex intervals are ignored for certain Android OS versions (in particular, API 23).
50 | final Duration flexInterval;
51 |
52 | /// Instantiates a [WorkIntent] with an [identifier].
53 | ///
54 | /// Optional properties include [tags], [input] data and an [initialDelay].
55 | const WorkIntent({
56 | @required this.identifier,
57 | this.tags,
58 | this.input,
59 | this.initialDelay,
60 | this.constraints,
61 | this.isProcessingTask,
62 | this.repeatInterval,
63 | this.flexInterval,
64 | });
65 | }
66 |
67 | /// Payload of a background work.
68 | @immutable
69 | class WorkPayload {
70 | /// Id of the work.
71 | ///
72 | /// It's the BGTask identifier on iOS, and work **UUID** on Android.
73 | ///
74 | /// Please notice that it's **NOT** the `identifier` you specify in the `WorkIntent`
75 | /// when you schedule the work on Android devices.
76 | /// Retrieves the `identifier` from `tags` instead.
77 | final String id;
78 |
79 | /// Tags of the work.
80 | ///
81 | /// Tags except [identifier] are only available on **Android**.
82 | final Iterable tags;
83 |
84 | /// Input of the work.
85 | final Map input;
86 |
87 | /// Instantiates the payload for a work.
88 | const WorkPayload._({this.id, this.tags, this.input});
89 |
90 | /// Decodes the input json into a [WorkPayload].
91 | factory WorkPayload.fromJson(Map json) {
92 | Map input = json['input'] ?? {};
93 | String inputJson = input['data'];
94 | input = inputJson?.isNotEmpty == true ? jsonDecode(inputJson) : {};
95 | return WorkPayload._(
96 | id: json['id'],
97 | tags: Iterable.castFrom(json['tags'] ?? []),
98 | input: input,
99 | );
100 | }
101 | }
102 |
--------------------------------------------------------------------------------
/example/ios/Podfile:
--------------------------------------------------------------------------------
1 | # Uncomment this line to define a global platform for your project
2 | # platform :ios, '9.0'
3 |
4 | # CocoaPods analytics sends network stats synchronously affecting flutter build latency.
5 | ENV['COCOAPODS_DISABLE_STATS'] = 'true'
6 |
7 | project 'Runner', {
8 | 'Debug' => :debug,
9 | 'Profile' => :release,
10 | 'Release' => :release,
11 | }
12 |
13 | def parse_KV_file(file, separator='=')
14 | file_abs_path = File.expand_path(file)
15 | if !File.exists? file_abs_path
16 | return [];
17 | end
18 | generated_key_values = {}
19 | skip_line_start_symbols = ["#", "/"]
20 | File.foreach(file_abs_path) do |line|
21 | next if skip_line_start_symbols.any? { |symbol| line =~ /^\s*#{symbol}/ }
22 | plugin = line.split(pattern=separator)
23 | if plugin.length == 2
24 | podname = plugin[0].strip()
25 | path = plugin[1].strip()
26 | podpath = File.expand_path("#{path}", file_abs_path)
27 | generated_key_values[podname] = podpath
28 | else
29 | puts "Invalid plugin specification: #{line}"
30 | end
31 | end
32 | generated_key_values
33 | end
34 |
35 | target 'Runner' do
36 | use_frameworks!
37 | use_modular_headers!
38 |
39 | # Flutter Pod
40 |
41 | copied_flutter_dir = File.join(__dir__, 'Flutter')
42 | copied_framework_path = File.join(copied_flutter_dir, 'Flutter.framework')
43 | copied_podspec_path = File.join(copied_flutter_dir, 'Flutter.podspec')
44 | unless File.exist?(copied_framework_path) && File.exist?(copied_podspec_path)
45 | # Copy Flutter.framework and Flutter.podspec to Flutter/ to have something to link against if the xcode backend script has not run yet.
46 | # That script will copy the correct debug/profile/release version of the framework based on the currently selected Xcode configuration.
47 | # CocoaPods will not embed the framework on pod install (before any build phases can generate) if the dylib does not exist.
48 |
49 | generated_xcode_build_settings_path = File.join(copied_flutter_dir, 'Generated.xcconfig')
50 | unless File.exist?(generated_xcode_build_settings_path)
51 | raise "Generated.xcconfig must exist. If you're running pod install manually, make sure flutter pub get is executed first"
52 | end
53 | generated_xcode_build_settings = parse_KV_file(generated_xcode_build_settings_path)
54 | cached_framework_dir = generated_xcode_build_settings['FLUTTER_FRAMEWORK_DIR'];
55 |
56 | unless File.exist?(copied_framework_path)
57 | FileUtils.cp_r(File.join(cached_framework_dir, 'Flutter.framework'), copied_flutter_dir)
58 | end
59 | unless File.exist?(copied_podspec_path)
60 | FileUtils.cp(File.join(cached_framework_dir, 'Flutter.podspec'), copied_flutter_dir)
61 | end
62 | end
63 |
64 | # Keep pod path relative so it can be checked into Podfile.lock.
65 | pod 'Flutter', :path => 'Flutter'
66 |
67 | # Plugin Pods
68 |
69 | # Prepare symlinks folder. We use symlinks to avoid having Podfile.lock
70 | # referring to absolute paths on developers' machines.
71 | system('rm -rf .symlinks')
72 | system('mkdir -p .symlinks/plugins')
73 | plugin_pods = parse_KV_file('../.flutter-plugins')
74 | plugin_pods.each do |name, path|
75 | symlink = File.join('.symlinks', 'plugins', name)
76 | File.symlink(path, symlink)
77 | pod name, :path => File.join(symlink, 'ios')
78 | end
79 | end
80 |
81 | # Prevent Cocoapods from embedding a second Flutter framework and causing an error with the new Xcode build system.
82 | install! 'cocoapods', :disable_input_output_paths => true
83 |
84 | post_install do |installer|
85 | installer.pods_project.targets.each do |target|
86 | target.build_configurations.each do |config|
87 | config.build_settings['ENABLE_BITCODE'] = 'NO'
88 | end
89 | end
90 | end
91 |
--------------------------------------------------------------------------------
/example/lib/rest.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io' show HttpException;
2 | import 'dart:convert' show Encoding, jsonDecode, jsonEncode;
3 |
4 | import 'package:meta/meta.dart' show required;
5 | import 'package:http/http.dart' as http;
6 |
7 | typedef ErrorCheck = http.Response Function(http.Response);
8 | typedef BodyParser = T Function(String body);
9 |
10 | /// A simple [BodyParser] doing nothing.
11 | T ignoreBody(String body) => null;
12 |
13 | /// A simple [BodyParser] returning response body as is.
14 | String stringBody(String body) => body;
15 |
16 | /// A [BodyParser] parsing a `JSON` response.
17 | dynamic jsonBody(String body) => jsonDecode(body);
18 |
19 | /// Issues a `HTTP GET` request for [uri].
20 | ///
21 | /// with optional [headers],
22 | /// customized [bodyParser] to parse the response body, defaults to *ignoring*,
23 | /// customized [errorCheck] handling HTTP status, `2xx` considered successful by default.
24 | Future get(uri, {
25 | Map headers,
26 | BodyParser bodyParser,
27 | ErrorCheck errorCheck,
28 | }) {
29 | print('GET $uri headers=$headers');
30 | return http.get(uri, headers: headers)
31 | .then(errorCheck ?? _checkHttpError)
32 | .then((resp) => (bodyParser ?? ignoreBody)(resp.body));
33 | }
34 |
35 | /// `GET` a `JSON` response from [uri].
36 | ///
37 | /// with optional [headers],
38 | /// customized [errorCheck] handling HTTP status, `2xx` considered successful by default.
39 | Future getJson(uri, {
40 | Map headers,
41 | ErrorCheck errorCheck,
42 | }) => get(uri, headers: headers, bodyParser: jsonBody, errorCheck: errorCheck);
43 |
44 | /// `HTTP POST` [body] to [uri].
45 | ///
46 | /// with optional body [encoding] and [headers],
47 | /// customized [bodyParser] to parse the response body, defaults to *ignoring*,
48 | /// customized [errorCheck] handling HTTP status, `2xx` considered successful by default.
49 | ///
50 | /// see also: [http.post]
51 | Future post(uri, {
52 | @required dynamic body,
53 | Encoding encoding,
54 | Map headers,
55 | BodyParser bodyParser,
56 | ErrorCheck errorCheck,
57 | }) {
58 | print('POST $uri headers=$headers');
59 | return http.post(uri,
60 | body: body,
61 | encoding: encoding,
62 | headers: headers,
63 | )
64 | .then(errorCheck ?? _checkHttpError)
65 | .then((resp) => (bodyParser ?? ignoreBody)(resp.body));
66 | }
67 |
68 | /// `HTTP POST` a JSON [body] to [uri].
69 | ///
70 | /// with optional body [encoding] and [headers],
71 | /// customized [bodyParser] to parse the response body, defaults to *ignoring*,
72 | /// customized [errorCheck] handling HTTP status, `2xx` considered successful by default.
73 | ///
74 | /// see also: [http.post]
75 | Future postJson(uri, {
76 | @required dynamic body,
77 | Encoding encoding,
78 | Map headers,
79 | ErrorCheck errorCheck,
80 | }) => post(
81 | uri,
82 | body: jsonEncode(body),
83 | encoding: encoding,
84 | headers: _mergeHeaders({
85 | 'Content-type': 'application/json',
86 | }, headers),
87 | bodyParser: jsonBody,
88 | errorCheck: errorCheck,
89 | );
90 |
91 | /// Checking HTTP status code for failures
92 | http.Response _checkHttpError(http.Response resp) {
93 | if (resp.statusCode < 200 || resp.statusCode >= 300) {
94 | throw HttpException('${resp.request.method} ${resp.request.url} failed: ${resp.statusCode}');
95 | }
96 | return resp;
97 | }
98 |
99 | /// Merge [extra] headers into the [base] one
100 | Map _mergeHeaders(Map base, Map extra) {
101 | final headers = {};
102 | if (base != null) headers.addAll(base);
103 | if (extra != null) headers.addAll(extra);
104 | return headers;
105 | }
106 |
--------------------------------------------------------------------------------
/ios/Classes/internal/BGTaskHandler.m:
--------------------------------------------------------------------------------
1 | //
2 | // BGTaskHandler.m
3 | // flt_worker
4 | //
5 | // Created by Yingxin Wu on 2020/3/13.
6 | //
7 |
8 | #import "BGTaskHandler.h"
9 | #import "BGTaskMgrDelegate.h"
10 |
11 | static BGTaskHandler *_instance = nil;
12 | static FuncRegisterPlugins _registerPlugins = nil;
13 |
14 | @implementation BGTaskHandler {
15 | BGTaskMgrDelegate *_delegate;
16 | FlutterEngine *_headlessEngine;
17 | BOOL _headlessEngineStarted;
18 | }
19 |
20 | + (FuncRegisterPlugins) registerPlugins {
21 | return _registerPlugins;
22 | }
23 |
24 | + (void) setRegisterPlugins:(FuncRegisterPlugins)registerPlugins {
25 | _registerPlugins = registerPlugins;
26 | }
27 |
28 | + (void)registerWithRegistrar:(nonnull NSObject *)registrar {
29 | }
30 |
31 | + (BGTaskHandler*) instance {
32 | @synchronized (self) {
33 | if (_instance == nil) {
34 | _instance = [[BGTaskHandler alloc] init];
35 | }
36 | }
37 | return _instance;
38 | }
39 |
40 | - (instancetype)init {
41 | self = [super init];
42 | if (self) {
43 | // init a headless engine instance for callback
44 | _headlessEngine = [[FlutterEngine alloc] initWithName:@"flt_worker_isolate"
45 | project:nil
46 | allowHeadlessExecution:YES];
47 | [self startCallbackDispatcher];
48 |
49 | // methodCallHandler must be set on a running engine
50 | _delegate = [[BGTaskMgrDelegate alloc] initWithEngine:_headlessEngine];
51 | [_delegate.methodChannel setMethodCallHandler:^(FlutterMethodCall * _Nonnull call, FlutterResult _Nonnull result) {
52 | [self handleMethodCall:call result:result];
53 | }];
54 | }
55 | return self;
56 | }
57 |
58 | - (void)handleMethodCall:(FlutterMethodCall * _Nonnull)call result:(FlutterResult _Nonnull)result {
59 | if (![_delegate handleMethodCall:call result:result]) {
60 | result(FlutterMethodNotImplemented);
61 | }
62 | }
63 |
64 | /** Start a headless engine instance with the entry handle */
65 | - (void)startCallbackDispatcher {
66 | @synchronized (self) {
67 | if (!_headlessEngineStarted) {
68 | int64_t handle = dispatcherHandle();
69 | FlutterCallbackInformation *cbInfo = [FlutterCallbackCache lookupCallbackInformation:handle];
70 | if (cbInfo == nil) {
71 | NSLog(@"callback not found for handle: %lld", handle);
72 | return;
73 | }
74 |
75 | [_headlessEngine runWithEntrypoint:cbInfo.callbackName libraryURI:cbInfo.callbackLibraryPath];
76 | _registerPlugins(_headlessEngine);
77 | _headlessEngineStarted = YES;
78 | }
79 | }
80 | }
81 |
82 | - (void)handleBGTask:(BGTask * _Nonnull)task {
83 | NSString *identifier = task.identifier;
84 | NSLog(@"Handling BGTask id=%@", identifier);
85 |
86 | int64_t handle = workerHandle();
87 | [_delegate.methodChannel invokeMethod:@API_METHOD(dispatch)
88 | arguments:@[
89 | @(handle),
90 | [_delegate packPayloadForTask:identifier],
91 | ]
92 | result:^(id _Nullable result) {
93 | if ([result isKindOfClass:[FlutterError class]]) {
94 | NSLog(@"BGTask '%@' execution failed: %@ %@", identifier, ((FlutterError*) result).code, ((FlutterError*) result).message);
95 | [task setTaskCompletedWithSuccess:NO];
96 | } else if (result == FlutterMethodNotImplemented) {
97 | NSLog(@"Dart worker for BGTask '%@' is NOT implemented", identifier);
98 | [task setTaskCompletedWithSuccess:NO];
99 | } else {
100 | NSLog(@"BGTask '%@' done. result=%@", identifier, result);
101 | [task setTaskCompletedWithSuccess:YES];
102 | }
103 | }];
104 |
105 | task.expirationHandler = ^{
106 | NSLog(@"WARN: BGTask expired. id=%@", identifier);
107 | };
108 | }
109 |
110 | @end
111 |
--------------------------------------------------------------------------------
/android/.idea/codeStyles/Project.xml:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 | xmlns:android
21 |
22 | ^$
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | xmlns:.*
32 |
33 | ^$
34 |
35 |
36 | BY_NAME
37 |
38 |
39 |
40 |
41 |
42 |
43 | .*:id
44 |
45 | http://schemas.android.com/apk/res/android
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 | .*:name
55 |
56 | http://schemas.android.com/apk/res/android
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 | name
66 |
67 | ^$
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 | style
77 |
78 | ^$
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 | .*
88 |
89 | ^$
90 |
91 |
92 | BY_NAME
93 |
94 |
95 |
96 |
97 |
98 |
99 | .*
100 |
101 | http://schemas.android.com/apk/res/android
102 |
103 |
104 | ANDROID_ATTRIBUTE_ORDER
105 |
106 |
107 |
108 |
109 |
110 |
111 | .*
112 |
113 | .*
114 |
115 |
116 | BY_NAME
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/example/lib/main.dart:
--------------------------------------------------------------------------------
1 | import 'dart:io';
2 |
3 | import 'package:flt_worker/flt_worker.dart';
4 | import 'package:flutter/material.dart';
5 |
6 | import 'background_tasks_btc_prices.dart';
7 | import 'background_tasks_counter.dart';
8 | import 'btc_prices.dart';
9 | import 'counter.dart';
10 | import 'work_manager_btc_prices.dart';
11 | import 'work_manager_counter.dart';
12 | import 'worker.dart';
13 |
14 | /// Background processing examples.
15 | ///
16 | /// Force start iOS background tasks:
17 | /// ```
18 | /// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.example.btc_prices_task"]
19 | /// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.example.counter_task"]
20 | /// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"com.example.task1"]
21 | /// e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"dev.example.task2"]
22 | /// ```
23 | ///
24 | /// > See [iOS documentations](https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development)
25 | void main() {
26 | runApp(MyApp());
27 | initializeWorker(worker);
28 | }
29 |
30 | class MyApp extends StatelessWidget {
31 | @override
32 | Widget build(BuildContext context) => MaterialApp(
33 | home: Scaffold(
34 | appBar: AppBar(
35 | title: const Text('Worker Examples'),
36 | ),
37 | body: Builder(
38 | builder: (context) => SingleChildScrollView(
39 | child: Container(
40 | padding: const EdgeInsets.symmetric(vertical: 48, horizontal: 32),
41 | child: DefaultTextStyle(
42 | textAlign: TextAlign.center,
43 | style: const TextStyle(),
44 | child: Column(
45 | mainAxisSize: MainAxisSize.min,
46 | crossAxisAlignment: CrossAxisAlignment.stretch,
47 | children: [
48 | const Text('High level API examples',
49 | style: TextStyle(
50 | fontSize: 14,
51 | color: Colors.black45,
52 | ),
53 | ),
54 | const SizedBox(height: 16),
55 | RaisedButton(
56 | child: const Text('Counter'),
57 | onPressed: () => Navigator.push(context, MaterialPageRoute(
58 | builder: (_) => Counter(),
59 | )),
60 | ),
61 | RaisedButton(
62 | child: const Text('Bitcoin price polling'),
63 | onPressed: () => Navigator.push(context, MaterialPageRoute(
64 | builder: (_) => BtcPrices(),
65 | )),
66 | ),
67 | Padding(
68 | padding: const EdgeInsets.only(top: 48, bottom: 16),
69 | child: const Text('Low level platform-specific API examples',
70 | style: TextStyle(
71 | fontSize: 14,
72 | color: Colors.black45,
73 | ),
74 | ),
75 | ),
76 | ...(Platform.isAndroid ? _workManagerExamples(context) : _bgTasksExamples(context)),
77 | ],
78 | ),
79 | ),
80 | ),
81 | ),
82 | ),
83 | ),
84 | );
85 | }
86 |
87 | List _workManagerExamples(BuildContext context) => [
88 | RaisedButton(
89 | child: const Text('Counter\n(OneTimeWorkRequest)'),
90 | onPressed: () => Navigator.push(context, MaterialPageRoute(
91 | builder: (_) => WorkManagerCounter(),
92 | )),
93 | ),
94 | RaisedButton(
95 | child: const Text('Bitcoin price polling\n(PeriodicWorkRequest)'),
96 | onPressed: () => Navigator.push(context, MaterialPageRoute(
97 | builder: (_) => WorkManagerBtcPrices(),
98 | )),
99 | ),
100 | ];
101 |
102 | List _bgTasksExamples(BuildContext context) => [
103 | RaisedButton(
104 | child: const Text('Counter\n(BGProcessingTaskRequest)'),
105 | onPressed: () {
106 | Navigator.push(context, MaterialPageRoute(
107 | builder: (_) => BackgroundTasksCounter(),
108 | ));
109 | },
110 | ),
111 | RaisedButton(
112 | child: const Text('Bitcoin price polling\n(BGAppRefreshTaskRequest)'),
113 | onPressed: () {
114 | Navigator.push(context, MaterialPageRoute(
115 | builder: (_) => BackgroundTasksBtcPrices(),
116 | ));
117 | },
118 | ),
119 | ];
120 |
--------------------------------------------------------------------------------
/pubspec.lock:
--------------------------------------------------------------------------------
1 | # Generated by pub
2 | # See https://dart.dev/tools/pub/glossary#lockfile
3 | packages:
4 | archive:
5 | dependency: transitive
6 | description:
7 | name: archive
8 | url: "https://pub.flutter-io.cn"
9 | source: hosted
10 | version: "2.0.11"
11 | args:
12 | dependency: transitive
13 | description:
14 | name: args
15 | url: "https://pub.flutter-io.cn"
16 | source: hosted
17 | version: "1.5.2"
18 | async:
19 | dependency: transitive
20 | description:
21 | name: async
22 | url: "https://pub.flutter-io.cn"
23 | source: hosted
24 | version: "2.4.0"
25 | boolean_selector:
26 | dependency: transitive
27 | description:
28 | name: boolean_selector
29 | url: "https://pub.flutter-io.cn"
30 | source: hosted
31 | version: "1.0.5"
32 | charcode:
33 | dependency: transitive
34 | description:
35 | name: charcode
36 | url: "https://pub.flutter-io.cn"
37 | source: hosted
38 | version: "1.1.2"
39 | collection:
40 | dependency: transitive
41 | description:
42 | name: collection
43 | url: "https://pub.flutter-io.cn"
44 | source: hosted
45 | version: "1.14.11"
46 | convert:
47 | dependency: transitive
48 | description:
49 | name: convert
50 | url: "https://pub.flutter-io.cn"
51 | source: hosted
52 | version: "2.1.1"
53 | crypto:
54 | dependency: transitive
55 | description:
56 | name: crypto
57 | url: "https://pub.flutter-io.cn"
58 | source: hosted
59 | version: "2.1.3"
60 | flutter:
61 | dependency: "direct main"
62 | description: flutter
63 | source: sdk
64 | version: "0.0.0"
65 | flutter_test:
66 | dependency: "direct dev"
67 | description: flutter
68 | source: sdk
69 | version: "0.0.0"
70 | image:
71 | dependency: transitive
72 | description:
73 | name: image
74 | url: "https://pub.flutter-io.cn"
75 | source: hosted
76 | version: "2.1.4"
77 | matcher:
78 | dependency: transitive
79 | description:
80 | name: matcher
81 | url: "https://pub.flutter-io.cn"
82 | source: hosted
83 | version: "0.12.6"
84 | meta:
85 | dependency: transitive
86 | description:
87 | name: meta
88 | url: "https://pub.flutter-io.cn"
89 | source: hosted
90 | version: "1.1.8"
91 | path:
92 | dependency: transitive
93 | description:
94 | name: path
95 | url: "https://pub.flutter-io.cn"
96 | source: hosted
97 | version: "1.6.4"
98 | pedantic:
99 | dependency: "direct dev"
100 | description:
101 | name: pedantic
102 | url: "https://pub.flutter-io.cn"
103 | source: hosted
104 | version: "1.9.0"
105 | petitparser:
106 | dependency: transitive
107 | description:
108 | name: petitparser
109 | url: "https://pub.flutter-io.cn"
110 | source: hosted
111 | version: "2.4.0"
112 | quiver:
113 | dependency: transitive
114 | description:
115 | name: quiver
116 | url: "https://pub.flutter-io.cn"
117 | source: hosted
118 | version: "2.0.5"
119 | sky_engine:
120 | dependency: transitive
121 | description: flutter
122 | source: sdk
123 | version: "0.0.99"
124 | source_span:
125 | dependency: transitive
126 | description:
127 | name: source_span
128 | url: "https://pub.flutter-io.cn"
129 | source: hosted
130 | version: "1.5.5"
131 | stack_trace:
132 | dependency: transitive
133 | description:
134 | name: stack_trace
135 | url: "https://pub.flutter-io.cn"
136 | source: hosted
137 | version: "1.9.3"
138 | stream_channel:
139 | dependency: transitive
140 | description:
141 | name: stream_channel
142 | url: "https://pub.flutter-io.cn"
143 | source: hosted
144 | version: "2.0.0"
145 | string_scanner:
146 | dependency: transitive
147 | description:
148 | name: string_scanner
149 | url: "https://pub.flutter-io.cn"
150 | source: hosted
151 | version: "1.0.5"
152 | term_glyph:
153 | dependency: transitive
154 | description:
155 | name: term_glyph
156 | url: "https://pub.flutter-io.cn"
157 | source: hosted
158 | version: "1.1.0"
159 | test_api:
160 | dependency: transitive
161 | description:
162 | name: test_api
163 | url: "https://pub.flutter-io.cn"
164 | source: hosted
165 | version: "0.2.15"
166 | typed_data:
167 | dependency: transitive
168 | description:
169 | name: typed_data
170 | url: "https://pub.flutter-io.cn"
171 | source: hosted
172 | version: "1.1.6"
173 | vector_math:
174 | dependency: transitive
175 | description:
176 | name: vector_math
177 | url: "https://pub.flutter-io.cn"
178 | source: hosted
179 | version: "2.0.8"
180 | xml:
181 | dependency: transitive
182 | description:
183 | name: xml
184 | url: "https://pub.flutter-io.cn"
185 | source: hosted
186 | version: "3.5.0"
187 | sdks:
188 | dart: ">=2.4.0 <3.0.0"
189 | flutter: ">=1.10.0 <2.0.0"
190 |
--------------------------------------------------------------------------------
/lib/src/work_manager/models.dart:
--------------------------------------------------------------------------------
1 | import 'dart:convert';
2 | import 'dart:math';
3 |
4 | import 'package:flutter/foundation.dart';
5 |
6 | import '../constraints.dart';
7 |
8 | export '../constraints.dart';
9 |
10 | /// An abstract class representing a work request.
11 | @immutable
12 | abstract class WorkRequest {
13 | /// Tags for grouping work.
14 | final Iterable tags;
15 |
16 | /// Input data of the work.
17 | final Map input;
18 |
19 | /// The duration of initial delay of the work.
20 | final Duration initialDelay;
21 |
22 | /// Constraints for the work to run.
23 | final WorkConstraints constraints;
24 |
25 | /// Sets the backoff policy and backoff delay for the work.
26 | final BackoffCriteria backoffCriteria;
27 |
28 | /// Instantiates a WorkRequest with optional [tags] and [input] data.
29 | ///
30 | /// Optionally provides an [initialDelay].
31 | const WorkRequest({
32 | this.tags,
33 | this.input,
34 | this.initialDelay,
35 | this.constraints,
36 | this.backoffCriteria,
37 | });
38 |
39 | /// Serializes this work request into a json object.
40 | Map toJson() => {
41 | 'type': (this is OneTimeWorkRequest) ? 'OneTime' : 'Periodic',
42 | 'tags': tags,
43 | 'input': {
44 | 'data': jsonEncode(input ?? {}), // always encode the input data
45 | },
46 | 'initialDelay': max(initialDelay?.inMicroseconds ?? 0, 0),
47 | 'constraints': constraints?.toJson(),
48 | 'backoffCriteria': backoffCriteria?.toJson(),
49 | };
50 | }
51 |
52 | /// Defines an one-off work request.
53 | @immutable
54 | class OneTimeWorkRequest extends WorkRequest {
55 | /// Instantiates an [OneTimeWorkRequest].
56 | ///
57 | /// With optional [tags] and [input] data.
58 | const OneTimeWorkRequest({
59 | Iterable tags,
60 | Map input,
61 | Duration initialDelay,
62 | WorkConstraints constraints,
63 | BackoffCriteria backoffCriteria,
64 | }) : super(
65 | tags: tags,
66 | input: input,
67 | initialDelay: initialDelay,
68 | constraints: constraints,
69 | backoffCriteria: backoffCriteria,
70 | );
71 | }
72 |
73 | /// Defines a periodic work request.
74 | @immutable
75 | class PeriodicWorkRequest extends WorkRequest {
76 | /// The repeat interval
77 | final Duration repeatInterval;
78 |
79 | /// The duration for which the work repeats from the end of the [repeatInterval].
80 | ///
81 | /// Note that flex intervals are ignored for certain OS versions (in particular, API 23).
82 | final Duration flexInterval;
83 |
84 | /// Creates a [PeriodicWorkRequest] to run periodically once every [repeatInterval] period
85 | /// , with an optional [flexInterval].
86 | const PeriodicWorkRequest({
87 | @required this.repeatInterval,
88 | this.flexInterval,
89 | Iterable tags,
90 | Map input,
91 | Duration initialDelay,
92 | WorkConstraints constraints,
93 | BackoffCriteria backoffCriteria,
94 | }) : super(
95 | tags: tags,
96 | input: input,
97 | initialDelay: initialDelay,
98 | constraints: constraints,
99 | backoffCriteria: backoffCriteria,
100 | );
101 |
102 | @override
103 | Map toJson() => super.toJson()
104 | ..['repeatInterval'] = repeatInterval.inMicroseconds
105 | ..['flexInterval'] = flexInterval?.inMicroseconds;
106 | }
107 |
108 | /// Sets the backoff policy and backoff delay for the work.
109 | @immutable
110 | class BackoffCriteria {
111 | /// Backoff policy for the work.
112 | ///
113 | /// The default value is dependent on the native `WorkManager`,
114 | /// which should be [BackoffPolicy.exponential] according to
115 | /// the [documentation](https://developer.android.com/reference/androidx/work/WorkRequest.Builder#setBackoffCriteria(androidx.work.BackoffPolicy,%20long,%20java.util.concurrent.TimeUnit)).
116 | final BackoffPolicy policy;
117 |
118 | /// Backoff backoff delay for the work.
119 | ///
120 | /// The default value and the valid range is dependent on the native `WorkManager`.
121 | /// According to the [documentation](https://developer.android.com/reference/androidx/work/WorkRequest.Builder#setBackoffCriteria(androidx.work.BackoffPolicy,%20long,%20java.util.concurrent.TimeUnit))
122 | /// it defaults to `30` seconds, and will be clamped between `10` seconds and `5` hours.
123 | final Duration delay;
124 |
125 | /// Creates a [BackoffCriteria] with the backoff [policy] and backoff [delay].
126 | const BackoffCriteria({
127 | @required this.policy,
128 | @required this.delay,
129 | });
130 |
131 | /// Serializes this backoff criteria into a json object.
132 | Map toJson() => {
133 | 'policy': policy?.index,
134 | 'delay': delay?.inMicroseconds,
135 | };
136 | }
137 |
138 | /// An enumeration of backoff policies when retrying work.
139 | ///
140 | /// TODO: These policies are used when you have a return ListenableWorker.Result.retry() from a worker
141 | /// to determine the correct backoff time.
142 | enum BackoffPolicy {
143 | /// Indicates that the backoff time should be increased exponentially.
144 | exponential,
145 |
146 | /// Indicates that the backoff time should be increased linearly.
147 | linear,
148 | }
149 |
--------------------------------------------------------------------------------
/android/src/main/java/dev/thinkng/flt_worker/internal/BackgroundWorkerPlugin.java:
--------------------------------------------------------------------------------
1 | package dev.thinkng.flt_worker.internal;
2 |
3 | import android.content.Context;
4 | import android.util.Log;
5 |
6 | import androidx.annotation.Keep;
7 | import androidx.annotation.Nullable;
8 | import androidx.annotation.UiThread;
9 | import androidx.work.Worker;
10 |
11 | import java.util.Arrays;
12 | import java.util.HashMap;
13 | import java.util.LinkedList;
14 | import java.util.Map;
15 | import java.util.concurrent.ExecutionException;
16 | import java.util.concurrent.Future;
17 | import java.util.concurrent.atomic.AtomicBoolean;
18 |
19 | import dev.thinkng.flt_worker.FltWorkerPlugin;
20 | import io.flutter.plugin.common.MethodChannel;
21 | import io.flutter.plugin.common.PluginRegistry;
22 | import io.flutter.view.FlutterCallbackInformation;
23 | import io.flutter.view.FlutterMain;
24 | import io.flutter.view.FlutterNativeView;
25 | import io.flutter.view.FlutterRunArguments;
26 |
27 | /** WorkerPlugin dedicated to the background isolate. */
28 | @Keep
29 | public class BackgroundWorkerPlugin extends AbsWorkerPlugin {
30 | private static BackgroundWorkerPlugin instance;
31 |
32 | public static BackgroundWorkerPlugin getInstance(Context context) {
33 | synchronized (BackgroundWorkerPlugin.class) {
34 | if (instance == null) {
35 | instance = new BackgroundWorkerPlugin(context);
36 | }
37 | }
38 | return instance;
39 | }
40 |
41 | // The headless Flutter instance to run the callbacks.
42 | private FlutterNativeView headlessView;
43 | private final AtomicBoolean headlessViewStarted = new AtomicBoolean();
44 |
45 | private BackgroundWorkerPlugin(Context context) {
46 | super(context);
47 | }
48 |
49 | /**
50 | * Callback for doing the background work.
51 | */
52 | public Future doWork(@Nullable Worker worker) throws ExecutionException, InterruptedException {
53 | if (worker != null) {
54 | Log.d(TAG, "executing Work id=" + worker.getId() + " tags=" + worker.getTags());
55 | } else {
56 | Log.d(TAG, "executing an empty Work (test only)");
57 | }
58 |
59 | runOnMainThread(new Runnable() {
60 | @Override
61 | public void run() {
62 | startHeadlessEngine(context);
63 | }
64 | }).get();
65 | return dispatchCallback(getPrefs().getLong("worker_handle", 0), worker);
66 | }
67 |
68 | @UiThread
69 | private void startHeadlessEngine(Context context) {
70 | synchronized (headlessViewStarted) {
71 | if (headlessView == null) {
72 | long handle = getPrefs().getLong("callback_dispatcher_handle", 0);
73 | FlutterCallbackInformation cbInfo = FlutterCallbackInformation.lookupCallbackInformation(handle);
74 | //noinspection ConstantConditions
75 | if (cbInfo == null) {
76 | Log.w(TAG, "callback dispatcher handle not found!");
77 | return;
78 | }
79 |
80 | headlessView = new FlutterNativeView(context, true);
81 | // register plugins to the callback isolate
82 | registerPluginsForHeadlessView();
83 |
84 | FlutterRunArguments args = new FlutterRunArguments();
85 | args.bundlePath = FlutterMain.findAppBundlePath();
86 | args.entrypoint = cbInfo.callbackName;
87 | args.libraryPath = cbInfo.callbackLibraryPath;
88 | headlessView.runFromBundle(args);
89 | }
90 |
91 | if (channel != null) {
92 | channel.setMethodCallHandler(null);
93 | }
94 | channel = new MethodChannel(headlessView, CHANNEL_NAME);
95 | channel.setMethodCallHandler(this);
96 | }
97 | }
98 |
99 | /* Register plugins for the headless isolate */
100 | private void registerPluginsForHeadlessView() {
101 | PluginRegistry registry = headlessView.getPluginRegistry();
102 | // if (!registry.hasPlugin("FltWorkerPlugin")) {
103 | // PluginRegistry.Registrar registrar = registry.registrarFor("FltWorkerPlugin");
104 | // new MethodChannel(registrar.messenger(), CHANNEL_NAME)
105 | // .setMethodCallHandler(this);
106 | // }
107 |
108 | if (FltWorkerPlugin.registerPluginsForWorkers != null) {
109 | FltWorkerPlugin.registerPluginsForWorkers.apply(registry);
110 | }
111 | }
112 |
113 | /** Dispatch a callback function call */
114 | private Future dispatchCallback(final long handle,
115 | @Nullable final Worker worker)
116 | throws InterruptedException, ExecutionException {
117 | final Map payload = new HashMap<>();
118 | if (worker != null) {
119 | // worker is null only if it's a testing request
120 | payload.put("id", worker.getId().toString());
121 | payload.put("input", worker.getInputData().getKeyValueMap());
122 |
123 | LinkedList tags = new LinkedList<>();
124 | for (String tag : worker.getTags()) {
125 | if (!tag.startsWith("dev.thinkng.flt_worker")) {
126 | tags.add(tag);
127 | }
128 | }
129 | payload.put("tags", tags);
130 | }
131 |
132 | final MethodCallFuture callback = new MethodCallFuture<>();
133 | runOnMainThread(new Runnable() {
134 | @Override
135 | public void run() {
136 | channel.invokeMethod(METHOD_PREFIX + "dispatch",
137 | Arrays.asList(handle, payload),
138 | callback);
139 | }
140 | }).get();
141 | return callback;
142 | }
143 | }
144 |
--------------------------------------------------------------------------------
/android/src/test/java/dev/thinkng/flt_worker/internal/WorkRequestsTest.java:
--------------------------------------------------------------------------------
1 | package dev.thinkng.flt_worker.internal;
2 |
3 | import android.os.Build;
4 |
5 | import androidx.work.BackoffPolicy;
6 | import androidx.work.Constraints;
7 | import androidx.work.NetworkType;
8 | import androidx.work.OneTimeWorkRequest;
9 | import androidx.work.PeriodicWorkRequest;
10 | import androidx.work.WorkRequest;
11 |
12 | import org.junit.Test;
13 |
14 | import java.util.ArrayList;
15 | import java.util.Arrays;
16 | import java.util.HashMap;
17 | import java.util.Map;
18 |
19 | import static org.junit.Assert.*;
20 |
21 | public class WorkRequestsTest {
22 |
23 | @Test
24 | public void parseOneTimeWorkRequest() {
25 | Map json = new HashMap<>();
26 | json.put("tags", Arrays.asList("test", "work"));
27 | json.put("initialDelay", 10000000); // 10 seconds in microseconds
28 |
29 | WorkRequest req = WorkRequests.parseRequest(json);
30 |
31 | assertTrue(req instanceof OneTimeWorkRequest);
32 | assertEquals(Arrays.asList(BackgroundWorker.class.getName(), "test", "work"),
33 | new ArrayList<>(req.getTags()));
34 | assertEquals(10000, req.getWorkSpec().initialDelay); // 10 seconds in milliseconds
35 | }
36 |
37 | @Test
38 | public void parsePeriodicWorkRequest() {
39 | Map json = new HashMap<>();
40 | json.put("type", "Periodic");
41 | json.put("repeatInterval", 86400 * 1000000L); // 1 day in microseconds
42 | json.put("tags", Arrays.asList("test", "periodic"));
43 | json.put("initialDelay", 10000000); // 10 seconds in microseconds
44 |
45 | WorkRequest req = WorkRequests.parseRequest(json);
46 |
47 | assertTrue(req instanceof PeriodicWorkRequest);
48 | assertEquals(Arrays.asList(BackgroundWorker.class.getName(), "test", "periodic"),
49 | new ArrayList<>(req.getTags()));
50 | assertEquals(10000, req.getWorkSpec().initialDelay); // 10 seconds in milliseconds
51 | assertEquals(86400 * 1000L, req.getWorkSpec().intervalDuration); // 1 day in milliseconds
52 | }
53 |
54 | @Test
55 | public void parseBackoffCriteria() {
56 | Map json = new HashMap<>();
57 | Map backoffCriteriaJson = new HashMap<>();
58 | backoffCriteriaJson.put("policy", BackoffPolicy.LINEAR.ordinal());
59 | backoffCriteriaJson.put("delay", 12000000); // 12 seconds in microseconds
60 | json.put("backoffCriteria", backoffCriteriaJson);
61 |
62 | WorkRequest req = WorkRequests.parseRequest(json);
63 |
64 | assertEquals(BackoffPolicy.LINEAR, req.getWorkSpec().backoffPolicy);
65 | assertEquals(12000, req.getWorkSpec().backoffDelayDuration); // 12 seconds in milliseconds
66 | }
67 |
68 | @Test
69 | public void parseInvalidBackoffCriteria() {
70 | // empty criteria
71 | Map json = new HashMap<>();
72 | Map backoffCriteriaJson = new HashMap<>();
73 | json.put("backoffCriteria", backoffCriteriaJson);
74 |
75 | WorkRequest req = WorkRequests.parseRequest(json);
76 |
77 | // should fallback to defaults
78 | assertEquals(BackoffPolicy.EXPONENTIAL, req.getWorkSpec().backoffPolicy);
79 |
80 | // the same should happen to an incomplete criteria
81 | backoffCriteriaJson.put("policy", BackoffPolicy.LINEAR.ordinal());
82 | req = WorkRequests.parseRequest(json);
83 | assertEquals(BackoffPolicy.EXPONENTIAL, req.getWorkSpec().backoffPolicy);
84 |
85 | json.clear();
86 | backoffCriteriaJson.put("delay", 12000000);
87 | req = WorkRequests.parseRequest(json);
88 | assertEquals(BackoffPolicy.EXPONENTIAL, req.getWorkSpec().backoffPolicy);
89 | assertEquals(WorkRequest.DEFAULT_BACKOFF_DELAY_MILLIS, req.getWorkSpec().backoffDelayDuration);
90 | }
91 |
92 | @SuppressWarnings("ConstantConditions")
93 | @Test
94 | public void parseConstraints() {
95 | Map constraintsJson = new HashMap<>();
96 | constraintsJson.put("networkType", NetworkType.NOT_ROAMING.ordinal());
97 | constraintsJson.put("batteryNotLow", true);
98 | constraintsJson.put("charging", null);
99 | constraintsJson.put("deviceIdle", true);
100 | constraintsJson.put("storageNotLow", false);
101 |
102 | Map reqJson = new HashMap<>();
103 | reqJson.put("constraints", constraintsJson);
104 | WorkRequest req = WorkRequests.parseRequest(reqJson);
105 |
106 | assertTrue("should has constraints", req.getWorkSpec().hasConstraints());
107 | final Constraints constraints = req.getWorkSpec().constraints;
108 | assertEquals(NetworkType.NOT_ROAMING, constraints.getRequiredNetworkType());
109 | assertTrue(constraints.requiresBatteryNotLow());
110 | if (Build.VERSION.SDK_INT >= 23) assertTrue(constraints.requiresDeviceIdle());
111 | assertFalse(constraints.requiresStorageNotLow());
112 | assertFalse(constraints.requiresCharging()); // fallback to defaults
113 | }
114 |
115 | @Test
116 | public void parseFlexInterval() {
117 | Map json = new HashMap<>();
118 | json.put("type", "Periodic");
119 | json.put("repeatInterval", 86400 * 1000000L); // 1 day in microseconds
120 | json.put("flexInterval", 600 * 1000000L); // 10 minutes in microseconds
121 |
122 | WorkRequest req = WorkRequests.parseRequest(json);
123 |
124 | assertTrue(req instanceof PeriodicWorkRequest);
125 | assertEquals(86400 * 1000L, req.getWorkSpec().intervalDuration); // 1 day in milliseconds
126 | assertEquals(600 * 1000L, req.getWorkSpec().flexDuration); // 10 minutes in milliseconds
127 | }
128 | }
129 |
--------------------------------------------------------------------------------
/android/src/main/java/dev/thinkng/flt_worker/internal/WorkRequests.java:
--------------------------------------------------------------------------------
1 | package dev.thinkng.flt_worker.internal;
2 |
3 | import android.os.Build;
4 |
5 | import androidx.annotation.NonNull;
6 | import androidx.annotation.Nullable;
7 | import androidx.annotation.VisibleForTesting;
8 | import androidx.core.util.Pair;
9 | import androidx.work.BackoffPolicy;
10 | import androidx.work.Constraints;
11 | import androidx.work.Data;
12 | import androidx.work.NetworkType;
13 | import androidx.work.OneTimeWorkRequest;
14 | import androidx.work.PeriodicWorkRequest;
15 | import androidx.work.WorkRequest;
16 |
17 | import java.util.ArrayList;
18 | import java.util.List;
19 | import java.util.Map;
20 | import java.util.concurrent.TimeUnit;
21 |
22 | final class WorkRequests {
23 | private WorkRequests() {}
24 |
25 | @Nullable
26 | static List extends WorkRequest> parseRequests(@NonNull Object input) {
27 | if (!(input instanceof List) || ((List) input).isEmpty()) {
28 | return null;
29 | }
30 |
31 | List requests = new ArrayList<>();
32 | List requestsJson = (List) input;
33 | for (Object json : requestsJson) {
34 | if (json instanceof Map) {
35 | requests.add(parseRequest((Map) json));
36 | }
37 | }
38 | return requests;
39 | }
40 |
41 | @SuppressWarnings("WeakerAccess")
42 | @VisibleForTesting
43 | @NonNull
44 | public static WorkRequest parseRequest(@NonNull Map json) {
45 | try {
46 | String type = (String) json.get("type");
47 | if ("Periodic".equals(type)) {
48 | return parsePeriodicWorkRequest(json);
49 | } else {
50 | return parseOneTimeWorkRequest(json);
51 | }
52 | } catch (Exception e) {
53 | throw new IllegalArgumentException("Failed to parse WorkRequest from " + json, e);
54 | }
55 | }
56 |
57 | @NonNull
58 | private static WorkRequest parseOneTimeWorkRequest(@NonNull Map json) {
59 | OneTimeWorkRequest.Builder builder = new OneTimeWorkRequest.Builder(BackgroundWorker.class);
60 | populateRequestBuilder(builder, json);
61 | return builder.build();
62 | }
63 |
64 | @SuppressWarnings("ConstantConditions")
65 | @NonNull
66 | private static WorkRequest parsePeriodicWorkRequest(@NonNull Map json) {
67 | long repeatInterval = ((Number) json.get("repeatInterval")).longValue();
68 | Number flexInterval = (Number) json.get("flexInterval");
69 |
70 | PeriodicWorkRequest.Builder builder = flexInterval != null ?
71 | new PeriodicWorkRequest.Builder(BackgroundWorker.class,
72 | repeatInterval, TimeUnit.MICROSECONDS,
73 | flexInterval.longValue(), TimeUnit.MICROSECONDS) :
74 | new PeriodicWorkRequest.Builder(BackgroundWorker.class,
75 | repeatInterval, TimeUnit.MICROSECONDS);
76 |
77 | populateRequestBuilder(builder, json);
78 | return builder.build();
79 | }
80 |
81 | private static void populateRequestBuilder(WorkRequest.Builder builder, @NonNull Map json) {
82 | Object tagsJson = json.get("tags");
83 | if (tagsJson instanceof Iterable) {
84 | for (Object tag : (List) tagsJson) {
85 | builder.addTag((String) tag);
86 | }
87 | }
88 |
89 | Object delayJson = json.get("initialDelay");
90 | if (delayJson instanceof Number) {
91 | long delay = ((Number) delayJson).longValue();
92 | if (delay > 0) {
93 | builder.setInitialDelay(delay, TimeUnit.MICROSECONDS);
94 | }
95 | }
96 |
97 | Constraints constraints = parseConstraints(json.get("constraints"));
98 | if (constraints != null) {
99 | builder.setConstraints(constraints);
100 | }
101 |
102 | Pair backoffCriteria = parseBackoffCriteria(json.get("backoffCriteria"));
103 | if (backoffCriteria != null) {
104 | //noinspection ConstantConditions
105 | builder.setBackoffCriteria(backoffCriteria.first,
106 | backoffCriteria.second, TimeUnit.MICROSECONDS);
107 | }
108 |
109 | Object inputJson = json.get("input");
110 | if (inputJson instanceof Map) {
111 | //noinspection unchecked
112 | builder.setInputData(new Data.Builder()
113 | .putAll((Map) inputJson)
114 | .build());
115 | }
116 | }
117 |
118 | @Nullable
119 | private static Pair parseBackoffCriteria(@Nullable Object json) {
120 | if (json instanceof Map) {
121 | Map criteria = (Map) json;
122 | Integer policy = (Integer) criteria.get("policy");
123 | if (policy != null) {
124 | Number delay = (Number) criteria.get("delay");
125 | if (delay != null) {
126 | return new Pair<>(
127 | policy == 1 ? BackoffPolicy.LINEAR : BackoffPolicy.EXPONENTIAL,
128 | delay.longValue()
129 | );
130 | }
131 | }
132 | }
133 |
134 | return null;
135 | }
136 |
137 | @SuppressWarnings("ConstantConditions")
138 | @Nullable
139 | private static Constraints parseConstraints(@Nullable Object json) {
140 | if (!(json instanceof Map)) {
141 | return null;
142 | }
143 |
144 | Map constraintsJson = (Map) json;
145 | Constraints.Builder builder = new Constraints.Builder();
146 |
147 | if (constraintsJson.get("networkType") != null) {
148 | builder.setRequiredNetworkType(
149 | NetworkType.values()[(Integer) constraintsJson.get("networkType")]);
150 | }
151 | if (constraintsJson.get("batteryNotLow") != null) {
152 | builder.setRequiresBatteryNotLow((Boolean) constraintsJson.get("batteryNotLow"));
153 | }
154 | if (constraintsJson.get("charging") != null) {
155 | builder.setRequiresCharging((Boolean) constraintsJson.get("charging"));
156 | }
157 | if (Build.VERSION.SDK_INT >= 23 && constraintsJson.get("deviceIdle") != null) {
158 | builder.setRequiresDeviceIdle((Boolean) constraintsJson.get("deviceIdle"));
159 | }
160 | if (constraintsJson.get("storageNotLow") != null) {
161 | builder.setRequiresStorageNotLow((Boolean) constraintsJson.get("storageNotLow"));
162 | }
163 |
164 | return builder.build();
165 | }
166 | }
167 |
--------------------------------------------------------------------------------
/ios/Classes/internal/BGTaskMgrDelegate.m:
--------------------------------------------------------------------------------
1 | //
2 | // BGTaskMgrDelegate.m
3 | // flt_worker
4 | //
5 | // Created by Yingxin Wu on 2020/3/12.
6 | //
7 |
8 | #import "BGTaskMgrDelegate.h"
9 | #import "BGTaskHandler.h"
10 | #import "utils.h"
11 | #import
12 |
13 | //#ifdef DEBUG
14 | //#import
15 | //#import
16 | //#endif
17 |
18 | @implementation BGTaskMgrDelegate
19 |
20 | // register background task indentifier/handler pairs
21 | + (void)registerBGTaskHandler {
22 | if (@available(iOS 13.0, *)) {
23 | NSArray *bgTaskIds = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"BGTaskSchedulerPermittedIdentifiers"];
24 | for (NSString *taskId in bgTaskIds) {
25 | [BGTaskScheduler.sharedScheduler registerForTaskWithIdentifier:taskId
26 | usingQueue:dispatch_get_main_queue()
27 | launchHandler:^(BGTask * _Nonnull task) {
28 | [BGTaskHandler.instance handleBGTask:task];
29 | }];
30 | }
31 | }
32 | }
33 |
34 | - (instancetype)initWithRegistrar:(NSObject*)registrar {
35 | self = [super init];
36 | if (self) {
37 | _methodChannel = [FlutterMethodChannel methodChannelWithName:@PLUGIN_PKG
38 | binaryMessenger:[registrar messenger]];
39 | }
40 | return self;
41 | }
42 |
43 | - (instancetype)initWithEngine:(FlutterEngine *)engine {
44 | self = [super init];
45 | if (self) {
46 | _methodChannel = [FlutterMethodChannel methodChannelWithName:@PLUGIN_PKG
47 | binaryMessenger:[engine binaryMessenger]];
48 | }
49 | return self;
50 | }
51 |
52 | - (void)saveHandles:(NSArray *)args {
53 | if (args.count > 1) {
54 | [workerDefaults() setObject:args[0] forKey:@DISPATCHER_KEY];
55 | [workerDefaults() setObject:args[1] forKey:@WORKER_KEY];
56 | } else {
57 | NSLog(@"Dispatcher & Worker callbacks are required!");
58 | }
59 | }
60 |
61 | /** Caches an extra input data for the given task */
62 | - (void)saveExtrasForTask:(NSString*)identifier extras:(NSString*)extras {
63 | [workerDefaults() setObject:extras forKey:TASK_KEY(identifier)];
64 | }
65 |
66 | /** Makes a payload dict used as input of the dart worker */
67 | - (NSDictionary*)packPayloadForTask:(NSString*)identifier {
68 | return @{
69 | @"id": identifier,
70 | @"tags": @[identifier],
71 | @"input": @{
72 | @"data": [workerDefaults() objectForKey:TASK_KEY(identifier)],
73 | },
74 | };
75 | }
76 |
77 | - (BOOL)handleMethodCall:(FlutterMethodCall*)call result:(FlutterResult)result {
78 | BOOL handled = YES;
79 | NSString *method = call.method;
80 | id args = call.arguments;
81 | if (@available(iOS 13.0, *)) {
82 | if ([@API_METHOD(submitTaskRequest) isEqualToString:method]) {
83 | BGTaskRequest *req = [self parseTaskRequest:args];
84 | BOOL submitted = [BGTaskScheduler.sharedScheduler submitTaskRequest:req error:nil];
85 | result(@(submitted));
86 | } else if ([@API_METHOD(cancelTaskRequest) isEqualToString:method]) {
87 | [BGTaskScheduler.sharedScheduler cancelTaskRequestWithIdentifier:args];
88 | result(nil);
89 | } else if ([@API_METHOD(cancelAllTaskRequests) isEqualToString:method]) {
90 | [BGTaskScheduler.sharedScheduler cancelAllTaskRequests];
91 | result(nil);
92 | } else if ([@API_METHOD(simulateLaunchTask) isEqualToString:method]) {
93 | [self simulateLaunchTask:args];
94 | result(nil);
95 | } else {
96 | handled = NO;
97 | }
98 | }
99 |
100 | return handled;
101 | }
102 |
103 | - (BGTaskRequest*)parseTaskRequest:(id)arguments API_AVAILABLE(ios(13.0)){
104 | BGTaskRequest *req;
105 | NSString *type = arguments[@"type"];
106 | NSString *identifier = arguments[@"identifier"];
107 | NSNumber *date = arguments[@"earliestBeginDate"];
108 | NSDate *earliestBeginDate = nil;
109 | if (IS_NONNULL(date)) {
110 | earliestBeginDate = [NSDate dateWithTimeIntervalSince1970:(date.doubleValue / 1000.0)];
111 | }
112 |
113 | if ([type isEqual:@"Processing"]) {
114 | BGProcessingTaskRequest *processReq = [[BGProcessingTaskRequest alloc] initWithIdentifier:identifier];
115 | req = processReq;
116 |
117 | id power = arguments[@"requiresExternalPower"];
118 | if (IS_NONNULL(power)) {
119 | processReq.requiresExternalPower = [power boolValue];
120 | }
121 |
122 | id network = arguments[@"requiresNetworkConnectivity"];
123 | if (IS_NONNULL(network)) {
124 | processReq.requiresNetworkConnectivity = [network boolValue];
125 | }
126 | } else {
127 | BGAppRefreshTaskRequest *refreshReq = [[BGAppRefreshTaskRequest alloc] initWithIdentifier:identifier];
128 | req = refreshReq;
129 | }
130 |
131 | req.earliestBeginDate = earliestBeginDate;
132 |
133 | // parse & cache extra input data
134 | NSString *extras = arguments[@"input"];
135 | [self saveExtrasForTask:identifier extras:extras];
136 | return req;
137 | }
138 |
139 | /** Simulate launch BGTask with the given identifier, using reflection & private APIs, for debugging only */
140 | - (void)simulateLaunchTask:(NSString*)identifier {
141 | //#ifdef DEBUG
142 | // Method simulateMethod = nil;
143 | // SEL simulateSel = nil;
144 | // unsigned int numMethods = 0;
145 | // Method *methods = class_copyMethodList([BGTaskScheduler class], &numMethods);
146 | //
147 | // for (int i = 0; i < numMethods; i++) {
148 | // SEL sel = method_getName(methods[i]);
149 | // const char *name = sel_getName(sel);
150 | // if (strcmp("_simulateLaunchForTaskWithIdentifier:", name) == 0) {
151 | // simulateMethod = methods[i];
152 | // simulateSel = sel;
153 | // break;
154 | // }
155 | // }
156 | //
157 | // if (simulateMethod) {
158 | // // only works on simulator??
159 | //// ((void (*)(id, SEL, ...))objc_msgSend)([BGTaskScheduler sharedScheduler], simulateSel, identifier);
160 | //// ((void (*)(id, Method, ...))method_invoke)([BGTaskScheduler sharedScheduler], simulateMethod, identifier);
161 | // }
162 | // if (methods) {
163 | // free(methods);
164 | // }
165 | //#endif
166 | }
167 |
168 | @end
169 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # flt_worker
2 |
3 | [![Pub][pub-badge]][pub]
4 | [![Check Status][check-badge]][github-runs]
5 | [![MIT][license-badge]][license]
6 |
7 | The `flt_worker` plugin allows you to schedule and execute Dart-written background tasks in a dedicated isolate, by utilizing the [WorkManager] API on Android, and the [BackgroundTasks] API on iOS 13.0+, respectively.
8 |
9 | Background processing is suitable for time-consuming tasks like downloading/uploading offline data, fitting a machine learning model, etc. You can use this plugin to schedule work like that. A pre-registed Dart worker will be launched and run in the background whenever the system decides to run the task.
10 |
11 | ## Integration
12 |
13 | Add a dependency to `pubspec.yaml`:
14 | ```yaml
15 | dependencies:
16 | flt_worker: ^0.1.0
17 | ```
18 |
19 | A worker is running in a separate instance of Flutter engine. Any plugins needed in the worker have to be registered again. In the following example, the `path_provider` plugin is registered for the background isolate.
20 |
21 | iOS:
22 | ```obj-c
23 | - (BOOL)application:(UIApplication *)application
24 | didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
25 | [GeneratedPluginRegistrant registerWithRegistry:self];
26 |
27 | // set a callback to register all plugins to a headless engine instance
28 | FltWorkerPlugin.registerPlugins = ^(NSObject *registry) {
29 | [FLTPathProviderPlugin registerWithRegistrar:[registry registrarForPlugin:@"FLTPathProviderPlugin"]];
30 | };
31 | ...
32 | }
33 | ```
34 |
35 | Android:
36 | ```java
37 | @Override
38 | public void configureFlutterEngine(@NonNull FlutterEngine flutterEngine) {
39 | GeneratedPluginRegistrant.registerWith(flutterEngine);
40 |
41 | // set a callback to register all plugins to a headless engine instance
42 | FltWorkerPlugin.registerPluginsForWorkers = registry -> {
43 | io.flutter.plugins.pathprovider.PathProviderPlugin.registerWith(
44 | registry.registrarFor("io.flutter.plugins.pathprovider.PathProviderPlugin"));
45 | return null;
46 | };
47 | }
48 | ```
49 |
50 | Fortunately, `flt_worker` itself is always available for the worker, so you don't have to register it again.
51 |
52 | One more thing has to be done if you're working on iOS: all task identifiers must be registered before you can subimit any `BGTaskRequest`.
53 |
54 | Add lines like this to the `Info.plist` file:
55 |
56 | ```xml
57 | BGTaskSchedulerPermittedIdentifiers
58 |
59 | com.example.counter_task
60 | dev.example.task2
61 | ...
62 |
63 | ```
64 |
65 | ## Usage
66 |
67 | ### Initialization & work dispatcher
68 |
69 | Before you can schedule background tasks, a worker callback must be registerted to the plugin:
70 |
71 | ```dart
72 | import 'package:flt_worker/flt_worker.dart';
73 |
74 | void main() {
75 | runApp(MyApp());
76 | initializeWorker(worker);
77 | }
78 | ```
79 |
80 | Please notice that the callback must be a [top-level or static function][CallbackHandle].
81 |
82 | The `worker` function acts as a dispatcher of all background tasks, you can call different functions according to the payload of the work, and return a `Future` so that the plugin can notify the system scheduler whenever the work is done.
83 |
84 | ```dart
85 | Future worker(WorkPayload payload) {
86 | if (payload.tags.contains('download')) {
87 | return _fetchData();
88 | } else if (...) {
89 | ...
90 | } else {
91 | return Future.value();
92 | }
93 | }
94 |
95 | /// Cache data for offline use
96 | Future _fetchData() async {
97 | // fetch data & update local storage
98 | }
99 | ```
100 |
101 | ### Scheduling work
102 |
103 | You can use the `enqueueWorkIntent` function to schedule a background `WorkIntent` like this:
104 |
105 | ```dart
106 | enqueueWorkIntent(WorkIntent(
107 | identifier: 'counter',
108 | initialDelay: Duration(seconds: 59),
109 | input: {
110 | 'counter': counter,
111 | },
112 | ));
113 | ```
114 |
115 | The name of `WorkIntent` is chosen to avoid conflict with the term `WorkRequest` from the [WorkManager] API for Android.
116 |
117 | Please see the documentation and also the example app to find out how to schedule different kinds of background work.
118 |
119 | ## High-level vs. Low-level APIs
120 |
121 | The background processing strategies and APIs are quite different on the Android and iOS platforms. The `flt_worker` plugin manages to provide a unified yet simplified API for general tasks, as the above example.
122 |
123 | However, to leverage the full power of each platform's background processing features, you may consider the low-level platform-specific APIs.
124 |
125 | For example, you can schedule a periodic work using the `WorkManager` APIs on an Android device:
126 |
127 | ```dart
128 | import 'package:flt_worker/android.dart';
129 |
130 | Future _startPolling() async {
131 | await cancelAllWorkByTag('tag'); // cancel the previous work
132 | await enqueueWorkRequest(const PeriodicWorkRequest(
133 | repeatInterval: Duration(hours: 4),
134 | flexInterval: Duration(minutes: 5),
135 | tags: ['tag'],
136 | constraints: WorkConstraints(
137 | networkType: NetworkType.connected,
138 | storageNotLow: true,
139 | ),
140 | backoffCriteria: BackoffCriteria(
141 | policy: BackoffPolicy.linear,
142 | delay: Duration(minutes: 1),
143 | ),
144 | ));
145 | }
146 | ```
147 |
148 | Or to use the `BackgroundTasks` APIs on iOS 13.0+:
149 |
150 | ```dart
151 | import 'package:flt_worker/ios.dart';
152 |
153 | void _increaseCounter(int counter) {
154 | submitTaskRequest(BGProcessingTaskRequest(
155 | 'com.example.counter_task',
156 | earliestBeginDate: DateTime.now().add(Duration(seconds: 10)),
157 | requiresNetworkConnectivity: false,
158 | requiresExternalPower: true,
159 | input: {
160 | 'counter': counter,
161 | },
162 | ));
163 | }
164 | ```
165 |
166 | ## Limitations
167 |
168 | It's the very beginning of this library, some limitations you may need to notice are:
169 |
170 | - It relies on the `BackgroundTasks` framework, which means it's not working on iOS before `13.0`
171 | - For the Android platform, advanced features of `WorkManager` like [Chaining Work] are not yet supported
172 |
173 | [github-runs]: https://github.com/xinthink/flt_worker/actions
174 | [check-badge]: https://github.com/xinthink/flt_worker/workflows/check/badge.svg
175 | [codecov-badge]: https://codecov.io/gh/xinthink/flt_worker/branch/master/graph/badge.svg
176 | [codecov]: https://codecov.io/gh/xinthink/flt_worker
177 | [license-badge]: https://img.shields.io/github/license/xinthink/flt_worker
178 | [license]: https://raw.githubusercontent.com/xinthink/flt_worker/master/LICENSE
179 | [pub]: https://pub.dev/packages/flt_worker
180 | [pub-badge]: https://img.shields.io/pub/v/flt_worker.svg
181 | [WorkManager]: https://developer.android.com/topic/libraries/architecture/workmanager
182 | [BackgroundTasks]: https://developer.apple.com/documentation/backgroundtasks
183 | [CallbackHandle]: https://api.flutter.dev/flutter/dart-ui/PluginUtilities/getCallbackHandle.html
184 | [Chaining Work]: https://developer.android.com/topic/libraries/architecture/workmanager/how-to/chain-work
185 |
--------------------------------------------------------------------------------
/example/pubspec.lock:
--------------------------------------------------------------------------------
1 | # Generated by pub
2 | # See https://dart.dev/tools/pub/glossary#lockfile
3 | packages:
4 | archive:
5 | dependency: transitive
6 | description:
7 | name: archive
8 | url: "https://pub.flutter-io.cn"
9 | source: hosted
10 | version: "2.0.11"
11 | args:
12 | dependency: transitive
13 | description:
14 | name: args
15 | url: "https://pub.flutter-io.cn"
16 | source: hosted
17 | version: "1.5.2"
18 | async:
19 | dependency: transitive
20 | description:
21 | name: async
22 | url: "https://pub.flutter-io.cn"
23 | source: hosted
24 | version: "2.4.0"
25 | boolean_selector:
26 | dependency: transitive
27 | description:
28 | name: boolean_selector
29 | url: "https://pub.flutter-io.cn"
30 | source: hosted
31 | version: "1.0.5"
32 | charcode:
33 | dependency: transitive
34 | description:
35 | name: charcode
36 | url: "https://pub.flutter-io.cn"
37 | source: hosted
38 | version: "1.1.2"
39 | collection:
40 | dependency: transitive
41 | description:
42 | name: collection
43 | url: "https://pub.flutter-io.cn"
44 | source: hosted
45 | version: "1.14.11"
46 | convert:
47 | dependency: transitive
48 | description:
49 | name: convert
50 | url: "https://pub.flutter-io.cn"
51 | source: hosted
52 | version: "2.1.1"
53 | crypto:
54 | dependency: transitive
55 | description:
56 | name: crypto
57 | url: "https://pub.flutter-io.cn"
58 | source: hosted
59 | version: "2.1.3"
60 | cupertino_icons:
61 | dependency: "direct main"
62 | description:
63 | name: cupertino_icons
64 | url: "https://pub.flutter-io.cn"
65 | source: hosted
66 | version: "0.1.3"
67 | flt_worker:
68 | dependency: "direct dev"
69 | description:
70 | path: ".."
71 | relative: true
72 | source: path
73 | version: "0.1.0"
74 | flutter:
75 | dependency: "direct main"
76 | description: flutter
77 | source: sdk
78 | version: "0.0.0"
79 | flutter_test:
80 | dependency: "direct dev"
81 | description: flutter
82 | source: sdk
83 | version: "0.0.0"
84 | http:
85 | dependency: "direct main"
86 | description:
87 | name: http
88 | url: "https://pub.flutter-io.cn"
89 | source: hosted
90 | version: "0.12.0+4"
91 | http_parser:
92 | dependency: transitive
93 | description:
94 | name: http_parser
95 | url: "https://pub.flutter-io.cn"
96 | source: hosted
97 | version: "3.1.3"
98 | image:
99 | dependency: transitive
100 | description:
101 | name: image
102 | url: "https://pub.flutter-io.cn"
103 | source: hosted
104 | version: "2.1.4"
105 | intl:
106 | dependency: "direct main"
107 | description:
108 | name: intl
109 | url: "https://pub.flutter-io.cn"
110 | source: hosted
111 | version: "0.16.1"
112 | matcher:
113 | dependency: transitive
114 | description:
115 | name: matcher
116 | url: "https://pub.flutter-io.cn"
117 | source: hosted
118 | version: "0.12.6"
119 | meta:
120 | dependency: transitive
121 | description:
122 | name: meta
123 | url: "https://pub.flutter-io.cn"
124 | source: hosted
125 | version: "1.1.8"
126 | path:
127 | dependency: transitive
128 | description:
129 | name: path
130 | url: "https://pub.flutter-io.cn"
131 | source: hosted
132 | version: "1.6.4"
133 | path_provider:
134 | dependency: "direct main"
135 | description:
136 | name: path_provider
137 | url: "https://pub.flutter-io.cn"
138 | source: hosted
139 | version: "1.6.5"
140 | path_provider_macos:
141 | dependency: transitive
142 | description:
143 | name: path_provider_macos
144 | url: "https://pub.flutter-io.cn"
145 | source: hosted
146 | version: "0.0.4"
147 | path_provider_platform_interface:
148 | dependency: transitive
149 | description:
150 | name: path_provider_platform_interface
151 | url: "https://pub.flutter-io.cn"
152 | source: hosted
153 | version: "1.0.1"
154 | pedantic:
155 | dependency: transitive
156 | description:
157 | name: pedantic
158 | url: "https://pub.flutter-io.cn"
159 | source: hosted
160 | version: "1.9.0"
161 | petitparser:
162 | dependency: transitive
163 | description:
164 | name: petitparser
165 | url: "https://pub.flutter-io.cn"
166 | source: hosted
167 | version: "2.4.0"
168 | platform:
169 | dependency: transitive
170 | description:
171 | name: platform
172 | url: "https://pub.flutter-io.cn"
173 | source: hosted
174 | version: "2.2.1"
175 | plugin_platform_interface:
176 | dependency: transitive
177 | description:
178 | name: plugin_platform_interface
179 | url: "https://pub.flutter-io.cn"
180 | source: hosted
181 | version: "1.0.2"
182 | quiver:
183 | dependency: transitive
184 | description:
185 | name: quiver
186 | url: "https://pub.flutter-io.cn"
187 | source: hosted
188 | version: "2.0.5"
189 | sky_engine:
190 | dependency: transitive
191 | description: flutter
192 | source: sdk
193 | version: "0.0.99"
194 | source_span:
195 | dependency: transitive
196 | description:
197 | name: source_span
198 | url: "https://pub.flutter-io.cn"
199 | source: hosted
200 | version: "1.5.5"
201 | stack_trace:
202 | dependency: transitive
203 | description:
204 | name: stack_trace
205 | url: "https://pub.flutter-io.cn"
206 | source: hosted
207 | version: "1.9.3"
208 | stream_channel:
209 | dependency: transitive
210 | description:
211 | name: stream_channel
212 | url: "https://pub.flutter-io.cn"
213 | source: hosted
214 | version: "2.0.0"
215 | string_scanner:
216 | dependency: transitive
217 | description:
218 | name: string_scanner
219 | url: "https://pub.flutter-io.cn"
220 | source: hosted
221 | version: "1.0.5"
222 | term_glyph:
223 | dependency: transitive
224 | description:
225 | name: term_glyph
226 | url: "https://pub.flutter-io.cn"
227 | source: hosted
228 | version: "1.1.0"
229 | test_api:
230 | dependency: transitive
231 | description:
232 | name: test_api
233 | url: "https://pub.flutter-io.cn"
234 | source: hosted
235 | version: "0.2.15"
236 | typed_data:
237 | dependency: transitive
238 | description:
239 | name: typed_data
240 | url: "https://pub.flutter-io.cn"
241 | source: hosted
242 | version: "1.1.6"
243 | vector_math:
244 | dependency: transitive
245 | description:
246 | name: vector_math
247 | url: "https://pub.flutter-io.cn"
248 | source: hosted
249 | version: "2.0.8"
250 | watcher:
251 | dependency: "direct main"
252 | description:
253 | name: watcher
254 | url: "https://pub.flutter-io.cn"
255 | source: hosted
256 | version: "0.9.7+14"
257 | xml:
258 | dependency: transitive
259 | description:
260 | name: xml
261 | url: "https://pub.flutter-io.cn"
262 | source: hosted
263 | version: "3.5.0"
264 | sdks:
265 | dart: ">=2.5.0 <3.0.0"
266 | flutter: ">=1.10.0 <2.0.0"
267 |
--------------------------------------------------------------------------------
/android/src/main/java/dev/thinkng/flt_worker/internal/AbsWorkerPlugin.java:
--------------------------------------------------------------------------------
1 | package dev.thinkng.flt_worker.internal;
2 |
3 | import android.content.Context;
4 | import android.content.SharedPreferences;
5 | import android.os.Handler;
6 | import android.os.Looper;
7 |
8 | import androidx.annotation.Keep;
9 | import androidx.annotation.NonNull;
10 | import androidx.annotation.UiThread;
11 | import androidx.work.Operation;
12 | import androidx.work.WorkManager;
13 | import androidx.work.WorkRequest;
14 |
15 | import java.util.List;
16 | import java.util.UUID;
17 | import java.util.concurrent.ExecutorService;
18 | import java.util.concurrent.Executors;
19 | import java.util.concurrent.Future;
20 | import java.util.concurrent.FutureTask;
21 |
22 | import io.flutter.embedding.engine.plugins.FlutterPlugin;
23 | import io.flutter.plugin.common.MethodCall;
24 | import io.flutter.plugin.common.MethodChannel;
25 | import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
26 | import io.flutter.plugin.common.MethodChannel.Result;
27 |
28 | @Keep
29 | public abstract class AbsWorkerPlugin implements FlutterPlugin, MethodCallHandler {
30 | static final String TAG = "FltWorker";
31 | protected static final String CHANNEL_NAME = "dev.thinkng.flt_worker";
32 | protected static final String METHOD_PREFIX = "FltWorkerPlugin#";
33 |
34 | protected Context context;
35 | private SharedPreferences prefs;
36 | private final ExecutorService workMgrExecutor = Executors.newCachedThreadPool();
37 | private Handler mainHandler;
38 |
39 | /// The MethodChannel that will the communication between Flutter and native Android
40 | ///
41 | /// This local reference serves to register the plugin with the Flutter Engine and unregister it
42 | /// when the Flutter Engine is detached from the Activity
43 | MethodChannel channel;
44 |
45 | public AbsWorkerPlugin() {
46 | }
47 |
48 | public AbsWorkerPlugin(Context context) {
49 | this.context = context.getApplicationContext();
50 | }
51 |
52 | protected SharedPreferences getPrefs() {
53 | if (prefs == null) {
54 | prefs = context.getSharedPreferences(CHANNEL_NAME, Context.MODE_PRIVATE);
55 | }
56 | return prefs;
57 | }
58 |
59 | @SuppressWarnings("deprecation")
60 | @Override
61 | public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
62 | if (context == null) {
63 | context = flutterPluginBinding.getApplicationContext();
64 | }
65 | channel = new MethodChannel(flutterPluginBinding.getFlutterEngine().getDartExecutor(), CHANNEL_NAME);
66 | channel.setMethodCallHandler(this);
67 | }
68 |
69 | @Override
70 | public void onDetachedFromEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
71 | if (channel != null) {
72 | channel.setMethodCallHandler(null);
73 | }
74 | workMgrExecutor.shutdownNow();
75 | }
76 |
77 | @UiThread
78 | @Override
79 | final public void onMethodCall(@NonNull MethodCall call, @NonNull Result result) {
80 | if(!handleMethodCall(call, result)) {
81 | result.notImplemented();
82 | }
83 | }
84 |
85 | @SuppressWarnings({"unused", "BooleanMethodIsAlwaysInverted"})
86 | public boolean handleMethodCall(@NonNull MethodCall call, @NonNull Result result) {
87 | boolean handled = true;
88 | switch (call.method) {
89 | case METHOD_PREFIX + "enqueue":
90 | enqueue(call, result);
91 | break;
92 | case METHOD_PREFIX + "cancelAllWorkByTag":
93 | cancelAllWorkByTag(call, result);
94 | break;
95 | case METHOD_PREFIX + "cancelUniqueWork":
96 | cancelUniqueWork(call, result);
97 | break;
98 | case METHOD_PREFIX + "cancelWorkById":
99 | cancelWorkById(call, result);
100 | break;
101 | case METHOD_PREFIX + "cancelAllWork":
102 | cancelAllWork(call, result);
103 | break;
104 | default:
105 | handled = false;
106 | break;
107 | }
108 | return handled;
109 | }
110 |
111 | private void enqueue(@NonNull MethodCall call, @NonNull final Result result) {
112 | List extends WorkRequest> requests = WorkRequests.parseRequests(call.arguments);
113 | if (requests == null || requests.isEmpty()) {
114 | result.success(false);
115 | return;
116 | }
117 |
118 | Operation op = WorkManager.getInstance(context).enqueue(requests);
119 | watchOperation(op, result, "enqueueWorkRequests");
120 | }
121 |
122 | private void cancelAllWorkByTag(@NonNull MethodCall call, @NonNull final Result result) {
123 | Operation op = WorkManager.getInstance(context).cancelAllWorkByTag((String) call.arguments);
124 | watchOperation(op, result, "cancelAllWorkByTag");
125 | }
126 |
127 | private void cancelUniqueWork(@NonNull MethodCall call, @NonNull final Result result) {
128 | Operation op = WorkManager.getInstance(context).cancelUniqueWork((String) call.arguments);
129 | watchOperation(op, result, "cancelUniqueWork");
130 | }
131 |
132 | private void cancelWorkById(@NonNull MethodCall call, @NonNull final Result result) {
133 | Operation op = WorkManager.getInstance(context).cancelWorkById(UUID.fromString((String) call.arguments));
134 | watchOperation(op, result, "cancelWorkById");
135 | }
136 |
137 | private void cancelAllWork(@NonNull MethodCall call, @NonNull final Result result) {
138 | Operation op = WorkManager.getInstance(context).cancelAllWork();
139 | watchOperation(op, result, "cancelAllWork");
140 | }
141 |
142 | /**
143 | * Waits for the operation's completion, and reports the result.
144 | */
145 | private void watchOperation(@NonNull final Operation operation,
146 | @NonNull final Result result,
147 | final String operationDesc) {
148 | workMgrExecutor.execute(new Runnable() {
149 | @Override
150 | public void run() {
151 | Throwable err = null;
152 | try {
153 | operation.getResult().get();
154 | } catch (Throwable e) {
155 | err = e;
156 | }
157 | reportResult(result, err, operationDesc);
158 | }
159 | });
160 | }
161 |
162 | private void reportResult(@NonNull final Result result,
163 | final Throwable e,
164 | final String operationDesc) {
165 | // reporting results on UI thread
166 | runOnMainThread(new Runnable() {
167 | @Override
168 | public void run() {
169 | if (e != null) {
170 | result.error("E", "Failed to " + operationDesc + ": " + e.getMessage(), null);
171 | } else {
172 | result.success(true);
173 | }
174 | }
175 | });
176 | }
177 |
178 | private void ensureMainHandler() {
179 | synchronized (AbsWorkerPlugin.class) {
180 | if (mainHandler == null) {
181 | mainHandler = new Handler(Looper.getMainLooper());
182 | }
183 | }
184 | }
185 |
186 | // /** Run the given [callable] on the main thread. */
187 | // Future runOnMainThread(final Callable callable) {
188 | // ensureMainHandler();
189 | // FutureTask task = new FutureTask<>(callable);
190 | // mainHandler.post(task);
191 | // return task;
192 | // }
193 |
194 | /** Run the given [runnable] on the main thread. */
195 | Future runOnMainThread(final Runnable runnable) {
196 | ensureMainHandler();
197 | FutureTask task = new FutureTask<>(runnable, null);
198 | mainHandler.post(task);
199 | return task;
200 | }
201 | }
202 |
--------------------------------------------------------------------------------