Skip to content
Merged
105 changes: 18 additions & 87 deletions CLAUDE.md
Original file line number Diff line number Diff line change
@@ -1,87 +1,18 @@
## Project Workflow
- Project uses GitHub Actions
- Use `ktlint -F .` in root folder to format Kotlin code
- Use SwiftLint for code formatting
- Always resolve formatting and analyzer errors before completing a task
- **CRITICAL**: Always run `ktlint -F .` after modifying any Kotlin files before committing

## Pigeon Code Generation
- Pigeon configuration is in `workmanager_platform_interface/pigeons/workmanager_api.dart`
- **MUST use melos to regenerate Pigeon files**: `melos run generate:pigeon`
- ⚠️ **DO NOT** run pigeon directly - always use the melos script for consistency
- Generated files:
- Dart: `workmanager_platform_interface/lib/src/pigeon/workmanager_api.g.dart`
- Kotlin: `workmanager_android/android/src/main/kotlin/dev/fluttercommunity/workmanager/pigeon/WorkmanagerApi.g.kt`
- Swift: `workmanager_apple/ios/Classes/pigeon/WorkmanagerApi.g.swift`
- Do not manually edit generated files (*.g.* files)
- Generated files may have different formatting than dart format - this is expected and handled by exclusion patterns

## Code Formatting Configuration
- `.editorconfig` in root folder configures ktlint to ignore Pigeon-generated Kotlin files
- `.swiftlint.yml` in root folder excludes Pigeon-generated Swift files from linting

## GitHub Actions Configuration
- Format checks: `.github/workflows/format.yml`
- Runs dart format, ktlint, and SwiftLint
- Tests: `.github/workflows/test.yml`
- `test`: Runs Dart unit tests
- `native_ios_tests`: Runs iOS native tests with xcodebuild
- `native_android_tests`: Runs Android native tests with Gradle
- `drive_ios`: Runs Flutter integration tests on iOS simulator
- `drive_android`: Runs Flutter integration tests on Android emulator

## Testing Strategy & Preferences
- **Focus on business logic**: Test unique platform implementation logic, not Pigeon plumbing
- **Trust third-party components**: Consider Pigeon a trusted component - don't test its internals
- **Platform-specific behavior**: Test what makes each platform unique (Android WorkManager vs iOS BGTaskScheduler)
- **Avoid channel mocking**: Don't mock platform channels unless absolutely necessary
- **Test unsupported operations**: Verify platform-specific UnsupportedError throwing
- **Integration over unit**: Prefer integration tests for complete platform behavior validation

## Test Execution
- Run all tests: `flutter test` (from root or individual package)
- Android tests: `cd workmanager_android && flutter test`
- Apple tests: `cd workmanager_apple && flutter test`
- Native Android tests: `cd example/android && ./gradlew :workmanager_android:test`
- Native iOS tests: `cd example/ios && xcodebuild test -workspace Runner.xcworkspace -scheme Runner -destination 'platform=iOS Simulator,name=iPhone 16,OS=latest'`
- Always build example app before completing: `cd example && flutter build apk --debug && flutter build ios --debug --no-codesign`

## Pigeon Migration Status
- ✅ Migration to Pigeon v22.7.4 completed successfully
- ✅ All platforms (Android, iOS) migrated from MethodChannel to Pigeon
- ✅ Unit tests refactored to focus on platform-specific business logic
- ✅ Code formatting and linting properly configured for generated files
- ✅ All tests passing: Dart unit tests, native Android tests, native iOS tests
- ✅ Example app builds successfully for both Android APK and iOS app

## Documentation Preferences
- Keep summaries concise - don't repeat completed tasks in status updates
- Focus on current progress and next steps
- Document decisions and architectural choices

## CHANGELOG Management
- Document improvements in CHANGELOG.md files immediately when implemented
- Use "Future" as the version header for unreleased changes (standard open source practice)
- Keep entries brief and focused on user-facing impact
- Relevant files: workmanager/CHANGELOG.md, workmanager_android/CHANGELOG.md, workmanager_apple/CHANGELOG.md

## GitHub Actions - Package Analysis
- The `analysis.yml` workflow runs package analysis for all packages
- It performs `flutter analyze` and `dart pub publish --dry-run` for each package
- The dry-run validates that packages are ready for publishing
- Common issues that cause failures:
- Uncommitted changes in git (packages should be published from clean state)
- Files ignored by .gitignore but checked into git (use .pubignore if needed)
- Modified files that haven't been committed
- Always ensure all changes are committed before pushing to avoid CI failures

## GitHub Actions - Formatting Issues
- The `format.yml` workflow runs formatting checks
- ❌ **Important Discovery**: `analysis_options.yml formatter.exclude` does NOT prevent `dart format` from formatting files
- ✅ **FIXED**: Updated CI workflow to use `find` command to exclude .g.dart files:
```bash
find . -name "*.dart" ! -name "*.g.dart" ! -path "*/.*" -print0 | xargs -0 dart format --set-exit-if-changed
```
- **Root Issue**: `dart format` ignores analysis_options.yml exclusions and will always format ALL Dart files
- **Solution**: Filter files before passing to dart format to exclude generated files
- The `analysis_options.yml` exclusions only affect static analysis, not formatting
## Pre-Commit Requirements
**CRITICAL**: Always run from project root before ANY commit:
1. `ktlint -F .`
2. `find . -name "*.dart" ! -name "*.g.dart" ! -path "*/.*" -print0 | xargs -0 dart format --set-exit-if-changed`
3. `flutter test` (all Dart tests)
4. `cd example/android && ./gradlew :workmanager_android:test` (Android native tests)

## Code Generation
- Regenerate Pigeon files: `melos run generate:pigeon`
- Do not manually edit *.g.* files

## Test Quality Requirements
- **NEVER create useless tests**: No `assert(true)`, `expect(true, true)`, or compilation-only tests
- **Test real logic**: Exercise actual methods with real inputs and verify meaningful outputs
- **Test edge cases**: null inputs, error conditions, boundary values

## Complex Component Testing
- **BackgroundWorker**: Cannot be unit tested due to Flutter engine dependencies - use integration tests
1 change: 1 addition & 0 deletions workmanager/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Future

## Bug Fixes & Improvements
* Fix null cast to map bug in executeTask when inputData contains null keys or values (thanks to @Dr-wgy)
* Internal improvements to development and testing infrastructure

# 0.8.0
Expand Down
26 changes: 17 additions & 9 deletions workmanager/lib/src/workmanager_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -295,22 +295,30 @@ class Workmanager {
Future<String> printScheduledTasks() async => _platform.printScheduledTasks();
}

/// Converts inputData from Pigeon format, filtering out null keys
@visibleForTesting
Map<String, dynamic>? convertPigeonInputData(Map<String?, Object?>? inputData) {
Map<String, dynamic>? convertedInputData;
if (inputData != null) {
convertedInputData = <String, dynamic>{};
for (final entry in inputData.entries) {
if (entry.key != null) {
convertedInputData[entry.key!] = entry.value;
}
}
}
return convertedInputData;
}

/// Implementation of WorkmanagerFlutterApi for handling background task execution
class _WorkmanagerFlutterApiImpl extends WorkmanagerFlutterApi {
@override
Future<void> backgroundChannelInitialized() async {
// This is called by the native side to indicate it's ready
// We don't need to do anything special here
}
Future<void> backgroundChannelInitialized() async {}

@override
Future<bool> executeTask(
String taskName, Map<String?, Object?>? inputData) async {
// Convert the input data to the expected format
final Map<String, dynamic>? convertedInputData =
inputData?.cast<String, dynamic>();

// Call the user's background task handler
final convertedInputData = convertPigeonInputData(inputData);
final result = await Workmanager._backgroundTaskHandler
?.call(taskName, convertedInputData);
return result ?? false;
Expand Down
95 changes: 95 additions & 0 deletions workmanager/test/pigeon_input_data_conversion_test.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
import 'package:test/test.dart';
import 'package:workmanager/src/workmanager_impl.dart';

void main() {
group('convertPigeonInputData', () {
test('handles null inputData', () {
final result = convertPigeonInputData(null);
expect(result, null);
});

test('filters null keys while preserving null values', () {
final Map<String?, Object?> inputData = {
'validKey': 'validValue',
'nullValueKey': null,
null: 'shouldBeFilteredOut',
'numberKey': 42,
'boolKey': true,
'listKey': ['item1', 'item2'],
};

final result = convertPigeonInputData(inputData);

expect(result, isNotNull);
expect(result, isA<Map<String, dynamic>>());
expect(result!.length, 5);

expect(result['validKey'], 'validValue');
expect(result['nullValueKey'], null);
expect(result['numberKey'], 42);
expect(result['boolKey'], true);
expect(result['listKey'], ['item1', 'item2']);
expect(result.containsKey(null), false);
});

test('handles empty inputData', () {
final result = convertPigeonInputData({});

expect(result, isNotNull);
expect(result, isEmpty);
expect(result, isA<Map<String, dynamic>>());
});

test('handles inputData with only null keys', () {
final result = convertPigeonInputData({null: 'value1'});

expect(result, isNotNull);
expect(result, isEmpty);
expect(result, isA<Map<String, dynamic>>());
});

test('handles mixed valid and invalid keys', () {
final Map<String?, Object?> mixedData = {
'key1': 'value1',
null: 'nullKeyValue',
'key2': null,
'': 'emptyStringKey',
'key3': 123,
};

final result = convertPigeonInputData(mixedData);

expect(result!.length, 4);
expect(result['key1'], 'value1');
expect(result['key2'], null);
expect(result[''], 'emptyStringKey');
expect(result['key3'], 123);
expect(result.containsKey(null), false);
});

test('preserves complex nested data structures', () {
final Map<String?, Object?> complexData = {
'mapKey': {'nested': 'value'},
'listKey': [
1,
2,
{'nested': 'list'}
],
'nullKey': null,
null: 'filtered',
};

final result = convertPigeonInputData(complexData);

expect(result!.length, 3);
expect(result['mapKey'], {'nested': 'value'});
expect(result['listKey'], [
1,
2,
{'nested': 'list'}
]);
expect(result['nullKey'], null);
expect(result.containsKey(null), false);
});
});
}
3 changes: 3 additions & 0 deletions workmanager_android/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
## Future

### Bug Fixes
* Fix null callback crash in BackgroundWorker when FlutterCallbackInformation is null (thanks to @jonathanduke, @Muneeza-PT)

### Improvements
* Improve SharedPreferenceHelper callback handling - now calls callback immediately when preferences are already loaded

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,13 @@ class BackgroundWorker(
) {
val callbackHandle = SharedPreferenceHelper.getCallbackHandle(applicationContext)
val callbackInfo = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle)

if (callbackInfo == null) {
Log.e(TAG, "Failed to resolve Dart callback for handle $callbackHandle.")
completer?.set(Result.failure())
return@ensureInitializationCompleteAsync
}

val dartBundlePath = flutterLoader.findAppBundlePath()

if (isInDebug) {
Expand Down
Loading