Flutter concurrency for Swift developers
Flutter concurrency for Swift developers
Flutter 起步教程chevron_rightFlutter forchevron_rightFlutter concurrency for Swift developers
Both Dart and Swift support concurrent programming. This guide should help you understand how concurrency works in Dart and how it compares to Swift. With this understanding, you can create high-performing iOS apps.
When developing in the Apple ecosystem, some tasks might take a long time to complete. These tasks include fetching or processing large amounts of data. iOS developers typically use Grand Central Dispatch (GCD) to schedule tasks using a shared thread pool. With GCD, developers add tasks to dispatch queues and GCD decides on which thread to execute them.
But, GCD spins up threads to handle remaining work items. This means you can end up with a large number of threads and the system can become over committed. With Swift, the structured concurrency model reduced the number of threads and context switches. Now, each core has only one thread.
Dart has a single-threaded execution model, with support for Isolates
, an event loop, and asynchronous code. An Isolate
is Dart's implementation of a lightweight thread. Unless you spawn an Isolate
, your Dart code runs in the main UI thread driven by an event loop. Flutter's event loop is equivalent to the iOS main loop—in other words, the Looper attached to the main thread.
Dart's single-threaded model doesn't mean you are required to run everything as a blocking operation that causes the UI to freeze. Instead, use the asynchronous features that the Dart language provides, such as async
/await
.
Asynchronous Programming
An asynchronous operation allows other operations to execute before it completes. Both Dart and Swift support asynchronous functions using the async
and await
keywords. In both cases, async
marks that a function performs asynchronous work, and await
tells the system to await a result from function. This means that the Dart VM could suspend the function, if necessary. For more details on asynchronous programming, check out Concurrency in Dart.
Leveraging the main thread/isolate
For Apple operating systems, the primary (also called the main) thread is where the application begins running. Rendering the user interface always happens on the main thread. One difference between Swift and Dart is that
Swift might use different threads for different tasks, and Swift doesn't guarantee which thread is used. So, when dispatching UI updates in Swift, you might need to ensure that the work occurs on the main thread.
Say you want to write a function that fetches the weather asynchronously and displays the results.
In GCD, to manually dispatch a process to the main thread, you might do something like the following.
First, define the Weather
enum
:
swift
enum Weather: String {
case rainy, sunny
}
content_copy
Next, define the view model and mark it as an @Observable
that publishes the result
of type Weather?
. Use GCD to create a background DispatchQueue
to send the work to the pool of threads, and then dispatch back to the main threaad to update the result
.
swift
@Observable class ContentViewModel {
private(set) var result: Weather?
private let queue = DispatchQueue(label: "weather_io_queue")
func load() {
// Mimic 1 second network delay.
queue.asyncAfter(deadline: .now() + 1) { [weak self] in
DispatchQueue.main.async {
self?.result = .sunny
}
}
}
}
content_copy
Finally, display the results:
swift
struct ContentView: View {
@State var viewModel = ContentViewModel()
var body: some View {
Text(viewModel.result?.rawValue ?? "Loading...")
.onAppear {
viewModel.load()
}
}
}
content_copy
More recently, Swift introduced actors to support synchronization for shared, mutable state. To ensure that work is performed on the main thread, define a view model class that is marked as a @MainActor
, with a load()
function that internally calls an asynchronous function using Task
.
swift
@MainActor @Observable class ContentViewModel {
private(set) var result: Weather?
func load() async {
// Mimic 1 second network delay.
try? await Task.sleep(nanoseconds: 1_000_000_000)
self.result = .sunny
}
}
content_copy
Next, define the view model as a state using @State
, with a load()
function that can be called by the view model:
swift
struct ContentView: View {
@State var viewModel = ContentViewModel()
var body: some View {
Text(viewModel.result?.rawValue ?? "Loading...")
.task {
await viewModel.load()
}
}
}
content_copy
In Dart, all work runs on the main isolate by default. To implement the same example in Dart, first, create the Weather
enum
:
dart
enum Weather {
rainy,
windy,
sunny,
}
content_copy
Then, define a simple view model (similar to what was created in SwiftUI), to fetch the weather. In Dart, a Future
object represents a value to be provided in the future. A Future
is similar to Swift's @Observable
. In this example, a function within the view model returns a Future<Weather>
object:
dart
@immutable
class HomePageViewModel {
const HomePageViewModel();
Future<Weather> load() async {
await Future.delayed(const Duration(seconds: 1));
return Weather.sunny;
}
}
content_copy
The load()
function in this example shares similarities with the Swift code. The Dart function is marked as async
because it uses the await
keyword.
Additionally, a Dart function marked as async
automatically returns a Future
. In other words, you don't have to create a Future
instance manually inside functions marked as async
.
For the last step, display the weather value. In Flutter, FutureBuilder
and StreamBuilder
widgets are used to display the results of a Future in the UI. The following example uses a FutureBuilder
:
dart
class HomePage extends StatelessWidget {
const HomePage({super.key});
final HomePageViewModel viewModel = const HomePageViewModel();
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
// Feed a FutureBuilder to your widget tree.
child: FutureBuilder<Weather>(
// Specify the Future that you want to track.
future: viewModel.load(),
builder: (context, snapshot) {
// A snapshot is of type `AsyncSnapshot` and contains the
// state of the Future. By looking if the snapshot contains
// an error or if the data is null, you can decide what to
// show to the user.
if (snapshot.hasData) {
return Center(
child: Text(
snapshot.data.toString(),
),
);
} else {
return const Center(
child: CupertinoActivityIndicator(),
);
}
},
),
);
}
}
content_copy
For the complete example, check out the async_weather file on GitHub.
Leveraging a background thread/isolate
Flutter apps can run on a variety of multi-core hardware, including devices running macOS and iOS. To improve the performance of these applications, you must sometimes run tasks on different cores concurrently. This is especially important to avoid blocking UI rendering with long-running operations.
In Swift, you can leverage GCD to run tasks on global queues with different quality of service class (qos) properties. This indicates the task's priority.
swift
func parse(string: String, completion: @escaping ([String:Any]) -> Void) {
// Mimic 1 sec delay.
DispatchQueue(label: "data_processing_queue", qos: .userInitiated)
.asyncAfter(deadline: .now() + 1) {
let result: [String:Any] = ["foo": 123]
completion(result)
}
}
}
content_copy
In Dart, you can offload computation to a worker isolate, often called a background worker. A common scenario spawns a simple worker isolate and returns the results in a message when the worker exits. As of Dart 2.19, you can use Isolate.run()
to spawn an isolate and run computations:
dart
void main() async {
// Read some data.
final jsonData = await Isolate.run(() => jsonDecode(jsonString) as Map<String, dynamic>);`
// Use that data.
print('Number of JSON keys: ${jsonData.length}');
}
content_copy
In Flutter, you can also use the compute
function to spin up an isolate to run a callback function:
dart
final jsonData = await compute(getNumberOfKeys, jsonString);
content_copy
In this case, the callback function is a top-level function as shown below:
dart
Map<String, dynamic> getNumberOfKeys(String jsonString) {
return jsonDecode(jsonString);
}