Skip to content

fix(android): prevent crash if callbackInfo is null in BackgroundWorker (fixes #527)#615

Closed
jonathanduke wants to merge 2 commits intofluttercommunity:mainfrom
jonathanduke:fix-npe
Closed

fix(android): prevent crash if callbackInfo is null in BackgroundWorker (fixes #527)#615
jonathanduke wants to merge 2 commits intofluttercommunity:mainfrom
jonathanduke:fix-npe

Conversation

@jonathanduke
Copy link

@jonathanduke jonathanduke commented Jul 9, 2025

Fixes #527

This PR prevents a NullPointerException when FlutterCallbackInformation.lookupCallbackInformation(...) returns null in BackgroundWorker.kt.

Cause

If the stored callback handle (retrieved from SharedPreferences) is invalid or stale, lookupCallbackInformation(...) returns null. The existing code attempts to access callbackInfo.callbackLibraryPath without checking for null, which crashes the app.

Fix

Add a null check for callbackInfo and fail gracefully:

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

This should prevent the app from terminating with an error such as those mentioned in the linked issue:
Attempt to read from field 'java.lang.String io.flutter.view.FlutterCallbackInformation.callbackLibraryPath'

EDIT: fixed compilation error on return thanks to @Tulleb

@docs-page
Copy link

docs-page bot commented Jul 9, 2025

To view this pull requests documentation preview, visit the following URL:

docs.page/fluttercommunity/flutter_workmanager~615

Documentation is deployed and generated using docs.page.

@Tulleb
Copy link

Tulleb commented Jul 17, 2025

I am facing compilation issue with your fix @jonathanduke:

e: file:///[...]/BackgroundWorker.kt:100:17 'return' is prohibited here.
e: file:///[...]/BackgroundWorker.kt:100:24 Return type mismatch: expected 'com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result>', actual 'androidx.work.ListenableWorker.Result'.

FAILURE: Build failed with an exception.

@jonathanduke
Copy link
Author

I am facing compilation issue with your fix @jonathanduke:

e: file:///[...]/BackgroundWorker.kt:100:17 'return' is prohibited here.
e: file:///[...]/BackgroundWorker.kt:100:24 Return type mismatch: expected 'com.google.common.util.concurrent.ListenableFuture<androidx.work.ListenableWorker.Result>', actual 'androidx.work.ListenableWorker.Result'.

FAILURE: Build failed with an exception.

Thank you, @Tulleb. I was able to update and compile locally.

@Tulleb
Copy link

Tulleb commented Jul 20, 2025

I can confirm my latest build release is not crashing anymore thanks to this fix 👍

But I'm worried it does prevent some background thread from properly running though.

@jonathanduke
Copy link
Author

But I'm worried it does prevent some background thread from properly running though.

Yes, I agree. Fixing the null exception should prevent the whole app from crashing, but the actual cause of the issue with the handle being invalid is not fixed here. From what I understand, the plugin seems to be storing a pointer in the shared preferences that can become invalid if the signature of your app's background entry method changes. But also in recent commits, the authors changed the key in the shared preferences dictionary, so it might also be related to that. I had a hard time replicating the error on-demand to investigate that further.

@jonathanduke
Copy link
Author

@ened, this issue seems to be affecting many users as discussed in issues #527 and #621. Can you review when you get a chance and publish a patched version if possible? As discussed above, it doesn't fix the cause of the issue related to the handle stored in shared preferences; this is just intended to prevent the exception from crashing the whole app. Thanks!

@mihalycsaba
Copy link

I can confirm this has happened to me, had to reinstall the app to be able to launch it.

@jonathanduke
Copy link
Author

I can confirm this has happened to me, had to reinstall the app to be able to launch it.

I attempted to add this workaround to my app to prevent the exception before I patched the plugin code:

  // isNewBuild is a custom value where I'm detecting if the build number increased since the last run
  if (Platform.isAndroid && (kDebugMode || isNewBuild)) {
    final dir = await getApplicationSupportDirectory();
    final prefsFile = File(
      p.join(dir.path, "../shared_prefs", "flutter_workmanager_plugin.xml"),
    );
    if (await prefsFile.exists()) {
      await prefsFile.delete();
    }
  }

  await Workmanager().initialize(/* ... */);

@spiderion
Copy link

Are we waiting for anyone to get this merger? Who needs to approve it?

ened added a commit that referenced this pull request Jul 29, 2025
Add null check for FlutterCallbackInformation.lookupCallbackInformation()
result to prevent NullPointerException when callback handle is invalid or stale.

This resolves crashes reported in:
- Issue #624: App crashes with null FlutterCallbackInformation
- Issue #621: App crashes after 0.8.0 upgrade
- PR #615 from @jonathanduke
- PR #625 from @Muneeza-PT

The fix logs error and returns failure result instead of crashing
when callback resolution fails.

Includes unit test to verify null callback handling.

Thanks to @jonathanduke and @Muneeza-PT for the initial fix.
ened added a commit that referenced this pull request Jul 29, 2025
…626)

* fix: prevent null callback crash in BackgroundWorker

Add null check for FlutterCallbackInformation.lookupCallbackInformation()
result to prevent NullPointerException when callback handle is invalid or stale.

This resolves crashes reported in:
- Issue #624: App crashes with null FlutterCallbackInformation
- Issue #621: App crashes after 0.8.0 upgrade
- PR #615 from @jonathanduke
- PR #625 from @Muneeza-PT

The fix logs error and returns failure result instead of crashing
when callback resolution fails.

Includes unit test to verify null callback handling.

Thanks to @jonathanduke and @Muneeza-PT for the initial fix.

* fix: prevent null cast exception in executeTask inputData handling

Replace unsafe cast() method with manual filtering to safely handle
null keys and values in inputData Map<String?, Object?>.

Original issue: inputData?.cast<String, dynamic>() would fail when
the map contained null keys or when cast failed with TypeError.

The fix:
- Manually iterate through inputData entries
- Filter out null keys to prevent invalid Map entries
- Preserve null values which are valid in Map<String, dynamic>
- Return null when inputData is null (preserves existing behavior)

This resolves crashes when native platforms pass inputData with
null keys or incompatible value types.

Includes unit test to verify null argument handling doesn't crash.

Thanks to @Dr-wgy for reporting this issue in PR #618.

* style: apply ktlint and dart format fixes

- ktlint formatted BackgroundWorkerTest.kt
- dart format fixed workmanager_test.dart formatting

These should have been run before the initial commits.

* refactor: remove useless tests and improve test quality requirements

- Remove meaningless assert(true) test from workmanager_test.dart
- Replace useless BackgroundWorkerTest with documented placeholder
- Add comprehensive test quality requirements to CLAUDE.md to prevent
  creation of compilation tests, assert(true) tests, and other
  meaningless test patterns
- Update formatting and verify all tests pass

The fixes are validated through compilation and integration testing
rather than pointless unit tests that check nothing.

* docs: document BackgroundWorker testing complexity and remove unused dependencies

- Remove complex BackgroundWorker unit test that fails due to Flutter engine dependencies
- Document why BackgroundWorker cannot be easily unit tested (Flutter native libraries)
- Add comprehensive testing notes for complex components to CLAUDE.md
- Remove unused Robolectric dependencies since BackgroundWorker requires integration testing
- Verify all remaining tests pass

The null callback handling fix is validated through:
1. Code review (null check exists in BackgroundWorker.kt)
2. Integration testing through example app
3. Manual testing with invalid callback handles

Unit testing BackgroundWorker requires Flutter engine initialization which
fails in JVM tests due to native library dependencies.

* test: add comprehensive tests for workmanager_impl inputData null handling

Create elaborate unit tests for the workmanager_impl.dart executeTask modification:
- Test null inputData handling
- Test inputData with null keys and values
- Test empty inputData
- Test mixed valid/invalid keys
- Demonstrate difference between unsafe cast() and safe manual filtering

The tests replicate the exact logic implemented in _WorkmanagerFlutterApiImpl.executeTask
to validate the null handling behavior that prevents TypeError crashes.

Also remove unused androidx.work:work-testing dependency as requested.

All tests pass, confirming the fix correctly:
- Filters out null keys to prevent downstream issues
- Preserves null values (which are valid in Map<String, dynamic>)
- Handles all edge cases safely

* refactor: remove test that duplicated implementation code

Remove workmanager_impl_test.dart that replicated the exact logic
from _WorkmanagerFlutterApiImpl.executeTask instead of testing it.

The null handling fix in workmanager_impl.dart is validated through:
- Code review: null handling logic exists in executeTask method
- Integration testing: example app and manual testing
- Compilation: code compiles and runs correctly
- Existing unit tests: other workmanager tests continue to pass

Testing private implementation details that can't be accessed
through public APIs should be avoided to prevent code duplication.

* refactor: extract input data conversion logic for proper unit testing

Extract the critical null handling logic from _WorkmanagerFlutterApiImpl.executeTask
into a separate @VisibleForTesting function convertPigeonInputData().

This approach:
- Avoids code duplication (tests the actual implementation, not a copy)
- Makes the critical logic testable without exposing private implementation details
- Maintains the same behavior in executeTask by calling the extracted function

Add comprehensive unit tests for convertPigeonInputData covering:
- Null inputData handling
- Null key filtering (preserves null values, filters null keys)
- Empty data handling
- Mixed valid/invalid keys
- Complex nested data structures
- Comparison with unsafe cast() approach

All tests pass (10 total), confirming the null handling logic prevents
the TypeError crashes reported in PR #618.

* refactor: clean up test and implementation comments

- Remove unnecessary 'demonstrates safe handling vs unsafe cast approach' test
- Simplify test names and remove excessive comments
- Clean up implementation comments to be more concise
- Code should be self-explanatory without dense commenting

The tests still validate all the important edge cases for null handling
without the unnecessary comparison test.
@ened
Copy link
Collaborator

ened commented Jul 29, 2025

Thank you for the contribution! This fix has been integrated into PR #626 and merged. A new version with this fix will be released soon.

@ened ened closed this Jul 29, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

java.lang.NullPointerException while app is in backeground around 2 hours than it's getting crashed

5 participants