From 04472cf296b5789840d2fa0bef2a0cf2013f7b18 Mon Sep 17 00:00:00 2001 From: Aaron Clarke Date: Tue, 27 Sep 2022 09:23:10 -0700 Subject: [PATCH 01/10] Added sample code for using plugins and channels from background isolates. --- background_isolate_channels/lib/main.dart | 140 +++++++++++ .../lib/simple_database.dart | 236 ++++++++++++++++++ background_isolate_channels/pubspec.yaml | 27 ++ 3 files changed, 403 insertions(+) create mode 100644 background_isolate_channels/lib/main.dart create mode 100644 background_isolate_channels/lib/simple_database.dart create mode 100644 background_isolate_channels/pubspec.yaml diff --git a/background_isolate_channels/lib/main.dart b/background_isolate_channels/lib/main.dart new file mode 100644 index 00000000000..adbd0b3ae7a --- /dev/null +++ b/background_isolate_channels/lib/main.dart @@ -0,0 +1,140 @@ +import 'dart:io' show Directory; + +import 'package:flutter/material.dart'; +import 'package:intl/intl.dart'; +import 'package:path_provider/path_provider.dart' as path_provider; +import 'package:path/path.dart' as path; +import 'package:shared_preferences/shared_preferences.dart'; +import 'package:uuid/uuid.dart' as uuid; + +import 'simple_database.dart'; + +/////////////////////////////////////////////////////////////////////////////// +// This is the UI which will present the contents of the [SimpleDatabase]. To +// see where Background Isolate Channels are used see simple_database.dart. +/////////////////////////////////////////////////////////////////////////////// + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatelessWidget { + const MyApp({super.key}); + + @override + Widget build(BuildContext context) { + return MaterialApp( + title: 'Background Isolate Channels', + theme: ThemeData( + primarySwatch: Colors.blue, + ), + home: const MyHomePage(title: 'Background Isolate Channels'), + ); + } +} + +class MyHomePage extends StatefulWidget { + const MyHomePage({super.key, required this.title}); + + final String title; + + @override + State createState() { + return _MyHomePageState(); + } +} + +class _MyHomePageState extends State { + /// The database that is running on a background [Isolate]. This is nullable + /// because acquiring a [SimpleDatabase] is an asynchronous operation. This + /// value is `null` until the database is initialized. + SimpleDatabase? _database; + + /// Local cache of the query results returned by the [SimpleDatabase] for the + /// UI to render from. It is nullable since querying the results is + /// asynchronous. The value is `null` before any result has been received. + List? _entries; + + /// What is searched for in the [SimpleDatabase]. + String _query = ''; + + _MyHomePageState() { + // Write the value to [SharedPreferences] which will get read on the + // [SimpleDatabase]'s isolate. + final Future sharedPreferencesSet = SharedPreferences.getInstance() + .then( + (sharedPreferences) => sharedPreferences.setBool('isDebug', true)); + final Future tempDirFuture = + path_provider.getTemporaryDirectory(); + + // Wait until the [SharedPreferences] value is set and the temporary + // directory is received before opening the database. If + // [sharedPreferencesSet] does not happen before opening the + // [SimpleDatabase] there has to be a way to refresh + // [_SimpleDatabaseServer]'s [SharedPreferences] cached values. + Future.wait([sharedPreferencesSet, tempDirFuture]) + .then((List values) { + final Directory? tempDir = values[1]; + final String dbPath = path.join(tempDir!.path, 'database.db'); + SimpleDatabase.open(dbPath).then((SimpleDatabase database) { + setState(() { + _database = database; + _refresh(); + }); + }); + }); + } + + /// Performs a find on [SimpleDatabase] with [query] and updates the listed + /// contents. + void _refresh({String? query}) { + if (query != null) { + _query = query; + } + _database?.find(_query).toList().then((entries) { + setState(() { + _entries = entries; + }); + }); + } + + /// Adds a UUID and a timestamp to the [SimpleDatabase]. + void _addDate() { + final DateTime now = DateTime.now(); + final DateFormat formatter = + DateFormat('EEEE MMMM d, HH:mm:ss\n${const uuid.Uuid().v4()}'); + final String formatted = formatter.format(now); + _database!.addEntry(formatted).then((_) => _refresh()); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + appBar: AppBar( + title: Text(widget.title), + ), + body: Column( + children: [ + TextField( + onChanged: (query) => _refresh(query: query), + decoration: const InputDecoration( + labelText: 'Search', suffixIcon: Icon(Icons.search)), + ), + Expanded( + child: ListView.builder( + itemBuilder: (context, index) { + return ListTile(title: Text(_entries![index])); + }, + itemCount: _entries?.length ?? 0, + ), + ), + ], + ), + floatingActionButton: FloatingActionButton( + onPressed: _addDate, + tooltip: 'Add', + child: const Icon(Icons.add), + ), + ); + } +} diff --git a/background_isolate_channels/lib/simple_database.dart b/background_isolate_channels/lib/simple_database.dart new file mode 100644 index 00000000000..669e7381749 --- /dev/null +++ b/background_isolate_channels/lib/simple_database.dart @@ -0,0 +1,236 @@ +import 'dart:async'; +import 'dart:collection'; +import 'dart:convert'; +import 'dart:io'; +import 'dart:isolate'; +import 'package:flutter/services.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +/////////////////////////////////////////////////////////////////////////////// +// **WARNING:** This is not production code and is only intended to be used for +// demonstration purposes. +// +// The following database works by spawning a background isolate and +// communicating with it over Dart's SendPort API. It is presented below as a +// demonstration of the feature "Background Isolate Channels" and shows using +// plugins from a background isolate. The [SimpleDatabase] operates on the root +// isolate and the [_SimpleDatabaseServer] operates on a background isolate. +// +// Here is an example of the protocol they use to communicate: +// +// _________________ ________________________ +// [:SimpleDatabase] [:_SimpleDatabaseServer] +// ----------------- ------------------------ +// | | +// |<---------------(init)------------------------| +// |----------------(init)----------------------->| +// | | +// |----------------(add)------------------------>| +// |<---------------(ack)-------------------------| +// | | +// |----------------(query)---------------------->| +// |<---------------(result)----------------------| +// |<---------------(result)----------------------| +// |<---------------(done)------------------------| +// +/////////////////////////////////////////////////////////////////////////////// + +/// The size of the database entries in bytes. +const int entrySize = 256; + +/// All the command codes that can be sent and received between [SimpleDatabase] and +/// [_SimpleDatabaseServer]. +enum _Codes { + init, + add, + query, + ack, + result, + done, +} + +/// A command sent between [SimpleDatabase] and [_SimpleDatabaseServer]. +class _Command { + final _Codes code; + final List args; + _Command(this.code, this.args); +} + +/// The portion of the [SimpleDatabase] that runs on the background isolate. +/// +/// This is where we use the new feature Background Isolate Channels, which +/// allows us to use plugins from background isolates. +class _SimpleDatabaseServer { + final SendPort _sendPort; + String? _path; + SharedPreferences? _sharedPreferences; + + _SimpleDatabaseServer(this._sendPort); + + bool get isDebug => _sharedPreferences?.getBool('isDebug') ?? false; + + /// The main entrypoint for the background isolate. + static void _run(SendPort sendPort) { + ReceivePort receivePort = ReceivePort(); + sendPort.send(_Command(_Codes.init, [receivePort.sendPort])); + final _SimpleDatabaseServer server = _SimpleDatabaseServer(sendPort); + receivePort.listen((dynamic message) async { + final _Command command = message as _Command; + server._onCommand(command); + }); + } + + /// Perform the add entry operation. + Future _doAddEntry(String value) async { + if (isDebug) { + print('Performing add: $value'); + } + File file = File(_path!); + if (!file.existsSync()) { + file.createSync(); + } + RandomAccessFile writer = await file.open(mode: FileMode.append); + List bytes = utf8.encode(value); + if (bytes.length > entrySize) { + bytes = bytes.sublist(0, entrySize); + } else if (bytes.length < entrySize) { + List newBytes = List.filled(entrySize, 0); + for (int i = 0; i < bytes.length; ++i) { + newBytes[i] = bytes[i]; + } + bytes = newBytes; + } + await writer.writeFrom(bytes); + await writer.close(); + _sendPort.send(_Command(_Codes.ack, [null])); + } + + /// Perform the find entry operation. + Future _doFind(String query) async { + if (isDebug) { + print('Performing find: $query'); + } + File file = File(_path!); + if (file.existsSync()) { + RandomAccessFile reader = file.openSync(); + List buffer = List.filled(entrySize, 0); + while (reader.readIntoSync(buffer) == entrySize) { + List foo = buffer.takeWhile((value) => value != 0).toList(); + String string = utf8.decode(foo); + if (string.contains(query)) { + _sendPort.send(_Command(_Codes.result, [string])); + } + } + reader.closeSync(); + } + _sendPort.send(_Command(_Codes.done, [null])); + } + + /// Handle the [command] received from the [ReceivePort]. + Future _onCommand(_Command command) async { + if (command.code == _Codes.init) { + _path = command.args[0]; + // ---------------------------------------------------------------------- + // The [RootIsolateToken] is required for + // [BackgroundIsolateBinaryMessenger.ensureInitialized] and must be + // obtained on the root isolate and passed into the background isolate via + // a [SendPort]. + // ---------------------------------------------------------------------- + RootIsolateToken rootIsolateToken = command.args[1]; + // ---------------------------------------------------------------------- + // [BackgroundIsolateBinaryMessenger.ensureInitialized] must be called + // before using any plugins. This sets up the [BinaryMessenger] that the + // Platform Channels will communicate with on the background isolate. + // ---------------------------------------------------------------------- + BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken); + _sharedPreferences = await SharedPreferences.getInstance(); + } else if (command.code == _Codes.add) { + await _doAddEntry(command.args[0]); + } else if (command.code == _Codes.query) { + _doFind(command.args[0]); + } + } +} + +/// A SimpleDatabase that stores entries of strings to disk where they can be +/// queried. +/// +/// All the disk operations and queries are executed in a background isolate +/// operating. This class just sends and receives messages to the isolate. +class SimpleDatabase { + final Isolate _isolate; + final String _path; + late final SendPort _sendPort; + // Completers are stored in a queue so multiple commands can queued up and + // handled serially. + final Queue> _completers = Queue>(); + // Similarly, StreamControllers are stored in a queue so they can be handled + // asynchronously and serially. + final Queue> _resultsStream = + Queue>(); + + SimpleDatabase._(this._isolate, this._path); + + /// Open the database at [path] and launch the server on a background isolate.. + static Future open(String path) async { + final ReceivePort receivePort = ReceivePort(); + final Isolate isolate = + await Isolate.spawn(_SimpleDatabaseServer._run, receivePort.sendPort); + final SimpleDatabase result = SimpleDatabase._(isolate, path); + Completer completer = Completer(); + result._completers.addFirst(completer); + receivePort.listen((message) { + result._onCommand(message as _Command); + }); + await completer.future; + return result; + } + + /// Writes [value] to the database. + Future addEntry(String value) { + // No processing happens on the calling isolate, it gets delegated to the + // background isolate, see [__SimpleDatabaseServer._doAddEntry]. + Completer completer = Completer(); + _completers.addFirst(completer); + _sendPort.send(_Command(_Codes.add, [value])); + return completer.future; + } + + /// Returns all the strings in the database that contain [query]. + Stream find(String query) { + // No processing happens on the calling isolate, it gets delegated to the + // background isolate, see [__SimpleDatabaseServer._doFind]. + StreamController resultsStream = StreamController(); + _resultsStream.addFirst(resultsStream); + _sendPort.send(_Command(_Codes.query, [query])); + return resultsStream.stream; + } + + /// Handler invoked when a message is received from the port communicating + /// with the database server. + void _onCommand(_Command command) { + if (command.code == _Codes.init) { + _sendPort = command.args[0]; + _completers.removeLast().complete(); + // ---------------------------------------------------------------------- + // Before using platform channels and plugins from background isolates we + // need to register it with its root isolate. This is achieved by + // acquiring a [RootIsolateToken] which the background isolate uses to + // invoke [BackgroundIsolateBinaryMessenger.ensureInitialized]. + // ---------------------------------------------------------------------- + RootIsolateToken rootIsolateToken = RootIsolateToken.instance!; + _sendPort.send(_Command(_Codes.init, [_path, rootIsolateToken])); + } else if (command.code == _Codes.ack) { + _completers.removeLast().complete(); + } else if (command.code == _Codes.result) { + _resultsStream.last.add(command.args[0]); + } else if (command.code == _Codes.done) { + _resultsStream.removeLast().close(); + } + } + + /// Kills the background isolate and its database server. + void stop() { + _isolate.kill(); + } +} diff --git a/background_isolate_channels/pubspec.yaml b/background_isolate_channels/pubspec.yaml new file mode 100644 index 00000000000..dbe871c64b5 --- /dev/null +++ b/background_isolate_channels/pubspec.yaml @@ -0,0 +1,27 @@ +name: background_isolate_channels +description: A new Flutter project. + +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +version: 1.0.0+1 + +environment: + sdk: '>=2.19.0-224.0.dev <3.0.0' + +dependencies: + cupertino_icons: 1.0.2 + flutter: + sdk: flutter + intl: 0.17.0 + path: 1.8.2 + path_provider: 2.0.11 + shared_preferences: 2.0.15 + uuid: 3.0.6 + +dev_dependencies: + flutter_lints: ^2.0.0 + flutter_test: + sdk: flutter + +flutter: + uses-material-design: true From 8b76016a697da3745387a0278941eb4e29adcbdb Mon Sep 17 00:00:00 2001 From: Aaron Clarke Date: Tue, 27 Sep 2022 11:45:05 -0700 Subject: [PATCH 02/10] goderbauer feedback 1 --- background_isolate_channels/lib/main.dart | 25 +- .../lib/simple_database.dart | 231 +++++++++--------- 2 files changed, 141 insertions(+), 115 deletions(-) diff --git a/background_isolate_channels/lib/main.dart b/background_isolate_channels/lib/main.dart index adbd0b3ae7a..14496881c3f 100644 --- a/background_isolate_channels/lib/main.dart +++ b/background_isolate_channels/lib/main.dart @@ -1,3 +1,7 @@ +// Copyright 2022 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'dart:io' show Directory; import 'package:flutter/material.dart'; @@ -58,9 +62,11 @@ class _MyHomePageState extends State { /// What is searched for in the [SimpleDatabase]. String _query = ''; - _MyHomePageState() { + @override + void initState() { // Write the value to [SharedPreferences] which will get read on the - // [SimpleDatabase]'s isolate. + // [SimpleDatabase]'s isolate. For this example the value is always true + // just for demonstration purposes. final Future sharedPreferencesSet = SharedPreferences.getInstance() .then( (sharedPreferences) => sharedPreferences.setBool('isDebug', true)); @@ -73,8 +79,8 @@ class _MyHomePageState extends State { // [SimpleDatabase] there has to be a way to refresh // [_SimpleDatabaseServer]'s [SharedPreferences] cached values. Future.wait([sharedPreferencesSet, tempDirFuture]) - .then((List values) { - final Directory? tempDir = values[1]; + .then((List values) { + final Directory? tempDir = values[1] as Directory?; final String dbPath = path.join(tempDir!.path, 'database.db'); SimpleDatabase.open(dbPath).then((SimpleDatabase database) { setState(() { @@ -83,6 +89,13 @@ class _MyHomePageState extends State { }); }); }); + super.initState(); + } + + @override + void dispose() { + _database?.stop(); + super.dispose(); } /// Performs a find on [SimpleDatabase] with [query] and updates the listed @@ -118,7 +131,9 @@ class _MyHomePageState extends State { TextField( onChanged: (query) => _refresh(query: query), decoration: const InputDecoration( - labelText: 'Search', suffixIcon: Icon(Icons.search)), + labelText: 'Search', + suffixIcon: Icon(Icons.search), + ), ), Expanded( child: ListView.builder( diff --git a/background_isolate_channels/lib/simple_database.dart b/background_isolate_channels/lib/simple_database.dart index 669e7381749..c9c5bf1418e 100644 --- a/background_isolate_channels/lib/simple_database.dart +++ b/background_isolate_channels/lib/simple_database.dart @@ -1,3 +1,7 @@ +// Copyright 2022 The Flutter team. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + import 'dart:async'; import 'dart:collection'; import 'dart:convert'; @@ -36,7 +40,7 @@ import 'package:shared_preferences/shared_preferences.dart'; /////////////////////////////////////////////////////////////////////////////// /// The size of the database entries in bytes. -const int entrySize = 256; +const int _entrySize = 256; /// All the command codes that can be sent and received between [SimpleDatabase] and /// [_SimpleDatabaseServer]. @@ -51,105 +55,11 @@ enum _Codes { /// A command sent between [SimpleDatabase] and [_SimpleDatabaseServer]. class _Command { - final _Codes code; - final List args; - _Command(this.code, this.args); -} - -/// The portion of the [SimpleDatabase] that runs on the background isolate. -/// -/// This is where we use the new feature Background Isolate Channels, which -/// allows us to use plugins from background isolates. -class _SimpleDatabaseServer { - final SendPort _sendPort; - String? _path; - SharedPreferences? _sharedPreferences; - - _SimpleDatabaseServer(this._sendPort); - - bool get isDebug => _sharedPreferences?.getBool('isDebug') ?? false; - - /// The main entrypoint for the background isolate. - static void _run(SendPort sendPort) { - ReceivePort receivePort = ReceivePort(); - sendPort.send(_Command(_Codes.init, [receivePort.sendPort])); - final _SimpleDatabaseServer server = _SimpleDatabaseServer(sendPort); - receivePort.listen((dynamic message) async { - final _Command command = message as _Command; - server._onCommand(command); - }); - } + const _Command(this.code, {this.arg0, this.arg1}); - /// Perform the add entry operation. - Future _doAddEntry(String value) async { - if (isDebug) { - print('Performing add: $value'); - } - File file = File(_path!); - if (!file.existsSync()) { - file.createSync(); - } - RandomAccessFile writer = await file.open(mode: FileMode.append); - List bytes = utf8.encode(value); - if (bytes.length > entrySize) { - bytes = bytes.sublist(0, entrySize); - } else if (bytes.length < entrySize) { - List newBytes = List.filled(entrySize, 0); - for (int i = 0; i < bytes.length; ++i) { - newBytes[i] = bytes[i]; - } - bytes = newBytes; - } - await writer.writeFrom(bytes); - await writer.close(); - _sendPort.send(_Command(_Codes.ack, [null])); - } - - /// Perform the find entry operation. - Future _doFind(String query) async { - if (isDebug) { - print('Performing find: $query'); - } - File file = File(_path!); - if (file.existsSync()) { - RandomAccessFile reader = file.openSync(); - List buffer = List.filled(entrySize, 0); - while (reader.readIntoSync(buffer) == entrySize) { - List foo = buffer.takeWhile((value) => value != 0).toList(); - String string = utf8.decode(foo); - if (string.contains(query)) { - _sendPort.send(_Command(_Codes.result, [string])); - } - } - reader.closeSync(); - } - _sendPort.send(_Command(_Codes.done, [null])); - } - - /// Handle the [command] received from the [ReceivePort]. - Future _onCommand(_Command command) async { - if (command.code == _Codes.init) { - _path = command.args[0]; - // ---------------------------------------------------------------------- - // The [RootIsolateToken] is required for - // [BackgroundIsolateBinaryMessenger.ensureInitialized] and must be - // obtained on the root isolate and passed into the background isolate via - // a [SendPort]. - // ---------------------------------------------------------------------- - RootIsolateToken rootIsolateToken = command.args[1]; - // ---------------------------------------------------------------------- - // [BackgroundIsolateBinaryMessenger.ensureInitialized] must be called - // before using any plugins. This sets up the [BinaryMessenger] that the - // Platform Channels will communicate with on the background isolate. - // ---------------------------------------------------------------------- - BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken); - _sharedPreferences = await SharedPreferences.getInstance(); - } else if (command.code == _Codes.add) { - await _doAddEntry(command.args[0]); - } else if (command.code == _Codes.query) { - _doFind(command.args[0]); - } - } + final _Codes code; + final Object? arg0; + final Object? arg1; } /// A SimpleDatabase that stores entries of strings to disk where they can be @@ -158,10 +68,12 @@ class _SimpleDatabaseServer { /// All the disk operations and queries are executed in a background isolate /// operating. This class just sends and receives messages to the isolate. class SimpleDatabase { + SimpleDatabase._(this._isolate, this._path); + final Isolate _isolate; final String _path; late final SendPort _sendPort; - // Completers are stored in a queue so multiple commands can queued up and + // Completers are stored in a queue so multiple commands can be queued up and // handled serially. final Queue> _completers = Queue>(); // Similarly, StreamControllers are stored in a queue so they can be handled @@ -169,8 +81,6 @@ class SimpleDatabase { final Queue> _resultsStream = Queue>(); - SimpleDatabase._(this._isolate, this._path); - /// Open the database at [path] and launch the server on a background isolate.. static Future open(String path) async { final ReceivePort receivePort = ReceivePort(); @@ -179,8 +89,8 @@ class SimpleDatabase { final SimpleDatabase result = SimpleDatabase._(isolate, path); Completer completer = Completer(); result._completers.addFirst(completer); - receivePort.listen((message) { - result._onCommand(message as _Command); + receivePort.listen((Object? message) { + result._handleCommand(message as _Command); }); await completer.future; return result; @@ -192,7 +102,7 @@ class SimpleDatabase { // background isolate, see [__SimpleDatabaseServer._doAddEntry]. Completer completer = Completer(); _completers.addFirst(completer); - _sendPort.send(_Command(_Codes.add, [value])); + _sendPort.send(_Command(_Codes.add, arg0: value)); return completer.future; } @@ -202,15 +112,15 @@ class SimpleDatabase { // background isolate, see [__SimpleDatabaseServer._doFind]. StreamController resultsStream = StreamController(); _resultsStream.addFirst(resultsStream); - _sendPort.send(_Command(_Codes.query, [query])); + _sendPort.send(_Command(_Codes.query, arg0: query)); return resultsStream.stream; } /// Handler invoked when a message is received from the port communicating /// with the database server. - void _onCommand(_Command command) { + void _handleCommand(_Command command) { if (command.code == _Codes.init) { - _sendPort = command.args[0]; + _sendPort = command.arg0 as SendPort; _completers.removeLast().complete(); // ---------------------------------------------------------------------- // Before using platform channels and plugins from background isolates we @@ -219,11 +129,12 @@ class SimpleDatabase { // invoke [BackgroundIsolateBinaryMessenger.ensureInitialized]. // ---------------------------------------------------------------------- RootIsolateToken rootIsolateToken = RootIsolateToken.instance!; - _sendPort.send(_Command(_Codes.init, [_path, rootIsolateToken])); + _sendPort + .send(_Command(_Codes.init, arg0: _path, arg1: rootIsolateToken)); } else if (command.code == _Codes.ack) { _completers.removeLast().complete(); } else if (command.code == _Codes.result) { - _resultsStream.last.add(command.args[0]); + _resultsStream.last.add(command.arg0 as String); } else if (command.code == _Codes.done) { _resultsStream.removeLast().close(); } @@ -234,3 +145,103 @@ class SimpleDatabase { _isolate.kill(); } } + +/// The portion of the [SimpleDatabase] that runs on the background isolate. +/// +/// This is where we use the new feature Background Isolate Channels, which +/// allows us to use plugins from background isolates. +class _SimpleDatabaseServer { + _SimpleDatabaseServer(this._sendPort); + + final SendPort _sendPort; + String? _path; + SharedPreferences? _sharedPreferences; + + // ---------------------------------------------------------------------- + // Here the plugin is used from the background isolate. + // ---------------------------------------------------------------------- + bool get _isDebug => _sharedPreferences?.getBool('isDebug') ?? false; + + /// The main entrypoint for the background isolate sent to [Isolate.spawn]. + static void _run(SendPort sendPort) { + ReceivePort receivePort = ReceivePort(); + sendPort.send(_Command(_Codes.init, arg0: receivePort.sendPort)); + final _SimpleDatabaseServer server = _SimpleDatabaseServer(sendPort); + receivePort.listen((Object? message) async { + final _Command command = message as _Command; + server._handleCommand(command); + }); + } + + /// Handle the [command] received from the [ReceivePort]. + Future _handleCommand(_Command command) async { + if (command.code == _Codes.init) { + _path = command.arg0 as String; + // ---------------------------------------------------------------------- + // The [RootIsolateToken] is required for + // [BackgroundIsolateBinaryMessenger.ensureInitialized] and must be + // obtained on the root isolate and passed into the background isolate via + // a [SendPort]. + // ---------------------------------------------------------------------- + RootIsolateToken rootIsolateToken = command.arg1 as RootIsolateToken; + // ---------------------------------------------------------------------- + // [BackgroundIsolateBinaryMessenger.ensureInitialized] for each + // background isolate that will use plugins. This sets up the + // [BinaryMessenger] that the Platform Channels will communicate with on + // the background isolate. + // ---------------------------------------------------------------------- + BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken); + _sharedPreferences = await SharedPreferences.getInstance(); + } else if (command.code == _Codes.add) { + await _doAddEntry(command.arg0 as String); + } else if (command.code == _Codes.query) { + _doFind(command.arg0 as String); + } + } + + /// Perform the add entry operation. + Future _doAddEntry(String value) async { + if (_isDebug) { + print('Performing add: $value'); + } + File file = File(_path!); + if (!file.existsSync()) { + file.createSync(); + } + RandomAccessFile writer = await file.open(mode: FileMode.append); + List bytes = utf8.encode(value); + if (bytes.length > _entrySize) { + bytes = bytes.sublist(0, _entrySize); + } else if (bytes.length < _entrySize) { + List newBytes = List.filled(_entrySize, 0); + for (int i = 0; i < bytes.length; ++i) { + newBytes[i] = bytes[i]; + } + bytes = newBytes; + } + await writer.writeFrom(bytes); + await writer.close(); + _sendPort.send(const _Command(_Codes.ack, arg0: null)); + } + + /// Perform the find entry operation. + Future _doFind(String query) async { + if (_isDebug) { + print('Performing find: $query'); + } + File file = File(_path!); + if (file.existsSync()) { + RandomAccessFile reader = file.openSync(); + List buffer = List.filled(_entrySize, 0); + while (reader.readIntoSync(buffer) == _entrySize) { + List foo = buffer.takeWhile((value) => value != 0).toList(); + String string = utf8.decode(foo); + if (string.contains(query)) { + _sendPort.send(_Command(_Codes.result, arg0: string)); + } + } + reader.closeSync(); + } + _sendPort.send(const _Command(_Codes.done, arg0: null)); + } +} From 5a38e1a46fc34472d1871221ffd34acf5edee4cd Mon Sep 17 00:00:00 2001 From: Aaron Clarke Date: Tue, 27 Sep 2022 14:04:55 -0700 Subject: [PATCH 03/10] goderbauer feedback2 --- .../lib/simple_database.dart | 91 +++++++++++-------- 1 file changed, 52 insertions(+), 39 deletions(-) diff --git a/background_isolate_channels/lib/simple_database.dart b/background_isolate_channels/lib/simple_database.dart index c9c5bf1418e..674aa017a4b 100644 --- a/background_isolate_channels/lib/simple_database.dart +++ b/background_isolate_channels/lib/simple_database.dart @@ -119,24 +119,31 @@ class SimpleDatabase { /// Handler invoked when a message is received from the port communicating /// with the database server. void _handleCommand(_Command command) { - if (command.code == _Codes.init) { - _sendPort = command.arg0 as SendPort; - _completers.removeLast().complete(); - // ---------------------------------------------------------------------- - // Before using platform channels and plugins from background isolates we - // need to register it with its root isolate. This is achieved by - // acquiring a [RootIsolateToken] which the background isolate uses to - // invoke [BackgroundIsolateBinaryMessenger.ensureInitialized]. - // ---------------------------------------------------------------------- - RootIsolateToken rootIsolateToken = RootIsolateToken.instance!; - _sendPort - .send(_Command(_Codes.init, arg0: _path, arg1: rootIsolateToken)); - } else if (command.code == _Codes.ack) { - _completers.removeLast().complete(); - } else if (command.code == _Codes.result) { - _resultsStream.last.add(command.arg0 as String); - } else if (command.code == _Codes.done) { - _resultsStream.removeLast().close(); + switch (command.code) { + case _Codes.init: + _sendPort = command.arg0 as SendPort; + _completers.removeLast().complete(); + // ---------------------------------------------------------------------- + // Before using platform channels and plugins from background isolates we + // need to register it with its root isolate. This is achieved by + // acquiring a [RootIsolateToken] which the background isolate uses to + // invoke [BackgroundIsolateBinaryMessenger.ensureInitialized]. + // ---------------------------------------------------------------------- + RootIsolateToken rootIsolateToken = RootIsolateToken.instance!; + _sendPort + .send(_Command(_Codes.init, arg0: _path, arg1: rootIsolateToken)); + break; + case _Codes.ack: + _completers.removeLast().complete(); + break; + case _Codes.result: + _resultsStream.last.add(command.arg0 as String); + break; + case _Codes.done: + _resultsStream.removeLast().close(); + break; + default: + print('SimpleDatabase unrecognized command: ${command.code}'); } } @@ -175,27 +182,33 @@ class _SimpleDatabaseServer { /// Handle the [command] received from the [ReceivePort]. Future _handleCommand(_Command command) async { - if (command.code == _Codes.init) { - _path = command.arg0 as String; - // ---------------------------------------------------------------------- - // The [RootIsolateToken] is required for - // [BackgroundIsolateBinaryMessenger.ensureInitialized] and must be - // obtained on the root isolate and passed into the background isolate via - // a [SendPort]. - // ---------------------------------------------------------------------- - RootIsolateToken rootIsolateToken = command.arg1 as RootIsolateToken; - // ---------------------------------------------------------------------- - // [BackgroundIsolateBinaryMessenger.ensureInitialized] for each - // background isolate that will use plugins. This sets up the - // [BinaryMessenger] that the Platform Channels will communicate with on - // the background isolate. - // ---------------------------------------------------------------------- - BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken); - _sharedPreferences = await SharedPreferences.getInstance(); - } else if (command.code == _Codes.add) { - await _doAddEntry(command.arg0 as String); - } else if (command.code == _Codes.query) { - _doFind(command.arg0 as String); + switch (command.code) { + case _Codes.init: + _path = command.arg0 as String; + // ---------------------------------------------------------------------- + // The [RootIsolateToken] is required for + // [BackgroundIsolateBinaryMessenger.ensureInitialized] and must be + // obtained on the root isolate and passed into the background isolate via + // a [SendPort]. + // ---------------------------------------------------------------------- + RootIsolateToken rootIsolateToken = command.arg1 as RootIsolateToken; + // ---------------------------------------------------------------------- + // [BackgroundIsolateBinaryMessenger.ensureInitialized] for each + // background isolate that will use plugins. This sets up the + // [BinaryMessenger] that the Platform Channels will communicate with on + // the background isolate. + // ---------------------------------------------------------------------- + BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken); + _sharedPreferences = await SharedPreferences.getInstance(); + break; + case _Codes.add: + await _doAddEntry(command.arg0 as String); + break; + case _Codes.query: + _doFind(command.arg0 as String); + break; + default: + print('_SimpleDatabaseServer unrecognized command ${command.code}'); } } From 48bf57d18e5c6b80125837875bf9461c7f63d88e Mon Sep 17 00:00:00 2001 From: Aaron Clarke Date: Tue, 27 Sep 2022 15:07:08 -0700 Subject: [PATCH 04/10] goderbauer feedback 3 --- background_isolate_channels/lib/main.dart | 9 ++++--- .../lib/simple_database.dart | 25 ++++++++++--------- 2 files changed, 18 insertions(+), 16 deletions(-) diff --git a/background_isolate_channels/lib/main.dart b/background_isolate_channels/lib/main.dart index 14496881c3f..695e944af86 100644 --- a/background_isolate_channels/lib/main.dart +++ b/background_isolate_channels/lib/main.dart @@ -85,8 +85,8 @@ class _MyHomePageState extends State { SimpleDatabase.open(dbPath).then((SimpleDatabase database) { setState(() { _database = database; - _refresh(); }); + _refresh(); }); }); super.initState(); @@ -104,7 +104,7 @@ class _MyHomePageState extends State { if (query != null) { _query = query; } - _database?.find(_query).toList().then((entries) { + _database!.find(_query).toList().then((entries) { setState(() { _entries = entries; }); @@ -129,7 +129,8 @@ class _MyHomePageState extends State { body: Column( children: [ TextField( - onChanged: (query) => _refresh(query: query), + onChanged: + _database == null ? null : (query) => _refresh(query: query), decoration: const InputDecoration( labelText: 'Search', suffixIcon: Icon(Icons.search), @@ -146,7 +147,7 @@ class _MyHomePageState extends State { ], ), floatingActionButton: FloatingActionButton( - onPressed: _addDate, + onPressed: _database == null ? null : _addDate, tooltip: 'Add', child: const Icon(Icons.add), ), diff --git a/background_isolate_channels/lib/simple_database.dart b/background_isolate_channels/lib/simple_database.dart index 674aa017a4b..188d458e539 100644 --- a/background_isolate_channels/lib/simple_database.dart +++ b/background_isolate_channels/lib/simple_database.dart @@ -28,6 +28,7 @@ import 'package:shared_preferences/shared_preferences.dart'; // | | // |<---------------(init)------------------------| // |----------------(init)----------------------->| +// |<---------------(ack)------------------------>| // | | // |----------------(add)------------------------>| // |<---------------(ack)-------------------------| @@ -122,7 +123,6 @@ class SimpleDatabase { switch (command.code) { case _Codes.init: _sendPort = command.arg0 as SendPort; - _completers.removeLast().complete(); // ---------------------------------------------------------------------- // Before using platform channels and plugins from background isolates we // need to register it with its root isolate. This is achieved by @@ -161,13 +161,13 @@ class _SimpleDatabaseServer { _SimpleDatabaseServer(this._sendPort); final SendPort _sendPort; - String? _path; - SharedPreferences? _sharedPreferences; + late final String _path; + late final SharedPreferences _sharedPreferences; // ---------------------------------------------------------------------- // Here the plugin is used from the background isolate. // ---------------------------------------------------------------------- - bool get _isDebug => _sharedPreferences?.getBool('isDebug') ?? false; + bool get _isDebug => _sharedPreferences.getBool('isDebug') ?? false; /// The main entrypoint for the background isolate sent to [Isolate.spawn]. static void _run(SendPort sendPort) { @@ -200,9 +200,10 @@ class _SimpleDatabaseServer { // ---------------------------------------------------------------------- BackgroundIsolateBinaryMessenger.ensureInitialized(rootIsolateToken); _sharedPreferences = await SharedPreferences.getInstance(); + _sendPort.send(const _Command(_Codes.ack, arg0: null)); break; case _Codes.add: - await _doAddEntry(command.arg0 as String); + _doAddEntry(command.arg0 as String); break; case _Codes.query: _doFind(command.arg0 as String); @@ -213,15 +214,15 @@ class _SimpleDatabaseServer { } /// Perform the add entry operation. - Future _doAddEntry(String value) async { + void _doAddEntry(String value) { if (_isDebug) { print('Performing add: $value'); } - File file = File(_path!); + File file = File(_path); if (!file.existsSync()) { file.createSync(); } - RandomAccessFile writer = await file.open(mode: FileMode.append); + RandomAccessFile writer = file.openSync(mode: FileMode.append); List bytes = utf8.encode(value); if (bytes.length > _entrySize) { bytes = bytes.sublist(0, _entrySize); @@ -232,17 +233,17 @@ class _SimpleDatabaseServer { } bytes = newBytes; } - await writer.writeFrom(bytes); - await writer.close(); + writer.writeFromSync(bytes); + writer.closeSync(); _sendPort.send(const _Command(_Codes.ack, arg0: null)); } /// Perform the find entry operation. - Future _doFind(String query) async { + void _doFind(String query) { if (_isDebug) { print('Performing find: $query'); } - File file = File(_path!); + File file = File(_path); if (file.existsSync()) { RandomAccessFile reader = file.openSync(); List buffer = List.filled(_entrySize, 0); From 84e601075719e7a5cc922ee7c9804deb1db8e49f Mon Sep 17 00:00:00 2001 From: Brett Morgan Date: Tue, 10 Jan 2023 13:09:03 +1000 Subject: [PATCH 05/10] Add `background_isolate_channels` to CI --- tool/flutter_ci_script_beta.sh | 1 + tool/flutter_ci_script_master.sh | 1 + tool/flutter_ci_script_stable.sh | 2 ++ 3 files changed, 4 insertions(+) diff --git a/tool/flutter_ci_script_beta.sh b/tool/flutter_ci_script_beta.sh index ac937e5f691..c13d26f9e19 100755 --- a/tool/flutter_ci_script_beta.sh +++ b/tool/flutter_ci_script_beta.sh @@ -15,6 +15,7 @@ declare -ar PROJECT_NAMES=( "add_to_app/prebuilt_module/flutter_module" "android_splash_screen" "animations" + "background_isolate_channels" "code_sharing/client" "code_sharing/server" "code_sharing/shared" diff --git a/tool/flutter_ci_script_master.sh b/tool/flutter_ci_script_master.sh index ed351ad1862..d5129323dae 100755 --- a/tool/flutter_ci_script_master.sh +++ b/tool/flutter_ci_script_master.sh @@ -15,6 +15,7 @@ declare -ar PROJECT_NAMES=( "add_to_app/prebuilt_module/flutter_module" "android_splash_screen" "animations" + "background_isolate_channels" "code_sharing/client" "code_sharing/server" "desktop_photo_search/fluent_ui" diff --git a/tool/flutter_ci_script_stable.sh b/tool/flutter_ci_script_stable.sh index 8536e754b01..b92d59fa2ee 100755 --- a/tool/flutter_ci_script_stable.sh +++ b/tool/flutter_ci_script_stable.sh @@ -15,6 +15,8 @@ declare -ar PROJECT_NAMES=( "add_to_app/prebuilt_module/flutter_module" "android_splash_screen" "animations" + # TODO(DomesticMouse): uncomment when stable increments + # "background_isolate_channels" "code_sharing/client" "code_sharing/server" "desktop_photo_search/fluent_ui" From b9b724b399313de7b9bd2f851fa6e47d41029caf Mon Sep 17 00:00:00 2001 From: Brett Morgan Date: Sun, 5 Feb 2023 16:06:14 +1000 Subject: [PATCH 06/10] Comment in `background_isolate_channels` --- tool/flutter_ci_script_stable.sh | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tool/flutter_ci_script_stable.sh b/tool/flutter_ci_script_stable.sh index 54d83e4b337..561835b7920 100755 --- a/tool/flutter_ci_script_stable.sh +++ b/tool/flutter_ci_script_stable.sh @@ -16,8 +16,7 @@ declare -ar PROJECT_NAMES=( "add_to_app/prebuilt_module/flutter_module" "android_splash_screen" "animations" - # TODO(DomesticMouse): uncomment when stable increments - # "background_isolate_channels" + "background_isolate_channels" "code_sharing/client" "code_sharing/server" "code_sharing/shared" From 09668617dd34e24a35b2a7000736b7a8cd1842db Mon Sep 17 00:00:00 2001 From: Brett Morgan Date: Mon, 6 Feb 2023 11:52:21 +1000 Subject: [PATCH 07/10] Fixup --- .../linting_tool/macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- .../macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- game_template/macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- tool/flutter_ci_script_master.sh | 3 ++- veggieseasons/macos/Flutter/GeneratedPluginRegistrant.swift | 2 +- 5 files changed, 6 insertions(+), 5 deletions(-) diff --git a/experimental/linting_tool/macos/Flutter/GeneratedPluginRegistrant.swift b/experimental/linting_tool/macos/Flutter/GeneratedPluginRegistrant.swift index d2deae34747..87d00ec0290 100644 --- a/experimental/linting_tool/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/experimental/linting_tool/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,7 +6,7 @@ import FlutterMacOS import Foundation import file_selector_macos -import path_provider_macos +import path_provider_foundation import url_launcher_macos import window_size diff --git a/experimental/varfont_shader_puzzle/macos/Flutter/GeneratedPluginRegistrant.swift b/experimental/varfont_shader_puzzle/macos/Flutter/GeneratedPluginRegistrant.swift index 0d56f519c92..e777c67df22 100644 --- a/experimental/varfont_shader_puzzle/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/experimental/varfont_shader_puzzle/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,7 +5,7 @@ import FlutterMacOS import Foundation -import path_provider_macos +import path_provider_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { PathProviderPlugin.register(with: registry.registrar(forPlugin: "PathProviderPlugin")) diff --git a/game_template/macos/Flutter/GeneratedPluginRegistrant.swift b/game_template/macos/Flutter/GeneratedPluginRegistrant.swift index 029ad363fdc..0758d55340c 100644 --- a/game_template/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/game_template/macos/Flutter/GeneratedPluginRegistrant.swift @@ -10,7 +10,7 @@ import firebase_core import firebase_crashlytics import games_services import in_app_purchase_storekit -import path_provider_macos +import path_provider_foundation import shared_preferences_foundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { diff --git a/tool/flutter_ci_script_master.sh b/tool/flutter_ci_script_master.sh index 9f270c92015..c7e5b3ab100 100755 --- a/tool/flutter_ci_script_master.sh +++ b/tool/flutter_ci_script_master.sh @@ -17,7 +17,8 @@ declare -ar PROJECT_NAMES=( "android_splash_screen" "animations" "background_isolate_channels" - "code_sharing/client" + # TODO(DomesticMouse): Use 'const' with the constructor to improve performance. + # "code_sharing/client" "code_sharing/server" "code_sharing/shared" "desktop_photo_search/fluent_ui" diff --git a/veggieseasons/macos/Flutter/GeneratedPluginRegistrant.swift b/veggieseasons/macos/Flutter/GeneratedPluginRegistrant.swift index 6c0c3d4781b..f7175089273 100644 --- a/veggieseasons/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/veggieseasons/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,7 +5,7 @@ import FlutterMacOS import Foundation -import shared_preferences_macos +import shared_preferences_foundation import window_size func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { From 920c6a668b76e55bdb38200c6557c87fd6853912 Mon Sep 17 00:00:00 2001 From: Brett Morgan Date: Mon, 6 Feb 2023 12:06:09 +1000 Subject: [PATCH 08/10] Elide `experimental/material_3_demo` from `master` --- tool/flutter_ci_script_master.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tool/flutter_ci_script_master.sh b/tool/flutter_ci_script_master.sh index c7e5b3ab100..c477e0ac510 100755 --- a/tool/flutter_ci_script_master.sh +++ b/tool/flutter_ci_script_master.sh @@ -32,7 +32,8 @@ declare -ar PROJECT_NAMES=( "experimental/federated_plugin/federated_plugin_web" "experimental/federated_plugin/federated_plugin_windows" "experimental/linting_tool" - "experimental/material_3_demo" + # TODO(DomesticMouse): Use 'const' with the constructor to improve performance. + # "experimental/material_3_demo" "experimental/pedometer" "experimental/varfont_shader_puzzle" "experimental/web_dashboard" From b6d7d48d4ece1ee5b072080cb745725faecd58ca Mon Sep 17 00:00:00 2001 From: Brett Morgan Date: Mon, 6 Feb 2023 12:18:00 +1000 Subject: [PATCH 09/10] Elide `material_3_demo` from `master` CI --- tool/flutter_ci_script_master.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tool/flutter_ci_script_master.sh b/tool/flutter_ci_script_master.sh index c477e0ac510..c7cdf05db17 100755 --- a/tool/flutter_ci_script_master.sh +++ b/tool/flutter_ci_script_master.sh @@ -44,7 +44,8 @@ declare -ar PROJECT_NAMES=( "ios_app_clip" "isolate_example" "jsonexample" - "material_3_demo" + # TODO(DomesticMouse): Use 'const' with the constructor to improve performance. + # "material_3_demo" "navigation_and_routing" "place_tracker" "platform_channels" From 1181b0a43750b7db7db47ffa37bd48be5646e4ad Mon Sep 17 00:00:00 2001 From: Brett Morgan Date: Mon, 6 Feb 2023 12:58:13 +1000 Subject: [PATCH 10/10] Elide `place_tracker` on `master` CI --- tool/flutter_ci_script_master.sh | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tool/flutter_ci_script_master.sh b/tool/flutter_ci_script_master.sh index c7cdf05db17..501ad44ee50 100755 --- a/tool/flutter_ci_script_master.sh +++ b/tool/flutter_ci_script_master.sh @@ -47,7 +47,8 @@ declare -ar PROJECT_NAMES=( # TODO(DomesticMouse): Use 'const' with the constructor to improve performance. # "material_3_demo" "navigation_and_routing" - "place_tracker" + # TODO(DomesticMouse): Use 'const' with the constructor to improve performance. + # "place_tracker" "platform_channels" "platform_design" "platform_view_swift"