diff --git a/ANDROID_SETUP.md b/ANDROID_SETUP.md deleted file mode 100644 index 6c519a66..00000000 --- a/ANDROID_SETUP.md +++ /dev/null @@ -1,24 +0,0 @@ -# Check your AndroidManifest.xml - -Check if you have the following in your `AndroidManifest.xml` file. - -```xml - -``` - -Ideally you should have this, if not follow the [upgrade guide](https://github.com/flutter/flutter/wiki/Upgrading-pre-1.12-Android-projects). -If for some reason you can't upgrade yet we still support the [older way of embedding](ANDROID_SETUP_V1.md): - -# How to Debug my background job - -Debugging a background task can be difficult, Android decides when is the best time to run. -There is no guaranteed way to enforce a run of a job even in debug mode. - -However to facilitate debugging, the plugin provides an `isInDebugMode` flag when initializing the plugin: `Workmanager().initialize(callbackDispatcher, isInDebugMode: true)` - -Once this flag is enabled you will receive a notification whenever a background task was triggered. -This way you can keep track whether that task ran successfully or not. - - diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 00000000..686020a1 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,208 @@ +# Contributing to Flutter Workmanager + +Thank you for your interest in contributing to Flutter Workmanager! This guide will help you get started. + +## Development Setup + +### Prerequisites +- Flutter SDK (latest stable version) +- Android Studio / Xcode for platform-specific development +- Melos for monorepo management + +### Getting Started + +1. Fork and clone the repository +2. Install melos globally: `dart pub global activate melos` +3. Bootstrap the workspace: `melos bootstrap` +4. Run tests: `melos run test` + +## Project Structure + +This is a federated plugin with the following packages: +- `workmanager/` - Main plugin package +- `workmanager_android/` - Android implementation +- `workmanager_apple/` - iOS/macOS implementation +- `workmanager_platform_interface/` - Shared interface +- `example/` - Demo application + +## Development Workflow + +### Making Changes + +1. Create a feature branch: `git checkout -b feature/your-feature` +2. Make your changes +3. Run formatting: `melos run format` +4. Run analysis: `melos run analyze` +5. Run tests: `melos run test` +6. Test on example app: `cd example && flutter run` + +### Code Generation + +If you modify the Pigeon API definition in `workmanager_platform_interface/pigeons/workmanager_api.dart`: + +```bash +# Regenerate Pigeon files +melos run generate:pigeon +``` + +**Important**: Never manually edit generated `*.g.*` files. + +### Code Formatting + +The project uses specific formatting rules: + +- **Dart**: Use `dart format` (configured to exclude generated files) +- **Kotlin**: Use `ktlint -F .` in root folder +- **Swift**: Use SwiftLint for formatting + +Generated files are automatically excluded from formatting checks. + +## Testing + +### Running Tests + +```bash +# All tests +melos run test + +# Specific package tests +cd workmanager_android && flutter test +cd workmanager_apple && flutter test + +# Native tests +cd example/android && ./gradlew :workmanager_android:test +cd example/ios && xcodebuild test -workspace Runner.xcworkspace -scheme Runner +``` + +### Integration Tests + +```bash +# iOS integration tests +melos run test:drive_ios + +# Android integration tests +melos run test:drive_android +``` + +### Example App Testing + +Always build the example app before completing your changes: + +```bash +cd example +flutter build apk --debug +flutter build ios --debug --no-codesign +``` + +## Platform-Specific Guidelines + +### Android +- Follow Android WorkManager best practices +- Test on various Android API levels +- Ensure background task constraints work properly + +### iOS +- Test both Background Fetch and BGTaskScheduler APIs +- Verify 30-second execution limits are respected +- Test on physical devices (background tasks don't work in simulator) + +## Publishing (Maintainers Only) + +### Pre-publish Checklist + +Before publishing any package, run the dry-run validation: + +```bash +# Validate all packages are ready for publishing +melos run publish:dry-run + +# Or for individual packages: +cd workmanager && dart pub publish --dry-run +cd workmanager_android && dart pub publish --dry-run +cd workmanager_apple && dart pub publish --dry-run +cd workmanager_platform_interface && dart pub publish --dry-run +``` + +This validates that: +- All dependencies are correctly specified +- No uncommitted changes exist +- Package follows pub.dev guidelines +- All required files are included + +### Version Management + +Use melos for coordinated version bumps: + +```bash +# Bump versions across related packages +melos version +``` + +### Publishing Process + +1. Ensure all tests pass: `melos run test` +2. Run dry-run validation: `melos run publish:dry-run` +3. Update CHANGELOGs for all modified packages +4. Create release PR with version bumps +5. After merge, tag release: `git tag v0.x.x` +6. Publish packages: `melos publish --no-dry-run` + +## Documentation + +### Updating Documentation + +- **API docs**: Documented inline in Dart code +- **User guides**: Located in `docs/` directory using docs.page +- **Setup guides**: Integrated into quickstart documentation + +### Documentation Structure + +- `docs/index.mdx` - Overview and features +- `docs/quickstart.mdx` - Installation and basic setup +- `docs/customization.mdx` - Advanced configuration +- `docs/debugging.mdx` - Troubleshooting guide + +### Testing Documentation + +Test documentation changes locally: +1. Push changes to a branch +2. View at: `https://docs.page/fluttercommunity/flutter_workmanager~your-branch` + +## GitHub Actions + +The project uses several CI workflows: + +- **Format** (`.github/workflows/format.yml`): Code formatting checks +- **Analysis** (`.github/workflows/analysis.yml`): Package analysis and dry-run validation +- **Test** (`.github/workflows/test.yml`): Unit tests, native tests, integration tests + +All checks must pass before merging PRs. + +## Common Issues + +### Generated Files + +If you see formatting or analysis errors in generated files: +- Never manually edit `*.g.*` files +- Use `melos run generate:pigeon` to regenerate +- Generated files are excluded from formatting by design + +### CI Failures + +**Package analysis failures**: Usually caused by uncommitted changes or missing dependencies +**Format failures**: Run `melos run format` locally first +**Test failures**: Ensure all tests pass locally with `melos run test` + +## Getting Help + +- **Bug reports**: [GitHub Issues](https://github.com/fluttercommunity/flutter_workmanager/issues) +- **Questions**: [GitHub Discussions](https://github.com/fluttercommunity/flutter_workmanager/discussions) +- **Documentation**: [docs.page](https://docs.page/fluttercommunity/flutter_workmanager) + +## Code of Conduct + +This project follows the [Flutter Community Code of Conduct](https://github.com/fluttercommunity/community/blob/main/CODE_OF_CONDUCT.md). + +## License + +By contributing to Flutter Workmanager, you agree that your contributions will be licensed under the [MIT License](LICENSE). \ No newline at end of file diff --git a/IOS_SETUP.md b/IOS_SETUP.md deleted file mode 100644 index 68a5c448..00000000 --- a/IOS_SETUP.md +++ /dev/null @@ -1,284 +0,0 @@ -# iOS Installation - -## Prerequisites - -This plugin is compatible with **Swift 4.2** and up. Make sure you are using **Xcode 10.3** or higher and have set your minimum deployment target to **iOS 10** or higher by defining a platform version in your podfile: `platform :ios, '10.0'` - - -## Enable BGTaskScheduler - -> ⚠️ BGTaskScheduler is similar to Background Fetch described below and brings a similar set of constraints. Most notably, there are no guarantees when the background task will be run. Excerpt from the documentation: -> -> Schedule a processing task request to ask that the system launch your app when conditions are favorable for battery life to handle deferrable, longer-running processing, such as syncing, database maintenance, or similar tasks. The system will attempt to fulfill this request to the best of its ability within the next two days as long as the user has used your app within the past week. -> -> Workmanager BGTaskScheduler methods `registerOneOffTask`, `registerPeriodicTask`, and `registerProcessingTask` are only available on iOS 13+ - -![Screenshot of Background Fetch Capabilities tab in Xcode ](.art/ios_background_mode_background_processing.png) - -This will add the **UIBackgroundModes** key to your project's `Info.plist`: - -``` xml -UIBackgroundModes - - processing - - - fetch - -``` - -You **MUST** amend your `AppDelegate.swift` and `Info.plist` file to register your task ID. - -- AppDelegate.swift -``` swift -import workmanager -``` - -``` swift -// In AppDelegate.application method -WorkmanagerPlugin.registerBGProcessingTask(withIdentifier: "task-identifier") - -// Register a periodic task in iOS 13+ -WorkmanagerPlugin.registerPeriodicTask(withIdentifier: "dev.fluttercommunity.workmanagerExample.iOSBackgroundAppRefresh", frequency: NSNumber(value: 20 * 60)) -``` - -- Info.plist -``` xml -BGTaskSchedulerPermittedIdentifiers - - task-identifier - - - dev.fluttercommunity.workmanagerExample.iOSBackgroundAppRefresh - -``` -> ⚠️ On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist for new `BGTaskScheduler` API disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval` -methods, which means you cannot use both old Background Fetch and new `registerPeriodicTask` at the same time, you have to choose one based on your minimum iOS target version. -For details see [Apple Docs](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app) - -And will set the correct *SystemCapabilities* for your target in the `project.pbxproj` file: - -``` -SystemCapabilities = { - com.apple.BackgroundModes = { - enabled = 1; - }; -}; -``` - -## Testing BGTaskScheduler - -Follow the instructions on https://developer.apple.com/documentation/backgroundtasks/starting_and_terminating_tasks_during_development. - -The exact command to trigger the WorkManager default BG Task is: - -``` -e -l objc -- (void)[[BGTaskScheduler sharedScheduler] _simulateLaunchForTaskWithIdentifier:@"workmanager.background.task"] -``` - -## Enabling Background Fetch - -> ⚠️ Background fetch is one supported way to do background work on iOS with work manager. Note that this API is deprecated starting iOS 13, however it still works on iOS 13+ as of writing this article - -> ⚠️ On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist for new `BGTaskScheduler` API disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval` -methods, which means you cannot use both old Background Fetch and new `registerPeriodicTask` at the same time, you have to choose one based on your minimum iOS target version. -For details see [Apple Docs](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app) - -Background fetching is very different compared to Android's Background Jobs. -In order for your app to support Background Fetch, you have to add the *Background Modes* capability in Xcode for your app's Target and check *Background fetch*: - -![Screenshot of Background Fetch Capabilities tab in Xcode ](.art/ios_background_mode_fetch.png) - -This will add the **UIBackgroundModes** key to your project's `Info.plist`: - -```xml -UIBackgroundModes - - fetch - -``` - -And will set the correct *SystemCapabilities* for your target in the `project.pbxproj` file: - -``` -SystemCapabilities = { - com.apple.BackgroundModes = { - enabled = 1; - }; -}; -``` - -Inside your app's delegate `didFinishLaunchingWithOptions`, set your desired **minimumBackgroundFetchInterval** : - - -```Swift -class AppDelegate:UIResponder,UIApplicationDelegate{ - func application(_ application:UIApplication,didFinishLaunchingWithOptions launchOptions:[UIApplicationLaunchOptionsKey:Any]?)->Bool{ - // Other intialization code… - UIApplication.shared.setMinimumBackgroundFetchInterval(TimeInterval(60*15)) - - return true - } -} -``` - -This ensures that the task is ran at most every 15 minutes. - -> 📝 Note: this is a **minimum** time interval, there's no guarantee on how often this will be called. - - - -## Testing Background Fetch - -You can wait for iOS to trigger the `performFetchWithCompletionHandler` but you as a developer have no control over *when* and how often iOS will allow your app to fetch data in the background: - -> When a good opportunity arises, the system wakes or launches your app into the background and calls the app delegate’s `application:performFetchWithCompletionHandler:` method. [...] **Apps that download small amounts of content quickly, and accurately reflect when they had content available to download, are more likely to receive execution time in the future** than apps that take a long time to download their content or that claim content was available but then do not download anything. - - - -> 📝 Note: also see relevant discussion in issue #23 - -But in order to test your implementation during development, you can *simulate* a `background fetch` in Xcode; go to `Debug` → `Simulate Background Fetch` - -![Screenshot of iOS Xcode's Debug menu](.art/ios_xcode_debug_menu.png) - -When the WorkManager plugin receives a `background fetch` event, it will start a new **Dart isolate**, using the entrypoint provided by the `initialize` method. - -Here is an example of a Flutter entrypoint called `callbackDispatcher`: - -```dart -@pragma('vm:entry-point') -void callbackDispatcher() { - Workmanager().executeTask((task, inputData) { - switch (task) { - case Workmanager.iOSBackgroundTask: - stderr.writeln("The iOS background fetch was triggered"); - break; - } - bool success = true; - return Future.value(success); - }); -} -``` - -If you then simulate a background fetch in Xcode, you should see `"The iOS background fetch was triggered"` log in Xcode's Console: - -![Screenshot of Xcode console's output](.art/ios_xcode_console.png) - -### Troubleshooting - -If the *Simulate Background Fetch* is greyed out in the *Debug* menu, that means Xcode's debugger is not attached to the current process. Attaching is done for you *automatically* when you run directly from Xcode. - -If you launched your app using the Flutter command line tools or another IDE like IntelliJ IDEA, you will need to attach to the running *Runner* process using the Debug menu in Xcode: - -![Screenshot of Xcode's attach to process menu item](.art/ios_xcode_attach_to_process.jpg) - -> 📝 Note that this feature of Xcode is not 100% reliable. For best results, run directly from Xcode - - - -## Debug mode - -To make background work more visible when developing, the WorkManager plugin provides an `isInDebugMode` flag when initializing the plugin: - -```dart -Workmanager().initialize(callbackDispatcher, isInDebugMode: true) -``` - -If `isInDebugMode` is `true`, a local notification will be displayed whenever a background fetch was triggered by iOS. In the example gif below, two background fetches were *simulated* in quick succession. Both completing succesfully after a few seconds in this case: - -![example of iOS debug notification](.art/ios_debug_notifications.gif) - - - -These are the three notification types, **start** work, finished **successfully**, finished with **failure**: - -![example of iOS debug notification](.art/ios_debug_notification_types.png) - -*Success* or *failure* depending on what you return in Dart: - -```dart -bool success = true; -return Future.value(success); -``` - - - -If your app is running in the **foreground**, notification banners are **not shown**. That is default behaviour on iOS. Triggering *Simulate Background Fetch* when running on a **real device** will put the app in the background *first*, before calling the `performFetchWithCompletionHandler`. This doesn't happen in the Simulator. So, make sure to go to the Home screen *before* triggering background fetch. - -> 📝 Note: the Home Indicator swipe-up gesture is sometimes tricky to do on a simulator. Use Simulator menu `Hardware` → `Home` or keyboard shortcut ⌘ command + ⇧ shift + H to make your life easier - -### Show notifications in foreground - -Alternatively, if you *do* want the banners to appear while your app is in the foreground you can implement `userNotificationCenter(_:willPresent:withCompletionHandler:)` of `UNUserNotificationCenterDelegate`. - -From the [Apple docs](https://developer.apple.com/documentation/usernotifications/unusernotificationcenterdelegate/1649518-usernotificationcenter): - -> If your **app is in the foreground** when a notification arrives, the shared user **notification center calls this method to deliver the notification directly to your app**. If you implement this method, you can take whatever actions are necessary to process the notification and update your app. When you finish, **call the completionHandler block and specify how you want the system to alert the user**, if at all. - -An easy way to get it working is by implementing it your `AppDelegate` like so: -```swift -class AppDelegate: FlutterAppDelegate { - // ... - override func userNotificationCenter(_ center: UNUserNotificationCenter, - willPresent notification: UNNotification, - withCompletionHandler completionHandler: @escaping (UNNotificationPresentationOptions) -> Void) { - completionHandler(.alert) // shows banner even if app is in foreground - } - -} -``` - -And assigning yourself as the delegate. Preferably as early as possible, in `application:didFinishLaunchingWithOptions:` - -```swift -UNUserNotificationCenter.current().delegate = self -``` - -Now the banners **are** shown when your app is running in the foreground 👇 - -![Screenshot of Simulator with app in foreground showing notification banner](.art/ios_notification_foreground.png) - -> 📝 Note: decide if implementing the delegate call makes sense for your app. You could make use of [active compilation conditions](https://blog.krzyzanowskim.com/2016/10/10/conditional-swift-testing/) to implement it *only* for the `Debug` configruation, for example - - - -## Registered plugins -Since the provided Flutter entry point is ran in a dedicated **Dart isolate**, the Flutter plugins which may -have been registered AppDelegate's `didFinishLaunchingWithOptions` (or somewhere else) are unavailable, -since they were registered on a different registry. - -In order to know when the Dart isolate has started, the plugin user may make use of the -WorkmanagerPlugin's `setPluginRegistrantCallback` function. For example : - -```Swift -class AppDelegate: FlutterAppDelegate { - /// Registers all pubspec-referenced Flutter plugins in the given registry. - static func registerPlugins(with registry: FlutterPluginRegistry) { - GeneratedPluginRegistrant.register(with: registry) - } - - override func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool { - - // ... Initialization code - - AppDelegate.registerPlugins(with: self) // Register the app's plugins in the context of a normal run - - WorkmanagerPlugin.setPluginRegistrantCallback { registry in - // The following code will be called upon WorkmanagerPlugin's registration. - // Note : all of the app's plugins may not be required in this context ; - // instead of using GeneratedPluginRegistrant.register(with: registry), - // you may want to register only specific plugins. - AppDelegate.registerPlugins(with: registry) - } - } -} -``` - - - -## Useful links - -- [Updating Your App with Background App Refresh](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/updating_your_app_with_background_app_refresh) - Apple Documentation - -- [UNUserNotificationCenterDelegate](https://developer.apple.com/documentation/usernotifications/unusernotificationcenterdelegate/1649518-usernotificationcenter) documentation diff --git a/README.md b/README.md index ef957757..a09b5e27 100644 --- a/README.md +++ b/README.md @@ -7,426 +7,34 @@ [![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/fluttercommunity/flutter_workmanager/test.yml?branch=main&label=tests)](https://github.com/fluttercommunity/flutter_workmanager/actions) [![license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/fluttercommunity/flutter_workmanager/blob/main/LICENSE) -Flutter WorkManager is a wrapper around [Android's WorkManager](https://developer.android.com/topic/libraries/architecture/workmanager), [iOS' performFetchWithCompletionHandler](https://developer.apple.com/documentation/uikit/uiapplicationdelegate/1623125-application) and [iOS BGAppRefreshTask](https://developer.apple.com/documentation/backgroundtasks/bgapprefreshtask), effectively enabling headless execution of Dart code in the background. +Execute Dart code in the background, even when your app is closed. Perfect for data sync, file uploads, and periodic maintenance tasks. -For iOS users, please watch this video on a general introduction to background processing: https://developer.apple.com/videos/play/wwdc2019/707. All of the constraints discussed in the video also apply to this plugin. +## 📖 Documentation -This is especially useful to run periodic tasks, such as fetching remote data on a regular basis. +Get started with background tasks in Flutter: -> This plugin was featured in this [Medium blogpost](https://medium.com/vrt-digital-studio/flutter-workmanager-81e0cfbd6f6e) +**[→ Quick Start Guide](https://docs.page/fluttercommunity/flutter_workmanager/quickstart)** - Installation and setup -## Federated Plugin Architecture +**[→ API Documentation](https://pub.dev/documentation/workmanager/latest/)** - Complete Dart API reference -This plugin uses a federated architecture, which means that the main `workmanager` package provides the API, while platform-specific implementations are in separate packages: +**[→ Debugging Guide](https://docs.page/fluttercommunity/flutter_workmanager/debugging)** - Troubleshooting help -- **workmanager**: The main package that provides the unified API -- **workmanager_platform_interface**: The common platform interface -- **workmanager_android**: Android-specific implementation -- **workmanager_apple**: Apple platform (iOS/macOS) implementation +## 🎯 Use Cases -This architecture allows for better platform-specific optimizations and easier maintenance. When you add `workmanager` to your `pubspec.yaml`, the platform-specific packages are automatically included through the endorsed federated plugin system. +Background tasks are perfect for: +- **Sync data from API** - Keep your app's data fresh +- **Upload files in background** - Reliable file uploads +- **Clean up old data** - Remove old files and cache +- **Fetch notifications** - Check for new messages +- **Database maintenance** - Optimize and clean databases -# Platform Setup -In order for background work to be scheduled correctly you should follow the Android and iOS setup first. +## 🐛 Issues & Support -- [Android Setup](https://github.com/fluttercommunity/flutter_workmanager/blob/master/ANDROID_SETUP.md) -- [iOS Setup](https://github.com/fluttercommunity/flutter_workmanager/blob/master/IOS_SETUP.md) +- **Bug reports**: [GitHub Issues →](https://github.com/fluttercommunity/flutter_workmanager/issues) +- **Questions**: [GitHub Discussions →](https://github.com/fluttercommunity/flutter_workmanager/discussions) +- **Documentation**: [docs.page/fluttercommunity/flutter_workmanager →](https://docs.page/fluttercommunity/flutter_workmanager) -## Publishing (For Maintainers) +## 🚀 Example App -This project uses a federated plugin architecture with multiple packages. To publish updates: - -1. **Update versions** in all `pubspec.yaml` files: - - `workmanager/pubspec.yaml` - - `workmanager_platform_interface/pubspec.yaml` - - `workmanager_android/pubspec.yaml` - - `workmanager_apple/pubspec.yaml` - -2. **Publish packages in order**: - ```bash - # 1. Publish platform interface first - cd workmanager_platform_interface && dart pub publish - - # 2. Publish platform implementations - cd ../workmanager_android && dart pub publish - cd ../workmanager_apple && dart pub publish - - # 3. Publish main package last - cd ../workmanager && dart pub publish - ``` - -3. **Update dependencies** in main package to point to pub.dev versions instead of path dependencies before publishing - -4. **Tag the release** with the version number: `git tag v0.8.0 && git push origin v0.8.0` - -# How to use the package? - -See sample folder for a complete working example. -Before registering any task, the WorkManager plugin must be initialized. - -```dart -@pragma('vm:entry-point') // Mandatory if the App is obfuscated or using Flutter 3.1+ -void callbackDispatcher() { - Workmanager().executeTask((task, inputData) { - print("Native called background task: $task"); //simpleTask will be emitted here. - return Future.value(true); - }); -} - -void main() { - Workmanager().initialize( - callbackDispatcher, // The top level function, aka callbackDispatcher - isInDebugMode: true // If enabled it will post a notification whenever the task is running. Handy for debugging tasks - ); - Workmanager().registerOneOffTask("task-identifier", "simpleTask"); - runApp(MyApp()); -} -``` - -> The `callbackDispatcher` needs to be either a static function or a top level function to be accessible as a Flutter entry point. - -The workmanager runs on a separate isolate from the main flutter isolate. Ensure to initialize all dependencies inside the `Workmanager().executeTask`. - -##### Debugging tips - -Wrap the code inside your `Workmanager().executeTask` in a `try and catch` in order to catch any exceptions thrown. - -```dart -@pragma('vm:entry-point') -void callbackDispatcher() { - Workmanager().executeTask((task, inputData) async { - - int? totalExecutions; - final _sharedPreference = await SharedPreferences.getInstance(); //Initialize dependency - - try { //add code execution - totalExecutions = _sharedPreference.getInt("totalExecutions"); - _sharedPreference.setInt("totalExecutions", totalExecutions == null ? 1 : totalExecutions+1); - } catch(err) { - Logger().e(err.toString()); // Logger flutter package, prints error on the debug console - throw Exception(err); - } - - return Future.value(true); - }); -} -``` - -Android tasks are identified using their `taskName`. -iOS tasks are identified using their `taskIdentifier`. - -However, there is an exception for iOS background fetch: `Workmanager.iOSBackgroundTask`, a constant for iOS background fetch task. - ---- - -# Work Result - -The `Workmanager().executeTask(...` block supports 3 possible outcomes: - -1. `Future.value(true)`: The task is successful. -2. `Future.value(false)`: The task did not complete successfully and needs to be retried. On Android, the retry is done automatically. On iOS (when using BGTaskScheduler), the retry needs to be scheduled manually. -3. `Future.error(...)`: The task failed. - -On Android, the `BackoffPolicy` will configure how `WorkManager` is going to retry the task. - -Refer to the example app for a successful, retrying and a failed task. - -# iOS specific setup and note - -Initialize Workmanager only once. -Background app refresh can only be tested on a real device, it cannot be tested on a simulator. - -### Migrate to 0.6.x -Version 0.6.x of this plugin has some breaking changes for iOS: -- Workmanager.registerOneOffTask was previously using iOS **BGProcessingTask**, now it will be an immediate run task which will continue in the background if user leaves the App. Since the previous solution meant the one off task will only run if the device is idle and as often experienced only when device is charging, in practice it means somewhere at night, or not at all during that day, because **BGProcessingTask** is meant for long running tasks. The new solution makes it more in line with Android except it does not support **initialDelay** -- If you need the old behavior you can use the new iOS only method `Workmanager.registerProcessingTask`: - 1. Replace `Workmanager().registerOneOffTask` with `Workmanager().registerProcessingTask` in your App - 1. Replace `WorkmanagerPlugin.registerTask` with `WorkmanagerPlugin.registerBGProcessingTask` in `AppDelegate.swift` -- Workmanager.registerOneOffTask does not support **initialDelay** -- Workmanager.registerOneOffTask now supports **inputData** which was always returning null in the previous solution -- Workmanager.registerOneOffTask now does NOT require `WorkmanagerPlugin.registerTask` call in `AppDelegate.swift` hence remove the call - -### One off tasks -iOS supports **One off tasks** only on iOS 13+ with a few basic constraints: - -`registerOneOffTask` starts immediately. It might run for only 30 seconds due to iOS restrictions. - -```dart -Workmanager().registerOneOffTask( - "task-identifier", - simpleTaskKey, // Ignored on iOS - initialDelay: Duration(minutes: 30), // Ignored on iOS - inputData: ... // fully supported -); -``` - -### Periodic tasks -iOS supports two types of **Periodic tasks**: -- On iOS 12 and lower you can use deprecated Background Fetch API, see [iOS Setup](./IOS_SETUP.md), even though the API is -deprecated by iOS it still works on iOS 13+ as of writing this article - -- `registerPeriodicTask` is only supported on iOS 13+, it might run for only 30 seconds due to iOS restrictions, but doesn't start immediately, rather iOS will schedule it as per user's App usage pattern. - -> ⚠️ On iOS 13+, adding a `BGTaskSchedulerPermittedIdentifiers` key to the Info.plist for new `BGTaskScheduler` API disables the `performFetchWithCompletionHandler` and `setMinimumBackgroundFetchInterval` -methods, which means you cannot use both old Background Fetch and new `registerPeriodicTask` at the same time, you have to choose one based on your minimum iOS target version. -For details see [Apple Docs](https://developer.apple.com/documentation/uikit/app_and_environment/scenes/preparing_your_ui_to_run_in_the_background/using_background_tasks_to_update_your_app) - -To use `registerPeriodicTask` first register the task in `Info.plist` and `AppDelegate.swift` [iOS Setup](./IOS_SETUP.md). Unlike Android, for iOS you have to set the frequency in `AppDelegate.swift`. The frequency is not guaranteed rather iOS will schedule it as per user's App usage pattern, iOS might take a few days to learn usage pattern. In reality frequency just means do not repeat the task before x seconds/minutes. If frequency is not provided it will default to 15 minutes. - -```objc -// Register a periodic task with 20 minutes frequency. The frequency is in seconds. -WorkmanagerPlugin.registerPeriodicTask(withIdentifier: "dev.fluttercommunity.workmanagerExample.iOSBackgroundAppRefresh", frequency: NSNumber(value: 20 * 60)) -``` - -Then schedule the task from your App -```dart -const iOSBackgroundAppRefresh = "dev.fluttercommunity.workmanagerExample.iOSBackgroundAppRefresh"; -Workmanager().registerPeriodicTask( - iOSBackgroundAppRefresh, - iOSBackgroundAppRefresh, - initialDelay: Duration(seconds: 10), - frequency: Duration(hours: 1), // Ignored on iOS, rather set in AppDelegate.swift - inputData: ... // Not supported -); -``` - -For more information see [BGAppRefreshTask](https://developer.apple.com/documentation/backgroundtasks/bgapprefreshtask) - -### Processing tasks -iOS supports **Processing tasks** only on iOS 13+ which can run for more than 30 seconds. - -`registerProcessingTask` is a long running one off background task, currently only for iOS. It can be run for more than 30 seconds but doesn't start immediately, rather iOS might schedule it when device is idle and charging. -Processing tasks are for long processes like data processing and app maintenance. Processing tasks can run for minutes, but the system can interrupt these. -iOS might terminate any running background processing tasks when the user starts using the device. -For more information see [BGProcessingTask](https://developer.apple.com/documentation/backgroundtasks/bgprocessingtask) - -```dart -const iOSBackgroundProcessingTask = "dev.fluttercommunity.workmanagerExample.iOSBackgroundProcessingTask"; -Workmanager().registerProcessingTask( - iOSBackgroundProcessingTask, - iOSBackgroundProcessingTask, - initialDelay: Duration(minutes: 2), - constraints: Constraints( - // Connected or metered mark the task as requiring internet - networkType: NetworkType.connected, - // Require external power - requiresCharging: true, - ), -); -``` - -### Background App Refresh permission - -On iOS user can disable `Background App Refresh` permission anytime, hence background tasks can only run if user has granted the permission. - -Use `permision_handler` to check for the permission: - -``` dart -final status = await Permission.backgroundRefresh.status; -if (status != PermissionStatus.granted) { - _showNoPermission(context, status); - return; -} -``` - -For more information see the [BGTaskScheduler documentation](https://developer.apple.com/documentation/backgroundtasks). - -### Print scheduled tasks -On iOS you can print scheduled tasks using `Workmanager.printScheduledTasks` - -It prints task details to console. To be used during development/debugging. -Currently only supported on iOS and only on iOS 13+. - -```dart -if (Platform.isIOS) { - Workmanager().printScheduledTasks(); - // Prints: [BGTaskScheduler] Task Identifier: iOSBackgroundAppRefresh earliestBeginDate: 2023.10.10 PM 11:10:12 - // Or: [BGTaskScheduler] There are no scheduled tasks -} -``` - - -# Customisation (Android) - -Not every `Android WorkManager` feature is ported. - -Two kinds of background tasks can be registered : - -- **One off task** : runs only once -- **Periodic tasks** : runs indefinitely on a regular basis - -```dart -// One off task registration -Workmanager().registerOneOffTask( - "oneoff-task-identifier", - "simpleTask" -); - -// Periodic task registration -Workmanager().registerPeriodicTask( - "periodic-task-identifier", - "simplePeriodicTask", - // When no frequency is provided the default 15 minutes is set. - // Minimum frequency is 15 min. Android will automatically change your frequency to 15 min if you have configured a lower frequency. - frequency: Duration(hours: 1), -) -``` - -Each task must have an **unique name**; -This allows cancellation of a started task. -The second parameter is the `String` that will be sent to your `callbackDispatcher` function, indicating the task's _type_. - -## Tagging - -You can set the optional `tag` property. -Handy for cancellation by `tag`. -This is different from the unique name in that you can group multiple tasks under one tag. - -```dart -Workmanager().registerOneOffTask("1", "simpleTask", tag: "tag"); -``` - -## Existing Work Policy - -Indicates the desired behaviour when the same task is scheduled more than once. -The default is `keep` - -```dart -Workmanager().registerOneOffTask("1", "simpleTask", existingWorkPolicy: ExistingWorkPolicy.append); -``` - -## Initial Delay - -Indicates how along a task should waitbefore its first run. - -```dart -Workmanager().registerOneOffTask("1", "simpleTask", initialDelay: Duration(seconds: 10)); -``` - -## Constraints - -> Constraints are mapped at best effort to each platform. Android's WorkManager supports most of the specific constraints, whereas iOS tasks are limited. - -- NetworkType - Constrains the type of network required for your work to run. For example, Connected. - The `NetworkType` lists various network conditions. `.connected` & `.metered` will be mapped to [`requiresNetworkConnectivity`](https://developer.apple.com/documentation/backgroundtasks/bgprocessingtaskrequest/3142242-requiresnetworkconnectivity) on iOS. -- RequiresBatteryNotLow (Android only) - When set to true, your work will not run if the device is in low battery mode. - **Enabling the battery saving mode on the android device prevents the job from running** -- RequiresCharging - When set to true, your work will only run when the device is charging. -- RequiresDeviceIdle (Android only) - When set to true, this requires the user’s device to be idle before the work will run. This can be useful for running batched operations that might otherwise have a - negative performance impact on other apps running actively on the user’s device. -- RequiresStorageNotLow (Android only) - When set to true, your work will not run if the user’s storage space on the device is too low. - -```dart -Workmanager().registerOneOffTask( - "1", - "simpleTask", - constraints: Constraints( - networkType: NetworkType.connected, - requiresBatteryNotLow: true, - requiresCharging: true, - requiresDeviceIdle: true, - requiresStorageNotLow: true - ) -); -``` - -### InputData - -Add some input data for your task. Valid value types are: `int`, `bool`, `double`, `String` and their `list` - -```dart - Workmanager().registerOneOffTask( - "1", - "simpleTask", - inputData: { - 'int': 1, - 'bool': true, - 'double': 1.0, - 'string': 'string', - 'array': [1, 2, 3], - }, -); -``` - -## BackoffPolicy - -Indicates the waiting strategy upon task failure. -The default is `BackoffPolicy.exponential`. -You can also specify the delay. - -```dart -Workmanager().registerOneOffTask("1", "simpleTask", backoffPolicy: BackoffPolicy.exponential, backoffPolicyDelay: Duration(seconds: 10)); -``` - -## Cancellation - -A task can be cancelled in different ways : - -### By Tag - -Cancels the task that was previously registered using this **Tag**, if any. - -```dart -Workmanager().cancelByTag("tag"); -``` - -### By Unique Name - -```dart -Workmanager().cancelByUniqueName(""); -``` - -### All - -```dart -Workmanager().cancelAll(); -``` - - -# Building project - -Project was migrated to [Melos](https://pub.dev/packages/melos) so build steps has changed. - -1. Install melos - -``` -dart pub global activate melos -``` - -2. In project root bootstrap - -``` -melos bootstrap -``` - -3. Get packages - -``` -melos run get -``` - -## Code Generation - -This project uses [Pigeon](https://pub.dev/packages/pigeon) for type-safe platform channel communication. If you modify the platform interface: - -**⚠️ IMPORTANT**: Always use melos to regenerate Pigeon files: - -```bash -melos run generate:pigeon -``` - -**DO NOT** run pigeon directly - always use the melos script for consistency. - -## Running the example - -Now you should be able to run example project - -``` -cd example -flutter run -``` +See the [example folder](./example/) for a complete working demo with all task types and platform configurations. \ No newline at end of file diff --git a/docs.json b/docs.json index efff7df7..513b3184 100644 --- a/docs.json +++ b/docs.json @@ -1,5 +1,39 @@ { - "name": "Workmanager", - "description": "Workmanager" - } - \ No newline at end of file + "name": "Flutter Workmanager", + "description": "Background task execution for Flutter apps", + "theme": { + "primaryColor": "#1e40af", + "accentColor": "#2563eb", + "textColor": "#1f2937" + }, + "sidebar": [ + { + "group": "Getting Started", + "tab": "root", + "pages": [ + { + "title": "Overview", + "href": "/" + }, + { + "title": "Quick Start", + "href": "/quickstart" + } + ] + }, + { + "group": "Reference", + "tab": "root", + "pages": [ + { + "title": "Task Customization", + "href": "/customization" + }, + { + "title": "Debugging", + "href": "/debugging" + } + ] + } + ] +} \ No newline at end of file diff --git a/docs/customization.mdx b/docs/customization.mdx new file mode 100644 index 00000000..69db3cbc --- /dev/null +++ b/docs/customization.mdx @@ -0,0 +1,282 @@ +--- +title: Task Customization +description: Advanced task configuration with constraints, input data, and management +--- + +Configure background tasks with constraints, input data, and advanced management options. + +## Input Data + +Pass data to your background tasks and access it in the callback: + +```dart +// Schedule task with input data +Workmanager().registerOneOffTask( + "upload-task", + "file_upload", + inputData: { + 'fileName': 'document.pdf', + 'uploadUrl': 'https://api.example.com/upload', + 'retryCount': 3, + 'userId': 12345, + }, +); + +// Access input data in your task +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask((task, inputData) async { + print('Task: $task'); + print('Input: $inputData'); + + // Extract specific values + String? fileName = inputData?['fileName']; + String? uploadUrl = inputData?['uploadUrl']; + int retryCount = inputData?['retryCount'] ?? 0; + int userId = inputData?['userId'] ?? 0; + + // Use the data in your task logic + await uploadFile(fileName, uploadUrl, userId); + + return Future.value(true); + }); +} +``` + + +**Input Data Types:** You can pass basic JSON-serializable types: `String`, `int`, `double`, `bool`, `List`, `Map`. Complex objects need to be serialized first. + + +## Task Constraints + +Control when tasks should run based on device conditions: + +```dart +Workmanager().registerOneOffTask( + "sync-task", + "data_sync", + constraints: Constraints( + networkType: NetworkType.connected, // Require internet connection + requiresBatteryNotLow: true, // Don't run when battery is low + requiresCharging: false, // Can run when not charging + requiresDeviceIdle: false, // Can run when device is active + requiresStorageNotLow: true, // Don't run when storage is low + ), +); +``` + +### Network Constraints + +```dart +// Different network requirements +NetworkType.connected // Any internet connection +NetworkType.unmetered // WiFi or unlimited data only +NetworkType.not_required // Can run without internet +``` + +### Battery and Charging + +```dart +constraints: Constraints( + requiresBatteryNotLow: true, // Wait for adequate battery + requiresCharging: true, // Only run when plugged in +) +``` + + +**Platform Differences:** Some constraints are Android-only. iOS background tasks have different system-level constraints that cannot be configured directly. + + +## Task Management + +### Tagging Tasks + +Group related tasks with tags for easier management: + +```dart +// Tag multiple related tasks +Workmanager().registerOneOffTask( + "sync-photos", + "photo_sync", + tag: "sync-tasks", +); + +Workmanager().registerOneOffTask( + "sync-documents", + "document_sync", + tag: "sync-tasks", +); + +// Cancel all tasks with a specific tag +Workmanager().cancelByTag("sync-tasks"); +``` + +### Canceling Tasks + +```dart +// Cancel a specific task by unique name +Workmanager().cancelByUniqueName("sync-photos"); + +// Cancel tasks by tag +Workmanager().cancelByTag("sync-tasks"); + +// Cancel all scheduled tasks +Workmanager().cancelAll(); +``` + +### Task Scheduling Options + +```dart +// One-time task with delay +Workmanager().registerOneOffTask( + "delayed-task", + "cleanup", + initialDelay: Duration(minutes: 30), + inputData: {'cleanupType': 'cache'} +); + +// Periodic task with custom frequency +Workmanager().registerPeriodicTask( + "hourly-sync", + "data_sync", + frequency: Duration(hours: 1), // Android: minimum 15 minutes + initialDelay: Duration(minutes: 5), // Wait before first execution + inputData: {'syncType': 'incremental'} +); +``` + +## Advanced Configuration + +### Task Identification + +Use meaningful, unique task names to avoid conflicts: + +```dart +// Good: Specific and unique +Workmanager().registerOneOffTask( + "user-${userId}-photo-upload-${timestamp}", + "upload_task", + inputData: {'userId': userId, 'type': 'photo'} +); + +// Avoid: Generic names that might conflict +Workmanager().registerOneOffTask( + "task1", + "upload", + // ... +); +``` + +### Task Types and Names + +```dart +// Use descriptive task type names in your callback +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask((task, inputData) async { + switch (task) { + case 'photo_upload': + return await handlePhotoUpload(inputData); + case 'data_sync': + return await handleDataSync(inputData); + case 'cache_cleanup': + return await handleCacheCleanup(inputData); + case 'notification_check': + return await handleNotificationCheck(inputData); + default: + print('Unknown task: $task'); + return Future.value(false); + } + }); +} +``` + +## Error Handling and Retries + +```dart +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask((task, inputData) async { + int retryCount = inputData?['retryCount'] ?? 0; + + try { + // Your task logic + await performTask(inputData); + return Future.value(true); + + } catch (e) { + print('Task failed: $e'); + + // Decide whether to retry + if (retryCount < 3 && isRetryableError(e)) { + print('Retrying task (attempt ${retryCount + 1})'); + return Future.value(false); // Tell system to retry + } else { + print('Task failed permanently'); + return Future.value(true); // Don't retry + } + } + }); +} + +bool isRetryableError(dynamic error) { + // Network errors, temporary server issues, etc. + return error.toString().contains('network') || + error.toString().contains('timeout'); +} +``` + +## Best Practices + +### Efficient Task Design + +```dart +// Good: Quick, focused tasks +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask((task, inputData) async { + // Fast operation + await syncCriticalData(); + return Future.value(true); + }); +} + +// Avoid: Long-running operations +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask((task, inputData) async { + // This might timeout on iOS (30-second limit) + await processLargeDataset(); // ❌ Too slow + return Future.value(true); + }); +} +``` + +### Resource Management + +```dart +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask((task, inputData) async { + HttpClient? client; + + try { + // Initialize resources in the background isolate + client = HttpClient(); + + // Perform task + await performNetworkOperation(client); + + return Future.value(true); + + } finally { + // Clean up resources + client?.close(); + } + }); +} +``` + + +**Pro Tip:** Always initialize dependencies inside your background task callback since it runs in a separate isolate from your main app. + \ No newline at end of file diff --git a/docs/debugging.mdx b/docs/debugging.mdx new file mode 100644 index 00000000..0b84d772 --- /dev/null +++ b/docs/debugging.mdx @@ -0,0 +1,254 @@ +--- +title: Debugging Background Tasks +description: Debug and troubleshoot background tasks on Android and iOS +--- + +Background tasks can be tricky to debug since they run when your app is closed. Here's how to effectively debug and troubleshoot them on both platforms. + +## Enable Debug Mode + +Always start by enabling debug notifications: + +```dart +Workmanager().initialize( + callbackDispatcher, + isInDebugMode: true, // Shows notifications when tasks execute +); +``` + +This shows system notifications whenever background tasks run, making it easy to verify execution. + +## Android Debugging + +### Job Scheduler Inspection + +Use ADB to inspect Android's job scheduler: + +```bash +# View scheduled jobs +adb shell dumpsys jobscheduler | grep yourapp + +# View detailed job info +adb shell dumpsys jobscheduler yourapp + +# Force run job (debug only) +adb shell cmd jobscheduler run -f yourapp JOB_ID +``` + +### Monitor Job Execution + +```bash +# Monitor WorkManager logs +adb logcat | grep WorkManager + +# Monitor app background execution +adb logcat | grep "yourapp" +``` + +### Common Android Issues + +**Tasks not running:** +- Check battery optimization settings +- Verify app is not in "App Standby" mode +- Ensure device isn't in Doze mode +- Check if constraints are too restrictive + +**Tasks running too often:** +- Android enforces minimum 15-minute intervals for periodic tasks +- Use appropriate constraints to limit execution + +### Debug Commands + +```bash +# Check if your app is whitelisted from battery optimization +adb shell dumpsys deviceidle whitelist + +# Check battery optimization status +adb shell settings get global battery_saver_constants + +# Force device into idle mode (testing) +adb shell dumpsys deviceidle force-idle +``` + +## iOS Debugging + +### Console Logging + +iOS background tasks have limited execution time. Add detailed logging: + +```dart +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask((task, inputData) async { + print('[iOS BG] Task started: $task at ${DateTime.now()}'); + + try { + // Your task logic + final result = await performTask(); + print('[iOS BG] Task completed successfully'); + return true; + } catch (e) { + print('[iOS BG] Task failed: $e'); + return false; + } + }); +} +``` + +### Xcode Debugging + +**For Background Fetch tasks:** +Use **Debug → Perform Fetch** from the Xcode run menu while your app is running. + +**For BGTaskScheduler tasks (processing/periodic):** +Use Xcode's console to trigger tasks manually: + +```objc +// Trigger specific BGTaskScheduler task +e -l objc -- (void)[[BGTaskScheduler sharedScheduler] + _simulateLaunchForTaskWithIdentifier:@"com.yourapp.task.identifier"] +``` + +### Monitor Scheduled Tasks + +Check what tasks iOS has scheduled: + +```dart +// iOS 13+ only +if (Platform.isIOS) { + final tasks = await Workmanager().printScheduledTasks(); + print('Scheduled tasks: $tasks'); +} +``` + +This prints output like: +``` +[BGTaskScheduler] Task Identifier: your.task.id earliestBeginDate: 2023.10.10 PM 11:10:12 +[BGTaskScheduler] There are no scheduled tasks +``` + +### Common iOS Issues + +**Background App Refresh disabled:** +- Check iOS Settings → General → Background App Refresh +- See [Apple's Background App Refresh Guide](https://support.apple.com/en-us/102425) + +**Tasks never run:** +- App hasn't been used recently (iOS learning algorithm) - [iOS Power Management](https://developer.apple.com/library/archive/documentation/Performance/Conceptual/power_efficiency_guidelines_osx/PrioritizeWorkAtTheTaskLevel.html) +- Task identifiers don't match between Info.plist and AppDelegate - [check iOS setup](quickstart#option-b-processing-tasks-for-complex-operations) +- Missing BGTaskSchedulerPermittedIdentifiers in Info.plist - [review iOS configuration](quickstart#ios) + +**Tasks stop working:** +- iOS battery optimization kicked in - [WWDC 2020: Background execution demystified](https://developer.apple.com/videos/play/wwdc2020/10063/) +- App removed from recent apps too often +- User disabled background refresh - [check Background Fetch setup](quickstart#option-a-periodic-tasks-recommended-for-most-use-cases) +- Task taking longer than 30 seconds - [BGTaskScheduler Documentation](https://developer.apple.com/documentation/backgroundtasks/bgtaskscheduler) + +**Tasks run but don't complete:** +- Hitting 30-second execution limit - [Background Tasks Best Practices](https://developer.apple.com/documentation/backgroundtasks/bgtask) +- Network requests timing out +- Heavy processing blocking the thread - [WWDC 2019: Advances in App Background Execution](https://developer.apple.com/videos/play/wwdc2019/707/) + +## General Debugging Tips + +### Add Comprehensive Logging + +```dart +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask((task, inputData) async { + final startTime = DateTime.now(); + print('🚀 Task started: $task'); + print('📊 Input data: $inputData'); + + try { + // Your task implementation + final result = await performTask(task, inputData); + + final duration = DateTime.now().difference(startTime); + print('✅ Task completed in ${duration.inSeconds}s'); + + return result; + } catch (e, stackTrace) { + final duration = DateTime.now().difference(startTime); + print('❌ Task failed after ${duration.inSeconds}s: $e'); + print('📋 Stack trace: $stackTrace'); + + return false; // Retry + } + }); +} +``` + +### Test Task Logic Separately + +Create a way to test your background logic from the UI: + +```dart +// Add a debug button to test task logic +ElevatedButton( + onPressed: () async { + // Test the same logic that runs in background + final result = await performTask('test_task', {'debug': true}); + print('Test result: $result'); + }, + child: Text('Test Task Logic'), +) +``` + +### Monitor Task Health + +Track when tasks last ran successfully: + +```dart +Future saveTaskExecutionTime(String taskName) async { + final prefs = await SharedPreferences.getInstance(); + await prefs.setString('${taskName}_last_run', DateTime.now().toIso8601String()); +} + +Future isTaskHealthy(String taskName, Duration maxAge) async { + final prefs = await SharedPreferences.getInstance(); + final lastRunString = prefs.getString('${taskName}_last_run'); + + if (lastRunString == null) return false; + + final lastRun = DateTime.parse(lastRunString); + final age = DateTime.now().difference(lastRun); + + return age < maxAge; +} +``` + +## Platform-Specific Testing + +### Android Testing Workflow + +1. **Enable debug mode** and install app +2. **Schedule task** and verify it appears in job scheduler +3. **Put device to sleep** and wait for execution +4. **Check debug notifications** to confirm execution +5. **Use ADB commands** to force execution if needed + +### iOS Testing Workflow + +1. **Test on physical device** (simulator doesn't support background tasks) +2. **Enable Background App Refresh** in iOS Settings +3. **Use Xcode debugger** to trigger tasks immediately +4. **Monitor Xcode console** for logging output +5. **Check iOS Settings > Battery** for background activity + +## Troubleshooting Checklist + +**Quick Checks:** +- [ ] Workmanager initialized in main() +- [ ] Task names are unique +- [ ] Platform setup completed ([iOS setup guide](quickstart#ios)) +- [ ] Debug notifications enabled (`isInDebugMode: kDebugMode`) + +**Performance & Reliability:** +- [ ] Task logic optimized for background execution +- [ ] Dependencies initialized in background isolate +- [ ] Error handling with try-catch blocks +- [ ] iOS 30-second execution limit respected + +Remember: Background task execution is controlled by the operating system and is never guaranteed. Always design your app to work gracefully when background tasks don't run as expected. \ No newline at end of file diff --git a/docs/index.mdx b/docs/index.mdx index 9de66d9f..8545bd68 100644 --- a/docs/index.mdx +++ b/docs/index.mdx @@ -1 +1,55 @@ -# Flutter Workmanager +--- +title: Flutter Workmanager +description: Background task execution for Flutter apps +--- + +Execute Dart code in the background, even when your app is closed. Perfect for data sync, file uploads, and periodic maintenance tasks. + +## Key Features + +- **Background Task Execution**: Execute Dart code in the background on both Android and iOS platforms +- **Multiple Task Types**: Support for one-off tasks, periodic tasks, and iOS processing tasks +- **Platform Constraints**: Configure network, battery, charging, and other constraints for task execution +- **Debug Support**: Built-in debugging with notifications to track background task execution + +## Supported Platforms + +| Platform | Support | Notes | +|----------|---------|--------| +| Android | ✅ Full | All WorkManager features supported | +| iOS | ✅ Full | Background Fetch + BGTaskScheduler APIs | +| macOS | 🚧 Planned | Future support through NSBackgroundActivityScheduler | +| Web | ❌ Not supported | Background execution not available | +| Windows/Linux | ❌ Not supported | No background task APIs | + +## Common Use Cases + +| Use Case | Description | +|----------|-------------| +| **Sync data from API** | Automatically fetch and sync data from your backend API | +| **Upload files in background** | Upload photos, documents when network conditions are optimal | +| **Clean up old data** | Remove old files, cache data, and maintain app performance | +| **Fetch notifications** | Check for new notifications and messages from your server | +| **Database maintenance** | Perform database cleanup and optimization tasks | + +## Architecture + +This plugin uses a **federated architecture**: +- `workmanager` - Main package that provides the unified API +- `workmanager_android` - Android implementation using WorkManager +- `workmanager_apple` - iOS/macOS implementation using BGTaskScheduler +- `workmanager_platform_interface` - Shared interface for platform implementations + +All packages are automatically included when you add `workmanager` to pubspec.yaml. + +## Get Started + +Ready to add background tasks to your Flutter app? + +**[→ Quick Start Guide](quickstart)** - Get up and running in minutes + +**[→ API Documentation](https://pub.dev/documentation/workmanager/latest/)** - Complete Dart API reference + +## Example Project + +See a complete working demo: **[Example App →](https://github.com/fluttercommunity/flutter_workmanager/tree/main/example)** diff --git a/docs/quickstart.mdx b/docs/quickstart.mdx new file mode 100644 index 00000000..1489d0cb --- /dev/null +++ b/docs/quickstart.mdx @@ -0,0 +1,197 @@ +--- +title: Quick Start +description: Get started with Flutter Workmanager in minutes +--- + +## Installation + +Add `workmanager` to your `pubspec.yaml`: + +```yaml +dependencies: + workmanager: ^0.8.0 +``` + +Then run: +```bash +flutter pub get +``` + +## Platform Setup + +### Android +Android works automatically - no additional setup required! ✅ + +### iOS +iOS requires a 5-minute setup in Xcode. Choose your approach based on your needs: + +#### Option A: Periodic Tasks (Recommended for most use cases) +For regular data sync, notifications, cleanup - uses iOS Background Fetch: + +1. **Enable Background Modes** in Xcode target capabilities ([Xcode Help](https://help.apple.com/xcode/mac/current/#/devbfa1532c4)) and add to Info.plist: +```xml +UIBackgroundModes + + fetch + +``` + +2. **No AppDelegate configuration needed** - works automatically from Dart code + + +**iOS Background Fetch scheduling:** iOS completely controls when Background Fetch runs (typically once per day based on user app usage patterns). You cannot force immediate execution - it's designed for non-critical periodic updates like refreshing content. + + +#### Option B: Processing Tasks (For complex operations) +For file uploads, data processing, longer tasks - uses BGTaskScheduler: + +1. **Enable Background Modes** in Xcode target capabilities ([Xcode Help](https://help.apple.com/xcode/mac/current/#/devbfa1532c4)) and add to Info.plist: +```xml +UIBackgroundModes + + processing + + +BGTaskSchedulerPermittedIdentifiers + + com.yourapp.processing_task + +``` + +2. **Configure AppDelegate.swift** (required for BGTaskScheduler): +```swift +import workmanager_apple + +// In application didFinishLaunching +WorkmanagerPlugin.registerBGProcessingTask( + withIdentifier: "com.yourapp.processing_task" +) +``` + + +**Why BGTaskScheduler registration is needed:** iOS requires explicit registration of background task identifiers for security and system resource management. The task identifier in Info.plist tells iOS which background tasks your app can schedule, while the AppDelegate registration connects the identifier to the actual task handler. Background Fetch (Option A) doesn't require this since it uses the simpler, system-managed approach. + + +#### Option C: Periodic Tasks with Custom Frequency +For periodic tasks with more control than Background Fetch - uses BGTaskScheduler with frequency: + +1. **Enable Background Modes** in Xcode target capabilities ([Xcode Help](https://help.apple.com/xcode/mac/current/#/devbfa1532c4)) and add to Info.plist: +```xml +UIBackgroundModes + + processing + + +BGTaskSchedulerPermittedIdentifiers + + com.yourapp.periodic_task + +``` + +2. **Configure AppDelegate.swift** (with frequency control): +```swift +import workmanager_apple + +// In application didFinishLaunching +WorkmanagerPlugin.registerPeriodicTask( + withIdentifier: "com.yourapp.periodic_task", + frequency: NSNumber(value: 20 * 60) // 20 minutes (15 min minimum) +) +``` + + +**Which option to choose?** +- **Option A (Background Fetch)** for non-critical updates that can happen once daily (data sync, content refresh) +- **Option B (BGTaskScheduler)** for one-time tasks, file uploads, or immediate task scheduling +- **Option C (Periodic Tasks)** for regular tasks with custom frequency control (15+ minutes) + + +## Basic Usage + +### 1. Create Background Task Handler + +```dart +@pragma('vm:entry-point') +void callbackDispatcher() { + Workmanager().executeTask((task, inputData) async { + switch (task) { + case "data_sync": + await syncDataWithServer(); + break; + case "cleanup": + await cleanupOldFiles(); + break; + case Workmanager.iOSBackgroundTask: + // iOS Background Fetch task + await handleBackgroundFetch(); + break; + default: + // Handle unknown task types + break; + } + + return Future.value(true); + }); +} +``` + + +**Important:** The `callbackDispatcher` must be a top-level function (not inside a class) since it runs in a separate isolate. + + +### 2. Initialize in main() + +```dart +import 'package:flutter/foundation.dart'; + +void main() { + Workmanager().initialize( + callbackDispatcher, + isInDebugMode: kDebugMode, + ); + + runApp(MyApp()); +} +``` + +### 3. Schedule Tasks + +```dart +// Schedule a one-time task +Workmanager().registerOneOffTask( + "sync-task", + "data_sync", + initialDelay: Duration(seconds: 10), +); + +// Schedule a periodic task +Workmanager().registerPeriodicTask( + "cleanup-task", + "cleanup", + frequency: Duration(hours: 24), +); +``` + +## Task Results + +Your background tasks can return: + +- `Future.value(true)` - ✅ Task successful +- `Future.value(false)` - 🔄 Task should be retried +- `Future.error(...)` - ❌ Task failed + + +## Key Points + +- **Callback Dispatcher**: Must be a top-level function (not inside a class) +- **Separate Isolate**: Background tasks run in isolation - initialize dependencies inside the task +- **Platform Differences**: + - Android: Reliable background execution, 15-minute minimum frequency + - iOS: 30-second limit, execution depends on user patterns and device state + + +## Next Steps + +- **[Task Customization](customization)** - Advanced configuration with constraints, input data, and management +- **[Debugging Guide](debugging)** - Learn how to debug and troubleshoot background tasks +- **[Example App](https://github.com/fluttercommunity/flutter_workmanager/tree/main/example)** - Complete working demo \ No newline at end of file diff --git a/example/README.md b/example/README.md index 6a9fca8e..d667478e 100644 --- a/example/README.md +++ b/example/README.md @@ -1,16 +1,63 @@ -# workmanager_example +# Flutter Workmanager Example -Demonstrates how to use the workmanager plugin. +Complete working demo showing all Flutter Workmanager features and task types. -## Getting Started +## Features Demonstrated -This project is a starting point for a Flutter application. +- **One-off tasks**: Immediate background execution +- **Periodic tasks**: Scheduled recurring background work +- **Processing tasks**: Long-running iOS background tasks +- **Task constraints**: Network, battery, and device state requirements +- **Debug notifications**: Visual feedback when tasks execute +- **Error handling**: Proper task success/failure/retry logic +- **Platform differences**: Android vs iOS background execution -A few resources to get you started if this is your first Flutter project: +## Quick Start -- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab) -- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook) +1. **Clone and run**: + ```bash + git clone https://github.com/fluttercommunity/flutter_workmanager.git + cd flutter_workmanager/example + flutter run + ``` -For help getting started with Flutter, view our -[online documentation](https://flutter.dev/docs), which offers tutorials, -samples, guidance on mobile development, and a full API reference. +2. **Platform setup**: + - **Android**: Works immediately ✅ + - **iOS**: Follow the iOS setup in `ios/Runner/AppDelegate.swift` and `ios/Runner/Info.plist` + +3. **Test background tasks**: + - Tap buttons to schedule different task types + - Put app in background to see tasks execute + - Check debug notifications to verify execution + +## Example Tasks + +The demo includes practical examples: + +- **Simulated API sync**: Fetches data and stores locally +- **File cleanup**: Removes old cached files +- **Periodic maintenance**: Regular app maintenance tasks +- **Long processing**: iOS-specific long-running tasks + +## Key Files + +- `lib/main.dart` - Main app with task scheduling UI +- `lib/callback_dispatcher.dart` - Background task execution logic +- `ios/Runner/AppDelegate.swift` - iOS background task registration +- `ios/Runner/Info.plist` - iOS background modes configuration + +## Testing Background Tasks + +**Android**: +- Tasks run reliably in background +- Enable debug mode to see notifications +- Use `adb shell dumpsys jobscheduler` to inspect scheduled tasks + +**iOS**: +- Test on physical device (not simulator) +- Enable Background App Refresh in Settings +- Use Xcode debugger commands to trigger tasks immediately + +## Documentation + +For detailed guides and real-world use cases, visit: **[docs.page/fluttercommunity/flutter_workmanager →](https://docs.page/fluttercommunity/flutter_workmanager)**