// 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 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((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(_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) { _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([ 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 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: [ 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())); }