blob: 2110cc7b6bb3850d8b2b05437785a052f2ad3755 [file] [log] [blame]
// 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');
} on FormatException catch (e, stack) {
debugPrint('Invalid JSON file: $e');
debugPrint('$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({super.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 final AnimationController _animation = AnimationController(
duration: const Duration(milliseconds: 3600),
vsync: this,
)..repeat();
late final CalculationManager _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:
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()));
}