| // 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/foundation.dart'; | |
| 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); | |
| 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 { | |
| @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; | |
| AnimationController _animation; | |
| 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: RaisedButton( | |
| child: Text(_label), | |
| onPressed: _handleButtonPressed, | |
| ), | |
| ), | |
| 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(MaterialApp(home: IsolateExampleWidget())); | |
| } |