Flutter 3.7.0 (#1556)
* Update `simplistic_editor` for Flutter 3.4 beta * Re-enable beta and master CI * Disable on master * Added sample code for using plugins and channels from background isolates. * goderbauer feedback 1 * goderbauer feedback2 * goderbauer feedback 3 * Add `background_isolate_channels` to CI * Enable beta CI * Enable all `stable` CI projects * `dart fix --apply` * `print` -> `denugPrint` * Make deps min version not pinned * Drop `_isDebug` * Remove unused import * `dart format` * Fixup `linting_tool` * Fixup `form_app` * Enable all `master` CI * Basic fixes * Patch `simplistic_editor` * Fix nl at eol * Comment out `simplistic_editor` * Incorporating @bleroux's latest changes * Clean up CI scripts * Copy `experimental/material_3_demo` to top level * Update `game_template` * Update `animations` * Update `desktop_photo_search` * Update `flutter_maps_firestore` * Update `form_app` * Update `infinite_list` * Update `isolate_example` * Update `jsonexample` * Update `navigation_and_routing` * Update `place_tracker` * Update `platform_channels` * Update `platform_design` * Update `provider_shopper` * Fixup `context_menus` * `dart format` * Update the main `material_3_demo` * Make `tool/flutter_ci_script_stable.sh` executable again Co-authored-by: Bruno Leroux <bruno.leroux@gmail.com> Co-authored-by: Aaron Clarke <aaclarke@google.com>pull/1591/head
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 295 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 450 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 462 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 704 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 586 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 762 B |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.4 KiB |
@ -0,0 +1 @@
|
||||
include: ../analysis_options.yaml
|
@ -0,0 +1,155 @@
|
||||
// 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';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:path/path.dart' as path;
|
||||
import 'package:path_provider/path_provider.dart' as path_provider;
|
||||
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<MyHomePage> createState() {
|
||||
return _MyHomePageState();
|
||||
}
|
||||
}
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> {
|
||||
/// 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<String>? _entries;
|
||||
|
||||
/// What is searched for in the [SimpleDatabase].
|
||||
String _query = '';
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
// Write the value to [SharedPreferences] which will get read on the
|
||||
// [SimpleDatabase]'s isolate. For this example the value is always true
|
||||
// just for demonstration purposes.
|
||||
final Future<void> sharedPreferencesSet = SharedPreferences.getInstance()
|
||||
.then(
|
||||
(sharedPreferences) => sharedPreferences.setBool('isDebug', true));
|
||||
final Future<Directory> 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((values) {
|
||||
final Directory? tempDir = values[1] as Directory?;
|
||||
final String dbPath = path.join(tempDir!.path, 'database.db');
|
||||
SimpleDatabase.open(dbPath).then((database) {
|
||||
setState(() {
|
||||
_database = database;
|
||||
});
|
||||
_refresh();
|
||||
});
|
||||
});
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_database?.stop();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
/// 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:
|
||||
_database == null ? null : (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: _database == null ? null : _addDate,
|
||||
tooltip: 'Add',
|
||||
child: const Icon(Icons.add),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -0,0 +1,255 @@
|
||||
// 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';
|
||||
import 'dart:io';
|
||||
import 'dart:isolate';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/services.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)----------------------->|
|
||||
// |<---------------(ack)------------------------>|
|
||||
// | |
|
||||
// |----------------(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 {
|
||||
const _Command(this.code, {this.arg0, this.arg1});
|
||||
|
||||
final _Codes code;
|
||||
final Object? arg0;
|
||||
final Object? arg1;
|
||||
}
|
||||
|
||||
/// 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 {
|
||||
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 be queued up and
|
||||
// handled serially.
|
||||
final Queue<Completer<void>> _completers = Queue<Completer<void>>();
|
||||
// Similarly, StreamControllers are stored in a queue so they can be handled
|
||||
// asynchronously and serially.
|
||||
final Queue<StreamController<String>> _resultsStream =
|
||||
Queue<StreamController<String>>();
|
||||
|
||||
/// Open the database at [path] and launch the server on a background isolate..
|
||||
static Future<SimpleDatabase> 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<void> completer = Completer<void>();
|
||||
result._completers.addFirst(completer);
|
||||
receivePort.listen((message) {
|
||||
result._handleCommand(message as _Command);
|
||||
});
|
||||
await completer.future;
|
||||
return result;
|
||||
}
|
||||
|
||||
/// Writes [value] to the database.
|
||||
Future<void> addEntry(String value) {
|
||||
// No processing happens on the calling isolate, it gets delegated to the
|
||||
// background isolate, see [__SimpleDatabaseServer._doAddEntry].
|
||||
Completer<void> completer = Completer<void>();
|
||||
_completers.addFirst(completer);
|
||||
_sendPort.send(_Command(_Codes.add, arg0: value));
|
||||
return completer.future;
|
||||
}
|
||||
|
||||
/// Returns all the strings in the database that contain [query].
|
||||
Stream<String> find(String query) {
|
||||
// No processing happens on the calling isolate, it gets delegated to the
|
||||
// background isolate, see [__SimpleDatabaseServer._doFind].
|
||||
StreamController<String> resultsStream = StreamController<String>();
|
||||
_resultsStream.addFirst(resultsStream);
|
||||
_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 _handleCommand(_Command command) {
|
||||
switch (command.code) {
|
||||
case _Codes.init:
|
||||
_sendPort = command.arg0 as SendPort;
|
||||
// ----------------------------------------------------------------------
|
||||
// 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:
|
||||
debugPrint('SimpleDatabase unrecognized command: ${command.code}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Kills the background isolate and its database server.
|
||||
void stop() {
|
||||
_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;
|
||||
late final String _path;
|
||||
|
||||
// ----------------------------------------------------------------------
|
||||
// Here the plugin is used from the background isolate.
|
||||
// ----------------------------------------------------------------------
|
||||
|
||||
/// 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((message) async {
|
||||
final _Command command = message as _Command;
|
||||
await server._handleCommand(command);
|
||||
});
|
||||
}
|
||||
|
||||
/// Handle the [command] received from the [ReceivePort].
|
||||
Future<void> _handleCommand(_Command command) async {
|
||||
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);
|
||||
_sendPort.send(const _Command(_Codes.ack, arg0: null));
|
||||
break;
|
||||
case _Codes.add:
|
||||
_doAddEntry(command.arg0 as String);
|
||||
break;
|
||||
case _Codes.query:
|
||||
_doFind(command.arg0 as String);
|
||||
break;
|
||||
default:
|
||||
debugPrint(
|
||||
'_SimpleDatabaseServer unrecognized command ${command.code}');
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform the add entry operation.
|
||||
void _doAddEntry(String value) {
|
||||
debugPrint('Performing add: $value');
|
||||
File file = File(_path);
|
||||
if (!file.existsSync()) {
|
||||
file.createSync();
|
||||
}
|
||||
RandomAccessFile writer = file.openSync(mode: FileMode.append);
|
||||
List<int> bytes = utf8.encode(value);
|
||||
if (bytes.length > _entrySize) {
|
||||
bytes = bytes.sublist(0, _entrySize);
|
||||
} else if (bytes.length < _entrySize) {
|
||||
List<int> newBytes = List.filled(_entrySize, 0);
|
||||
for (int i = 0; i < bytes.length; ++i) {
|
||||
newBytes[i] = bytes[i];
|
||||
}
|
||||
bytes = newBytes;
|
||||
}
|
||||
writer.writeFromSync(bytes);
|
||||
writer.closeSync();
|
||||
_sendPort.send(const _Command(_Codes.ack, arg0: null));
|
||||
}
|
||||
|
||||
/// Perform the find entry operation.
|
||||
void _doFind(String query) {
|
||||
debugPrint('Performing find: $query');
|
||||
File file = File(_path);
|
||||
if (file.existsSync()) {
|
||||
RandomAccessFile reader = file.openSync();
|
||||
List<int> buffer = List.filled(_entrySize, 0);
|
||||
while (reader.readIntoSync(buffer) == _entrySize) {
|
||||
List<int> 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));
|
||||
}
|
||||
}
|
@ -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.18.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.1
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
|
||||
flutter:
|
||||
uses-material-design: true
|
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 295 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 450 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 462 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 704 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 586 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 762 B |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.4 KiB |
Before Width: | Height: | Size: 564 B After Width: | Height: | Size: 295 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 450 B |
Before Width: | Height: | Size: 1.0 KiB After Width: | Height: | Size: 282 B |
Before Width: | Height: | Size: 1.7 KiB After Width: | Height: | Size: 462 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 704 B |
Before Width: | Height: | Size: 1.3 KiB After Width: | Height: | Size: 406 B |
Before Width: | Height: | Size: 1.9 KiB After Width: | Height: | Size: 586 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 2.6 KiB After Width: | Height: | Size: 862 B |
Before Width: | Height: | Size: 3.7 KiB After Width: | Height: | Size: 1.6 KiB |
Before Width: | Height: | Size: 1.8 KiB After Width: | Height: | Size: 762 B |
Before Width: | Height: | Size: 3.2 KiB After Width: | Height: | Size: 1.2 KiB |
Before Width: | Height: | Size: 3.5 KiB After Width: | Height: | Size: 1.4 KiB |