mirror of
https://github.com/flutter/flutter
synced 2024-10-13 19:52:53 +00:00
309 lines
9 KiB
Dart
309 lines
9 KiB
Dart
// Copyright 2014 The Flutter Authors. 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:convert';
|
|
import 'dart:isolate';
|
|
|
|
import 'package:flutter/material.dart';
|
|
import 'package:flutter/services.dart';
|
|
|
|
typedef OnProgressListener = void Function(double completed, double total);
|
|
typedef OnResultListener = void Function(String result);
|
|
|
|
// An encapsulation of a large amount of synchronous processing.
|
|
//
|
|
// The choice of JSON parsing here is meant as an example that might surface
|
|
// in real-world applications.
|
|
class Calculator {
|
|
Calculator({ required this.onProgressListener, required this.onResultListener, String? data })
|
|
: assert(onProgressListener != null),
|
|
assert(onResultListener != null),
|
|
// In order to keep the example files smaller, we "cheat" a little and
|
|
// replicate our small json string into a 10,000-element array.
|
|
_data = _replicateJson(data, 10000);
|
|
|
|
final OnProgressListener onProgressListener;
|
|
final OnResultListener onResultListener;
|
|
final String _data;
|
|
// This example assumes that the number of objects to parse is known in
|
|
// advance. In a real-world situation, this might not be true; in that case,
|
|
// the app might choose to display an indeterminate progress indicator.
|
|
static const int _NUM_ITEMS = 110000;
|
|
static const int _NOTIFY_INTERVAL = 1000;
|
|
|
|
// Run the computation associated with this Calculator.
|
|
void run() {
|
|
int i = 0;
|
|
final JsonDecoder decoder = JsonDecoder(
|
|
(dynamic key, dynamic value) {
|
|
if (key is int && i++ % _NOTIFY_INTERVAL == 0)
|
|
onProgressListener(i.toDouble(), _NUM_ITEMS.toDouble());
|
|
return value;
|
|
},
|
|
);
|
|
try {
|
|
final List<dynamic> result = decoder.convert(_data) as List<dynamic>;
|
|
final int n = result.length;
|
|
onResultListener('Decoded $n results');
|
|
} catch (e, stack) {
|
|
print('Invalid JSON file: $e');
|
|
print(stack);
|
|
}
|
|
}
|
|
|
|
static String _replicateJson(String? data, int count) {
|
|
final StringBuffer buffer = StringBuffer()..write('[');
|
|
for (int i = 0; i < count; i++) {
|
|
buffer.write(data);
|
|
if (i < count - 1)
|
|
buffer.write(',');
|
|
}
|
|
buffer.write(']');
|
|
return buffer.toString();
|
|
}
|
|
}
|
|
|
|
// The current state of the calculation.
|
|
enum CalculationState {
|
|
idle,
|
|
loading,
|
|
calculating
|
|
}
|
|
|
|
// Structured message to initialize the spawned isolate.
|
|
class CalculationMessage {
|
|
CalculationMessage(this.data, this.sendPort);
|
|
String data;
|
|
SendPort sendPort;
|
|
}
|
|
|
|
// A manager for the connection to a spawned isolate.
|
|
//
|
|
// Isolates communicate with each other via ReceivePorts and SendPorts.
|
|
// This class manages these ports and maintains state related to the
|
|
// progress of the background computation.
|
|
class CalculationManager {
|
|
CalculationManager({ required this.onProgressListener, required this.onResultListener })
|
|
: assert(onProgressListener != null),
|
|
assert(onResultListener != null),
|
|
_receivePort = ReceivePort() {
|
|
_receivePort.listen(_handleMessage);
|
|
}
|
|
|
|
CalculationState _state = CalculationState.idle;
|
|
CalculationState get state => _state;
|
|
bool get isRunning => _state != CalculationState.idle;
|
|
|
|
double _completed = 0.0;
|
|
double _total = 1.0;
|
|
|
|
final OnProgressListener onProgressListener;
|
|
final OnResultListener onResultListener;
|
|
|
|
// Start the background computation.
|
|
//
|
|
// Does nothing if the computation is already running.
|
|
void start() {
|
|
if (!isRunning) {
|
|
_state = CalculationState.loading;
|
|
_runCalculation();
|
|
}
|
|
}
|
|
|
|
// Stop the background computation.
|
|
//
|
|
// Kills the isolate immediately, if spawned. Does nothing if the
|
|
// computation is not running.
|
|
void stop() {
|
|
if (isRunning) {
|
|
_state = CalculationState.idle;
|
|
if (_isolate != null) {
|
|
_isolate!.kill(priority: Isolate.immediate);
|
|
_isolate = null;
|
|
_completed = 0.0;
|
|
_total = 1.0;
|
|
}
|
|
}
|
|
}
|
|
|
|
final ReceivePort _receivePort;
|
|
Isolate? _isolate;
|
|
|
|
void _runCalculation() {
|
|
// Load the JSON string. This is done in the main isolate because spawned
|
|
// isolates do not have access to the root bundle. However, the loading
|
|
// process is asynchronous, so the UI will not block while the file is
|
|
// loaded.
|
|
rootBundle.loadString('services/data.json').then<void>((String data) {
|
|
if (isRunning) {
|
|
final CalculationMessage message = CalculationMessage(data, _receivePort.sendPort);
|
|
// Spawn an isolate to JSON-parse the file contents. The JSON parsing
|
|
// is synchronous, so if done in the main isolate, the UI would block.
|
|
Isolate.spawn<CalculationMessage>(_calculate, message).then<void>((Isolate isolate) {
|
|
if (!isRunning) {
|
|
isolate.kill(priority: Isolate.immediate);
|
|
} else {
|
|
_state = CalculationState.calculating;
|
|
_isolate = isolate;
|
|
}
|
|
});
|
|
}
|
|
});
|
|
}
|
|
|
|
void _handleMessage(dynamic message) {
|
|
if (message is List<double>) {
|
|
_completed = message[0];
|
|
_total = message[1];
|
|
onProgressListener(_completed, _total);
|
|
} else if (message is String) {
|
|
_completed = 0.0;
|
|
_total = 1.0;
|
|
_isolate = null;
|
|
_state = CalculationState.idle;
|
|
onResultListener(message);
|
|
}
|
|
}
|
|
|
|
// Main entry point for the spawned isolate.
|
|
//
|
|
// This entry point must be static, and its (single) argument must match
|
|
// the message passed in Isolate.spawn above. Typically, some part of the
|
|
// message will contain a SendPort so that the spawned isolate can
|
|
// communicate back to the main isolate.
|
|
//
|
|
// Static and global variables are initialized anew in the spawned isolate,
|
|
// in a separate memory space.
|
|
static void _calculate(CalculationMessage message) {
|
|
final SendPort sender = message.sendPort;
|
|
final Calculator calculator = Calculator(
|
|
onProgressListener: (double completed, double total) {
|
|
sender.send(<double>[ completed, total ]);
|
|
},
|
|
onResultListener: sender.send,
|
|
data: message.data,
|
|
);
|
|
calculator.run();
|
|
}
|
|
}
|
|
|
|
// Main app widget.
|
|
//
|
|
// The app shows a simple UI that allows control of the background computation,
|
|
// as well as an animation to illustrate that the UI does not block while this
|
|
// computation is performed.
|
|
//
|
|
// This is a StatefulWidget in order to hold the CalculationManager and
|
|
// the AnimationController for the running animation.
|
|
class IsolateExampleWidget extends StatefulWidget {
|
|
const IsolateExampleWidget({Key? key}) : super(key: key);
|
|
|
|
@override
|
|
IsolateExampleState createState() => IsolateExampleState();
|
|
}
|
|
|
|
// Main application state.
|
|
class IsolateExampleState extends State<StatefulWidget> with SingleTickerProviderStateMixin {
|
|
|
|
String _status = 'Idle';
|
|
String _label = 'Start';
|
|
String _result = ' ';
|
|
double _progress = 0.0;
|
|
late AnimationController _animation;
|
|
late CalculationManager _calculationManager;
|
|
|
|
@override
|
|
void initState() {
|
|
super.initState();
|
|
_animation = AnimationController(
|
|
duration: const Duration(milliseconds: 3600),
|
|
vsync: this,
|
|
)..repeat();
|
|
_calculationManager = CalculationManager(
|
|
onProgressListener: _handleProgressUpdate,
|
|
onResultListener: _handleResult,
|
|
);
|
|
}
|
|
|
|
@override
|
|
void dispose() {
|
|
_animation.dispose();
|
|
super.dispose();
|
|
}
|
|
|
|
@override
|
|
Widget build(BuildContext context) {
|
|
return Material(
|
|
child: Column(
|
|
mainAxisAlignment: MainAxisAlignment.spaceAround,
|
|
children: <Widget>[
|
|
RotationTransition(
|
|
turns: _animation,
|
|
child: Container(
|
|
width: 120.0,
|
|
height: 120.0,
|
|
color: const Color(0xFF882222),
|
|
),
|
|
),
|
|
Opacity(
|
|
opacity: _calculationManager.isRunning ? 1.0 : 0.0,
|
|
child: CircularProgressIndicator(
|
|
value: _progress,
|
|
),
|
|
),
|
|
Text(_status),
|
|
Center(
|
|
child: ElevatedButton(
|
|
onPressed: _handleButtonPressed,
|
|
child: Text(_label),
|
|
),
|
|
),
|
|
Text(_result),
|
|
],
|
|
),
|
|
);
|
|
}
|
|
|
|
void _handleProgressUpdate(double completed, double total) {
|
|
_updateState(' ', completed / total);
|
|
}
|
|
|
|
void _handleResult(String result) {
|
|
_updateState(result, 0.0);
|
|
}
|
|
|
|
void _handleButtonPressed() {
|
|
if (_calculationManager.isRunning)
|
|
_calculationManager.stop();
|
|
else
|
|
_calculationManager.start();
|
|
_updateState(' ', 0.0);
|
|
}
|
|
|
|
String _getStatus(CalculationState state) {
|
|
switch (state) {
|
|
case CalculationState.loading:
|
|
return 'Loading...';
|
|
case CalculationState.calculating:
|
|
return 'In Progress';
|
|
case CalculationState.idle:
|
|
default:
|
|
return 'Idle';
|
|
}
|
|
}
|
|
|
|
void _updateState(String result, double progress) {
|
|
setState(() {
|
|
_result = result;
|
|
_progress = progress;
|
|
_label = _calculationManager.isRunning ? 'Stop' : 'Start';
|
|
_status = _getStatus(_calculationManager.state);
|
|
});
|
|
}
|
|
}
|
|
|
|
void main() {
|
|
runApp(const MaterialApp(home: IsolateExampleWidget()));
|
|
}
|