Skip to content

Commit ecaab1e

Browse files
enedclaude
andcommitted
feat: Complete federated plugin architecture with comprehensive tests
- Fix platform implementation registration in Workmanager constructor - Add comprehensive unit tests for Android and iOS platform packages - Fix iOS native tests import from workmanager to workmanager_ios - Add publishing instructions to README for maintainer guidance - Add melos test script for running tests across all packages - Resolve method channel serialization type casting issues 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <[email protected]>
1 parent 3ef530c commit ecaab1e

File tree

6 files changed

+439
-2
lines changed

6 files changed

+439
-2
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,33 @@ In order for background work to be scheduled correctly you should follow the And
1919
- [Android Setup](https://github.com/fluttercommunity/flutter_workmanager/blob/master/ANDROID_SETUP.md)
2020
- [iOS Setup](https://github.com/fluttercommunity/flutter_workmanager/blob/master/IOS_SETUP.md)
2121

22+
## Publishing (For Maintainers)
23+
24+
This project uses a federated plugin architecture with multiple packages. To publish updates:
25+
26+
1. **Update versions** in all `pubspec.yaml` files:
27+
- `workmanager/pubspec.yaml`
28+
- `workmanager_platform_interface/pubspec.yaml`
29+
- `workmanager_android/pubspec.yaml`
30+
- `workmanager_ios/pubspec.yaml`
31+
32+
2. **Publish packages in order**:
33+
```bash
34+
# 1. Publish platform interface first
35+
cd workmanager_platform_interface && dart pub publish
36+
37+
# 2. Publish platform implementations
38+
cd ../workmanager_android && dart pub publish
39+
cd ../workmanager_ios && dart pub publish
40+
41+
# 3. Publish main package last
42+
cd ../workmanager && dart pub publish
43+
```
44+
45+
3. **Update dependencies** in main package to point to pub.dev versions instead of path dependencies before publishing
46+
47+
4. **Tag the release** with the version number: `git tag v0.8.0 && git push origin v0.8.0`
48+
2249
# How to use the package?
2350

2451
See sample folder for a complete working example.

example/ios/RunnerTests/WorkmanagerTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88

99
import XCTest
1010

11-
@testable import workmanager
11+
@testable import workmanager_ios
1212

1313
class WorkmanagerTests: XCTestCase {
1414

melos.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,10 @@ packages:
88
scripts:
99
get: melos exec -- dart pub get
1010

11+
test:
12+
run: melos exec --depends-on="flutter_test" -- "flutter test"
13+
description: Run tests for all packages with flutter_test dependency.
14+
1115
generate:dart:
1216
run: melos exec -c 1 --depends-on="build_runner" --no-flutter -- "dart run build_runner build --delete-conflicting-outputs"
1317
description: Build all generated files for Dart packages in this project.

workmanager/lib/src/workmanager_impl.dart

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
import 'dart:async';
2+
import 'dart:io';
23

34
import 'package:flutter/services.dart';
45
import 'package:workmanager_platform_interface/workmanager_platform_interface.dart';
6+
import 'package:workmanager_android/workmanager_android.dart';
7+
import 'package:workmanager_ios/workmanager_ios.dart';
58

69
/// Function that executes your background work.
710
/// You should return whether the task ran successfully or not.
@@ -71,10 +74,23 @@ typedef BackgroundTaskHandler = Future<bool> Function(
7174
class Workmanager {
7275
factory Workmanager() => _instance;
7376

74-
Workmanager._internal();
77+
Workmanager._internal() {
78+
_ensurePlatformImplementation();
79+
}
7580

7681
static final Workmanager _instance = Workmanager._internal();
7782

83+
static void _ensurePlatformImplementation() {
84+
if (WorkmanagerPlatform.instance is! WorkmanagerAndroid &&
85+
WorkmanagerPlatform.instance is! WorkmanagerIOS) {
86+
if (Platform.isAndroid) {
87+
WorkmanagerPlatform.instance = WorkmanagerAndroid();
88+
} else if (Platform.isIOS) {
89+
WorkmanagerPlatform.instance = WorkmanagerIOS();
90+
}
91+
}
92+
}
93+
7894
/// Use this constant inside your callbackDispatcher to identify when an iOS Background Fetch occurred.
7995
///
8096
/// ```
Lines changed: 210 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,210 @@
1+
import 'package:flutter/services.dart';
2+
import 'package:flutter_test/flutter_test.dart';
3+
import 'package:workmanager_android/workmanager_android.dart';
4+
import 'package:workmanager_platform_interface/workmanager_platform_interface.dart';
5+
6+
void main() {
7+
TestWidgetsFlutterBinding.ensureInitialized();
8+
9+
group('WorkmanagerAndroid', () {
10+
late WorkmanagerAndroid workmanager;
11+
late List<MethodCall> methodCalls;
12+
13+
setUp(() {
14+
workmanager = WorkmanagerAndroid();
15+
methodCalls = <MethodCall>[];
16+
17+
// Mock the method channel
18+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
19+
.setMockMethodCallHandler(
20+
const MethodChannel('dev.fluttercommunity.workmanager/foreground_channel_work_manager'),
21+
(MethodCall methodCall) async {
22+
methodCalls.add(methodCall);
23+
return null;
24+
},
25+
);
26+
});
27+
28+
tearDown(() {
29+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
30+
.setMockMethodCallHandler(
31+
const MethodChannel('dev.fluttercommunity.workmanager/foreground_channel_work_manager'),
32+
null,
33+
);
34+
});
35+
36+
group('registerOneOffTask', () {
37+
test('should serialize all parameters correctly', () async {
38+
await workmanager.registerOneOffTask(
39+
'testTask',
40+
'testTaskName',
41+
inputData: {'key': 'value'},
42+
initialDelay: const Duration(seconds: 30),
43+
constraints: Constraints(
44+
networkType: NetworkType.connected,
45+
requiresCharging: true,
46+
requiresBatteryNotLow: false,
47+
requiresDeviceIdle: true,
48+
requiresStorageNotLow: false,
49+
),
50+
existingWorkPolicy: ExistingWorkPolicy.replace,
51+
backoffPolicy: BackoffPolicy.exponential,
52+
backoffPolicyDelay: const Duration(minutes: 1),
53+
tag: 'testTag',
54+
outOfQuotaPolicy: OutOfQuotaPolicy.run_as_non_expedited_work_request,
55+
);
56+
57+
expect(methodCalls, hasLength(1));
58+
expect(methodCalls.first.method, 'registerOneOffTask');
59+
60+
final arguments = Map<String, dynamic>.from(methodCalls.first.arguments);
61+
expect(arguments['uniqueName'], 'testTask');
62+
expect(arguments['taskName'], 'testTaskName');
63+
expect(arguments['inputData'], {'key': 'value'});
64+
expect(arguments['initialDelaySeconds'], 30);
65+
expect(arguments['networkType'], 'connected');
66+
expect(arguments['requiresCharging'], true);
67+
expect(arguments['requiresBatteryNotLow'], false);
68+
expect(arguments['requiresDeviceIdle'], true);
69+
expect(arguments['requiresStorageNotLow'], false);
70+
expect(arguments['existingWorkPolicy'], 'replace');
71+
expect(arguments['backoffPolicy'], 'exponential');
72+
expect(arguments['backoffDelayInMilliseconds'], 60000);
73+
expect(arguments['tag'], 'testTag');
74+
expect(arguments['outOfQuotaPolicy'], 'run_as_non_expedited_work_request');
75+
});
76+
77+
test('should handle null optional parameters', () async {
78+
await workmanager.registerOneOffTask('testTask', 'testTaskName');
79+
80+
expect(methodCalls, hasLength(1));
81+
final arguments = Map<String, dynamic>.from(methodCalls.first.arguments);
82+
expect(arguments['inputData'], null);
83+
expect(arguments['initialDelaySeconds'], null);
84+
expect(arguments['networkType'], null);
85+
expect(arguments['requiresCharging'], null);
86+
expect(arguments['requiresBatteryNotLow'], null);
87+
expect(arguments['requiresDeviceIdle'], null);
88+
expect(arguments['requiresStorageNotLow'], null);
89+
expect(arguments['existingWorkPolicy'], null);
90+
expect(arguments['backoffPolicy'], null);
91+
expect(arguments['backoffDelayInMilliseconds'], null);
92+
expect(arguments['tag'], null);
93+
expect(arguments['outOfQuotaPolicy'], null);
94+
});
95+
});
96+
97+
group('registerPeriodicTask', () {
98+
test('should serialize all parameters correctly', () async {
99+
await workmanager.registerPeriodicTask(
100+
'periodicTask',
101+
'periodicTaskName',
102+
frequency: const Duration(hours: 1),
103+
flexInterval: const Duration(minutes: 15),
104+
inputData: {'periodic': 'data'},
105+
initialDelay: const Duration(minutes: 5),
106+
constraints: Constraints(networkType: NetworkType.unmetered),
107+
existingWorkPolicy: ExistingWorkPolicy.keep,
108+
backoffPolicy: BackoffPolicy.linear,
109+
backoffPolicyDelay: const Duration(seconds: 30),
110+
tag: 'periodicTag',
111+
);
112+
113+
expect(methodCalls, hasLength(1));
114+
expect(methodCalls.first.method, 'registerPeriodicTask');
115+
116+
final arguments = Map<String, dynamic>.from(methodCalls.first.arguments);
117+
expect(arguments['uniqueName'], 'periodicTask');
118+
expect(arguments['taskName'], 'periodicTaskName');
119+
expect(arguments['frequencySeconds'], 3600);
120+
expect(arguments['flexIntervalSeconds'], 900);
121+
expect(arguments['inputData'], {'periodic': 'data'});
122+
expect(arguments['initialDelaySeconds'], 300);
123+
expect(arguments['networkType'], 'unmetered');
124+
expect(arguments['existingWorkPolicy'], 'keep');
125+
expect(arguments['backoffPolicy'], 'linear');
126+
expect(arguments['backoffDelayInMilliseconds'], 30000);
127+
expect(arguments['tag'], 'periodicTag');
128+
});
129+
});
130+
131+
group('registerProcessingTask', () {
132+
test('should throw UnsupportedError on Android', () async {
133+
expect(
134+
() => workmanager.registerProcessingTask('processingTask', 'processingTaskName'),
135+
throwsA(isA<UnsupportedError>()),
136+
);
137+
});
138+
});
139+
140+
group('cancelByUniqueName', () {
141+
test('should call correct method with parameters', () async {
142+
await workmanager.cancelByUniqueName('testTask');
143+
144+
expect(methodCalls, hasLength(1));
145+
expect(methodCalls.first.method, 'cancelTaskByUniqueName');
146+
expect(methodCalls.first.arguments, {'uniqueName': 'testTask'});
147+
});
148+
});
149+
150+
group('cancelByTag', () {
151+
test('should call correct method with parameters', () async {
152+
await workmanager.cancelByTag('testTag');
153+
154+
expect(methodCalls, hasLength(1));
155+
expect(methodCalls.first.method, 'cancelTaskByTag');
156+
expect(methodCalls.first.arguments, {'tag': 'testTag'});
157+
});
158+
});
159+
160+
group('cancelAll', () {
161+
test('should call correct method', () async {
162+
await workmanager.cancelAll();
163+
164+
expect(methodCalls, hasLength(1));
165+
expect(methodCalls.first.method, 'cancelAllTasks');
166+
expect(methodCalls.first.arguments, null);
167+
});
168+
});
169+
170+
group('isScheduledByUniqueName', () {
171+
test('should return result from method channel', () async {
172+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
173+
.setMockMethodCallHandler(
174+
const MethodChannel('dev.fluttercommunity.workmanager/foreground_channel_work_manager'),
175+
(MethodCall methodCall) async {
176+
if (methodCall.method == 'isScheduledByUniqueName') {
177+
return true;
178+
}
179+
return null;
180+
},
181+
);
182+
183+
final result = await workmanager.isScheduledByUniqueName('testTask');
184+
185+
expect(result, true);
186+
});
187+
188+
test('should return false when method channel returns null', () async {
189+
TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger
190+
.setMockMethodCallHandler(
191+
const MethodChannel('dev.fluttercommunity.workmanager/foreground_channel_work_manager'),
192+
(MethodCall methodCall) async => null,
193+
);
194+
195+
final result = await workmanager.isScheduledByUniqueName('testTask');
196+
197+
expect(result, false);
198+
});
199+
});
200+
201+
group('printScheduledTasks', () {
202+
test('should throw UnsupportedError on Android', () async {
203+
expect(
204+
() => workmanager.printScheduledTasks(),
205+
throwsA(isA<UnsupportedError>()),
206+
);
207+
});
208+
});
209+
});
210+
}

0 commit comments

Comments
 (0)