Permalink
9e67385 Sep 26, 2016
@Hixie @pq @krisgiesing @abarth
313 lines (281 sloc) 9.09 KB
// Copyright 2016 The Chromium 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 void OnProgressListener(double completed, double total);
typedef void OnResultListener(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({ this.onProgressListener, this.onResultListener, String data })
// 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) {
assert(onProgressListener != null);
assert(onResultListener != null);
}
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;
JsonDecoder decoder = new JsonDecoder(
(dynamic key, dynamic value) {
if (key is int && i++ % _NOTIFY_INTERVAL == 0)
onProgressListener(i.toDouble(), _NUM_ITEMS.toDouble());
return value;
}
);
try {
List<dynamic> result = decoder.convert(_data);
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) {
StringBuffer buffer = new 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({ this.onProgressListener, this.onResultListener })
: _receivePort = new ReceivePort() {
assert(onProgressListener != null);
assert(onResultListener != null);
_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. Note that this is done in the main isolate; at the
// moment, spawned isolates do not have access to Mojo services, including
// the root bundle (see https://github.com/flutter/flutter/issues/3294).
// However, the loading process is asynchronous, so the UI will not block
// while the file is loaded.
rootBundle.loadString('services/data.json').then((String data) {
if (isRunning) {
CalculationMessage message = new 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(_calculate, message).then((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) {
SendPort sender = message.sendPort;
Calculator calculator = new Calculator(
onProgressListener: (double completed, double total) {
sender.send(<double>[ completed, total ]);
},
onResultListener: (String result) {
sender.send(result);
},
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 {
@override
IsolateExampleState createState() => new IsolateExampleState();
}
// Main application state.
class IsolateExampleState extends State<StatefulWidget> with SingleTickerProviderStateMixin {
String _status = 'Idle';
String _label = 'Start';
String _result = ' ';
double _progress = 0.0;
AnimationController _animation;
CalculationManager _calculationManager;
@override
void initState() {
super.initState();
_animation = new AnimationController(
duration: const Duration(milliseconds: 3600),
vsync: this,
)..repeat();
_calculationManager = new CalculationManager(
onProgressListener: _handleProgressUpdate,
onResultListener: _handleResult
);
}
@override
void dispose() {
_animation.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return new Material(
child: new Column(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: <Widget>[
new RotationTransition(
turns: _animation,
child: new Container(
width: 120.0,
height: 120.0,
decoration: const BoxDecoration(
backgroundColor: const Color(0xFF882222)
)
)
),
new Opacity(
opacity: _calculationManager.isRunning ? 1.0 : 0.0,
child: new CircularProgressIndicator(
value: _progress
)
),
new Text(_status),
new Center(
child: new RaisedButton(
child: new Text(_label),
onPressed: _handleButtonPressed
)
),
new 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(new MaterialApp(home: new IsolateExampleWidget()));
}