foreground_service_isolate 0.1.4 copy "foreground_service_isolate: ^0.1.4" to clipboard
foreground_service_isolate: ^0.1.4 copied to clipboard

Spawn a long-running Flutter isolate in a foreground service

example/lib/main.dart

import 'dart:async';
import 'dart:isolate';
import 'dart:ui';

import 'package:material_ui/material_ui.dart';
import 'package:flutter_local_notifications/flutter_local_notifications.dart'
    hide NotificationDetails;
import 'package:foreground_service_isolate/foreground_service_isolate.dart';
import 'package:permission_handler/permission_handler.dart';

const eventChannelId = 'foreground_service_isolate_event';
const isolateName = 'foreground_service_isolate';
const notificationId = 1;

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  await Permission.notification.request();

  runApp(const ExampleApp());
}

@pragma('vm:entry-point')
void isolateEntryPoint(SendPort? send) {
  final connection = setupIsolate(
    send,
    onSendPortReady: (send) {
      IsolateNameServer.removePortNameMapping(isolateName);
      IsolateNameServer.registerPortWithName(send, isolateName);
    },
  );

  final stream = Stream<String>.periodic(
    const Duration(seconds: 1),
    (i) => 'Hello from the isolate: $i',
  ).asBroadcastStream();

  () async {
    while (true) {
      await Future.delayed(const Duration(seconds: 1));

      // This is an example
      // ignore: avoid_print
      print('Isolate is running...');
    }
  }();

  final eventChannel = IsolateEventChannel(eventChannelId, connection);
  eventChannel.setStreamHandler(
    IsolateStreamHandler.inline(
      onListen: (_, sink) => stream.listen(sink.success),
    ),
  );
}

class ExampleApp extends StatefulWidget {
  const ExampleApp({super.key});

  @override
  State<StatefulWidget> createState() => ExampleAppState();
}

class ExampleAppState extends State<ExampleApp> {
  IsolateConnection? connection;
  final messages = <String>[];

  @override
  void initState() {
    super.initState();
    initStateAsync();
  }

  void initStateAsync() async {
    final notifications = FlutterLocalNotificationsPlugin();
    await notifications.initialize(
      settings: const InitializationSettings(
        android: AndroidInitializationSettings('ic_launcher'),
        iOS: DarwinInitializationSettings(),
      ),
      onDidReceiveNotificationResponse: (details) {
        insertMessage('Tapped notification ${details.id}');
      },
    );

    final launchDetails = await notifications.getNotificationAppLaunchDetails();
    final launchedFromNotification =
        launchDetails != null &&
        launchDetails.didNotificationLaunchApp &&
        launchDetails.notificationResponse?.id == notificationId;

    if (launchedFromNotification) {
      insertMessage('Launched from notification $notificationId');
    }
  }

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      home: Scaffold(
        appBar: AppBar(title: const Text('Foreground Service Isolate')),
        body: Column(
          children: [
            ElevatedButton(onPressed: spawn, child: const Text('Spawn')),
            ElevatedButton(onPressed: connect, child: const Text('Connect')),
            ElevatedButton(onPressed: kill, child: const Text('Kill')),
            Expanded(
              child: ListView(
                children: [for (final message in messages) Text(message)],
              ),
            ),
          ],
        ),
      ),
    );
  }

  void spawn() async {
    connection = await spawnForegroundServiceIsolate(
      isolateEntryPoint,
      notificationDetails: const NotificationDetails(
        channelId: 'foreground_service_isolate',
        channelName: 'Foreground Service Isolate',
        id: notificationId,
        contentTitle: 'Foreground Service Isolate',
        contentText: 'Running...',
        smallIcon: 'ic_launcher',
      ),
    );
    stream();
  }

  void connect() async {
    if (connection != null) return;

    final send = IsolateNameServer.lookupPortByName(isolateName);
    if (send == null) return;

    connection = await connectToIsolate(send);
    stream();
  }

  void kill() {
    final connection = this.connection;
    if (connection == null) return;

    connection.close();
    this.connection = null;

    ForegroundServiceIsolate.stopService();
  }

  void stream() {
    final connection = this.connection;
    if (connection == null) return;

    final eventChannel = IsolateEventChannel(eventChannelId, connection);
    eventChannel.receiveBroadcastStream().cast<String>().listen(insertMessage);
  }

  void insertMessage(String message) {
    setState(() => messages.insert(0, message));
  }
}