Skip to content

[Bug]: No implementation found for method finish on channel com.transistorsoft/flutter_background_fetch/methods #409

@galacticgibbon

Description

@galacticgibbon

Required Reading

  • Confirmed

Plugin Version

^1.5.0

Flutter Doctor

[✓] Flutter (Channel stable, 3.38.5, on macOS 15.5 24F74 darwin-arm64, locale en-GB)
[!] Android toolchain - develop for Android devices (Android SDK version 35.0.0)
    ! Some Android licenses not accepted. To resolve this, run: flutter doctor --android-licenses
[✓] Xcode - develop for iOS and macOS (Xcode 16.4)
[✓] Chrome - develop for the web
[✓] Connected device (4 available)
    ! Error: Browsing on the local area network for Development iPad. Ensure the device is unlocked and attached with a cable or associated with the same local area network as this Mac.
      The device must be opted into Developer Mode to connect wirelessly. (code -27)
    ! Error: Browsing on the local area network for Apple Watch. Ensure the device is unlocked and discoverable via Bluetooth. (code -27)
    ! Error: Browsing on the local area network for Dev phone. Ensure the device is unlocked and attached with a cable or associated with the same local area network as this Mac.
      The device must be opted into Developer Mode to connect wirelessly. (code -27)
[✓] Network resources

Mobile operating-system(s)

  • iOS
  • Android

Device Manufacturer(s) and Model(s)

SM-S921B 47% SM-X730 46%

Device operating-systems(s)

Android 16

What happened?

Not sure, this is happening in production and I can't reproduce.

Plugin Code and/or Config

/// Background Refresh Handler - Daily widget snapshot rebuild via background_fetch
///
/// ## Purpose
///
/// Ensures widget snapshots are regenerated daily so "today" queries reflect
/// the correct date. This is separate from FCM sync - FCM handles data freshness,
/// this handles date boundary changes.
///
/// ## Execution Context
///
/// background_fetch runs in the same process as the main app (similar to FCM).
/// Uses global database singletons to avoid lock conflicts.
///
/// ## When This Runs
///
/// - iOS: ~every 15 minutes (OS controlled, cannot be more frequent)
/// - Android: configurable minimum interval
/// - Only rebuilds if the day has changed since last refresh
library;

import 'dart:async';

import 'package:background_fetch/background_fetch.dart';
import 'package:bullet_common/common.dart';
import 'package:home_widget/home_widget.dart';
import 'package:hooks_riverpod/hooks_riverpod.dart';

import 'init_services.dart';
import 'src/config/bullet_env.dart';
import 'src/features/widget/widget_snapshot_coordinator.dart';

/// Initialize background fetch - call before runApp()
Future<void> initBackgroundRefresh() async {
  if (!kIsMobile) return;

  try {
    await BackgroundFetch.configure(
      BackgroundFetchConfig(
        minimumFetchInterval: 15, // iOS minimum, Android can be lower
        stopOnTerminate: false,
        enableHeadless: true,
        startOnBoot: true,
        requiresBatteryNotLow: false,
        requiresCharging: false,
        requiresStorageNotLow: false,
        requiresDeviceIdle: false,
        requiredNetworkType: NetworkType.NONE,
      ),
      _onBackgroundFetch,
      _onBackgroundFetchTimeout,
    );

    // Register Android headless task for background fetch after app termination
    BackgroundFetch.registerHeadlessTask(_headlessTask);
  } catch (e, stackTrace) {
    // Background fetch initialization can fail on beta OS versions or
    // devices that don't support it - log and continue since it's optional
    await ErrorMonitor.captureException(
      e,
      stackTrace,
      handler: 'background_refresh_init',
      level: SentryLevel.warning,
    );
  }
}

/// Headless task for Android - runs even after app termination
@pragma('vm:entry-point')
void _headlessTask(HeadlessTask task) async {
  final taskId = task.taskId;

  if (task.timeout) {
    BackgroundFetch.finish(taskId);
    return;
  }

  await _handleBackgroundRefresh(taskId);
}

/// Event callback - called when background fetch fires
Future<void> _onBackgroundFetch(String taskId) async {
  await _handleBackgroundRefresh(taskId);
}

/// Timeout callback - must finish immediately
Future<void> _onBackgroundFetchTimeout(String taskId) async {
  BackgroundFetch.finish(taskId);
}

/// Core refresh logic - shared by foreground and headless handlers
Future<void> _handleBackgroundRefresh(String taskId) async {
  try {
    // Check if day changed since last refresh
    final lastRefreshDate = await HomeWidget.getWidgetData<String>(
      'last_refresh_date',
    );
    final today = Date.today().toString();

    if (lastRefreshDate == today) {
      // Already refreshed today, skip
      BackgroundFetch.finish(taskId);
      return;
    }

    final env = BulletEnv();

    // Create container with background-safe PowerSync override
    final container = ProviderContainer(
      overrides: [
        envProvider.overrideWithValue(env),
        powersyncInitializerProvider.overrideWith((ref) async {
          final authRepo = ref.watch(authRepositoryProvider);
          final packageInfo = ref
              .watch(packageInfoInitializerProvider)
              .requireValue;
          final connection = PowersyncConnection.instance;
          await connection.initBackground(
            app: env.app,
            authRepo: authRepo,
            packageInfo: packageInfo,
          );
          return connection;
        }),
      ],
    );

    try {
      await initServices(container: container);

      final coordinator = await container.read(
        widgetSnapshotCoordinatorProvider.future,
      );

      await coordinator.rebuildAllSnapshots(shouldNotify: true);

      // Mark today as refreshed
      await HomeWidget.saveWidgetData('last_refresh_date', today);
    } finally {
      container.dispose();
    }
  } catch (e, stackTrace) {
    await ErrorMonitor.captureException(
      e,
      stackTrace,
      handler: 'background_widget_refresh',
      level: SentryLevel.warning,
    );
  }

  BackgroundFetch.finish(taskId);
}

Relevant log output

Crashed in non-app: platform_channel.dart in MethodChannel._invokeMethod within flutter

Show 1 more frame
background_fetch.dart in BackgroundFetch.finish at line 582 within background_fetch
In App
No additional details are available for this frame.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions