flutter-concurrency
Skill12 starsChanged 56 days agoMITView on GitHub
Use when dealing with asynchrony in Flutter - async/await inside a Cubit, Streams and RxDart, Isolates for heavy work, cancelling an in-flight operation, or choosing between FutureBuilder and BlocBuilder. Triggers on UI freezes, jank, race conditions, or "setState after dispose".
1 other project uses a version of this.
Adopted by
Most recently taken up by
- gspencergoog/skillstheir own version, first seen Oct 16, 2026
Other versions1 project uses a version of this.
Word for word · 0
None found.
Edited · 0
None found.
Wrote their own version · 1
- gspencergoog/skills★ 2same name
How matches are found. Word for word: the exact same text. Edited: shares passages with this file (“99% same” means 99% of its five-word phrases also appear here; 10% or more counts). Their own version: similar meaning, or, weakest, the same name. We can’t tell which project had it first. Projects are counted among the 200 most-starred candidates, and mentions are links to the repository we found, up to 50 per site, so both counts are a floor.
In this repository15 other files it gives its agents.
- flutter-accessibilitySkill
- flutter-cachingSkill
- flutter-clean-archSkill
- flutter-dart-styleSkill
- flutter-design-systemSkill
- flutter-diSkill
- flutter-formsSkill
- flutter-monorepoSkill
- flutter-new-moduleSkill
- flutter-runSkill
- flutter-stateSkill
- flutter-store-cdSkill
- flutter-testingSkill
- flutter-themingSkill
- flutter-webSkill
The file
We split this skill into the rules it gives an agent
10 rules. About 0 are common, 2 are distinctive, and 8 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈2
Distinctive · estimate
This author's own choices. Worth a look.
8
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 19
Rule of thumb: if it does NOT block the main isolate for more than one frame (~16 ms), use async/await.
- Very specific: line 50
Always emit Loading before the async call
- Distinctive: line 52
Never emit after the Cubit is closed (see Pitfalls section)
- Very specific: line 96
Always dispose BehaviorSubject and cancel StreamSubscription in close()
- Very specific: line 97
Use debounceTime for user input to avoid excessive API calls
- Very specific: line 98
Use distinct() to skip duplicate emissions
- Very specific: line 99
Use BehaviorSubject (not PublishSubject) when you need the latest value on subscription
- Distinctive: line 149
All arguments must be sendable across isolate boundaries (primitives, Lists, Maps, typed data classes — no Cubits, no Dio instances)
- Very specific: line 233
Always provide a way to stop the isolate (kill() + close the port)
- Very specific: line 316
Rule: Every subscription, subject, and cancel token created in a controller must be cleaned up in close().
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
You are implementing concurrency for a Flutter feature. Choose the pattern based on the skill argument, if one was given, or on the context of the operation.
Decision Framework
Pick the right tool for the workload:
| Workload | Pattern | Examples |
| I/O (network, storage) | async/await | Dio API calls, SharedPreferences, Hive reads |
| CPU < 16 ms | async/await | Small list transforms, formatting |
| One-time heavy CPU | Isolate.run() | PDF receipt generation, large JSON parsing |
| Continuous background | Isolate.spawn() with ports | Payment status polling, data sync |
Rule of thumb: if it does NOT block the main isolate for more than one frame (~16 ms), use async/await. Otherwise, offload to an isolate.
Pattern 1: Async in Cubits (default)
Best for: API calls, storage reads, form submissions.
Controllers follow the Loading → Success/Failure emission pattern:
class OrderController extends Cubit<OrderStatus> {
final OrderRepository _repository;
OrderController(this._repository) : super(const OrderInitial());
Future<void> loadOrders() async {
emit(const OrderLoading());
try {
final orders = await _repository.getOrders();
emit(OrderSuccess(orders));
} on DioException catch (e) {
emit(OrderFailure(e.message ?? 'Erro ao carregar pedidos'));
} catch (e) {
emit(OrderFailure(e.toString()));
}
}
}Rules:
- Always emit
Loadingbefore the async call - Catch
DioExceptionseparately for HTTP errors - Never emit after the Cubit is closed (see Pitfalls section)
Pattern 2: Stream Management with RxDart
Best for: real-time data, debounced search, reactive pipelines.
class SearchController extends Cubit<SearchStatus> {
final SearchRepository _repository;
final _querySubject = BehaviorSubject<String>.seeded('');
StreamSubscription<String>? _querySub;
SearchController(this._repository) : super(const SearchInitial()) {
_querySub = _querySubject
.debounceTime(const Duration(milliseconds: 400))
.where((q) => q.length >= 2)
.distinct()
.listen(_performSearch);
}
void onQueryChanged(String query) => _querySubject.add(query);
Future<void> _performSearch(String query) async {
emit(const SearchLoading());
try {
final results = await _repository.search(query);
emit(SearchSuccess(results));
} catch (e) {
emit(SearchFailure(e.toString()));
}
}
@override
Future<void> close() {
_querySub?.cancel();
_querySubject.close();
return super.close();
}
}Rules:
- Always dispose
BehaviorSubjectand cancelStreamSubscriptioninclose() - Use
debounceTimefor user input to avoid excessive API calls - Use
distinct()to skip duplicate emissions - Use
BehaviorSubject(notPublishSubject) when you need the latest value on subscription
Pattern 3: Isolate.run() for One-Time Heavy Work
Best for: PDF receipt generation, parsing large JSON payloads, image processing.
PDF Receipt Generation
class ReceiptController extends Cubit<ReceiptStatus> {
ReceiptController() : super(const ReceiptInitial());
Future<void> generateReceipt(Order order) async {
emit(const ReceiptLoading());
try {
// Offload PDF generation to a background isolate
final pdfBytes = await Isolate.run(() => _buildReceiptPdf(order));
emit(ReceiptSuccess(pdfBytes));
} catch (e) {
emit(ReceiptFailure('Erro ao gerar comprovante: $e'));
}
}
}
// Top-level or static function (required for Isolate.run)
Uint8List _buildReceiptPdf(Order order) {
final pdf = pw.Document();
pdf.addPage(
pw.Page(
pageFormat: PdfPageFormat(80 * PdfPageFormat.mm, double.infinity),
build: (context) => pw.Column(
crossAxisAlignment: pw.CrossAxisAlignment.start,
children: [
pw.Text('Pedido #${order.id}', style: pw.TextStyle(fontSize: 14, fontWeight: pw.FontWeight.bold)),
pw.SizedBox(height: 4),
...order.items.map((item) => pw.Text('${item.quantity}x ${item.name} - R\$ ${item.total.toStringAsFixed(2)}')),
pw.Divider(),
pw.Text('Total: R\$ ${order.total.toStringAsFixed(2)}', style: pw.TextStyle(fontWeight: pw.FontWeight.bold)),
],
),
),
);
return pdf.save();
}Rules:
- The function passed to
Isolate.run()must be top-level or static (no closures over instance state) - All arguments must be sendable across isolate boundaries (primitives, Lists, Maps, typed data classes — no Cubits, no Dio instances)
Isolate.run()handles spawning and killing the isolate automatically
Large Data Processing
Future<void> processReport(List<Map<String, dynamic>> rawData) async {
emit(const ReportLoading());
try {
final processed = await Isolate.run(() => _aggregateReportData(rawData));
emit(ReportSuccess(processed));
} catch (e) {
emit(ReportFailure('Erro ao processar relatório: $e'));
}
}
// Top-level function
ReportData _aggregateReportData(List<Map<String, dynamic>> rawData) {
// Heavy aggregation, grouping, sorting — runs off the main isolate
final grouped = <String, double>{};
for (final entry in rawData) {
final category = entry['category'] as String;
final value = (entry['value'] as num).toDouble();
grouped[category] = (grouped[category] ?? 0) + value;
}
return ReportData(grouped);
}Pattern 4: Isolate.spawn() for Continuous Background Work
Best for: payment status polling, background data sync.
class PaymentPollingService {
Isolate? _isolate;
ReceivePort? _receivePort;
StreamSubscription? _portSub;
Future<void> startPolling(String transactionId) async {
_receivePort = ReceivePort();
_isolate = await Isolate.spawn(
_pollPaymentStatus,
_PollingArgs(transactionId, _receivePort!.sendPort),
);
_portSub = _receivePort!.listen((message) {
if (message is PaymentStatusUpdate) {
// Forward to EventBus/Broker or Cubit
Broker.instance.fire(PaymentStatusEvent(message));
}
});
}
void stopPolling() {
_portSub?.cancel();
_receivePort?.close();
_isolate?.kill(priority: Isolate.immediate);
_isolate = null;
}
}
// Top-level function
void _pollPaymentStatus(_PollingArgs args) async {
while (true) {
await Future.delayed(const Duration(seconds: 3));
// Simulate checking payment status (no Dio here — use dart:io HttpClient or pass data via ports)
final status = await _checkStatus(args.transactionId);
args.sendPort.send(PaymentStatusUpdate(status));
if (status == 'approved' || status == 'declined') break;
}
}
class _PollingArgs {
final String transactionId;
final SendPort sendPort;
_PollingArgs(this.transactionId, this.sendPort);
}Rules:
- Always provide a way to stop the isolate (
kill()+ close the port) - The spawned function cannot use objects from the main isolate (no Dio, no Cubit, no repos)
- Communicate via
SendPort/ReceivePortonly with sendable types - Clean up in the service's
dispose()or controller'sclose()
FutureBuilder vs BlocBuilder
With the Cubit pattern, prefer BlocBuilder over FutureBuilder.
FutureBuilder | BlocBuilder | |
| Use when | Quick prototype, one-off Future with no state | The default for anything with state |
| Re-triggers on rebuild | Yes (unless Future is cached) | No (reacts to state only) |
| Testable | Hard | Easy (mock Cubit, emit states) |
| Error handling | In builder snapshot.hasError | In state class (FailureStatus) |
// PREFERRED
BlocBuilder<OrderController, OrderStatus>(
builder: (context, state) {
if (state is OrderLoading) return const CircularProgressIndicator();
if (state is OrderSuccess) return OrderListView(orders: state.orders);
if (state is OrderFailure) return ErrorWidget(message: state.message);
return const SizedBox.shrink();
},
)
// AVOID unless truly one-off and outside of a Cubit-managed feature
FutureBuilder<List<Order>>(
future: _future, // must be cached in initState, never inline
builder: (context, snapshot) { ... },
)Cancellation
HTTP Requests with Dio CancelToken
class ProductController extends Cubit<ProductStatus> {
final ProductRepository _repository;
CancelToken? _cancelToken;
ProductController(this._repository) : super(const ProductInitial());
Future<void> loadProducts() async {
_cancelToken?.cancel('New request started');
_cancelToken = CancelToken();
emit(const ProductLoading());
try {
final products = await _repository.getProducts(cancelToken: _cancelToken!);
emit(ProductSuccess(products));
} on DioException catch (e) {
if (e.type == DioExceptionType.cancel) return; // silently ignore
emit(ProductFailure(e.message ?? 'Erro'));
}
}
@override
Future<void> close() {
_cancelToken?.cancel('Controller closed');
return super.close();
}
}Stream Subscriptions
@override
Future<void> close() {
_subscription?.cancel(); // StreamSubscription
_subject.close(); // BehaviorSubject
_cancelToken?.cancel(); // Dio CancelToken
return super.close();
}Rule: Every subscription, subject, and cancel token created in a controller must be cleaned up in close().
Common Pitfalls
1. Emit after close
// BAD — can throw StateError if Cubit was closed during the await
Future<void> load() async {
emit(const Loading());
final data = await _repo.fetch();
emit(Success(data)); // 💥 if navigated away
}
// GOOD — guard before emitting
Future<void> load() async {
emit(const Loading());
final data = await _repo.fetch();
if (isClosed) return;
emit(Success(data));
}2. Uncancelled subscriptions
// BAD — leaks the subscription
class MyController extends Cubit<MyStatus> {
MyController(Stream<int> stream) : super(const MyInitial()) {
stream.listen((val) => emit(MyUpdated(val))); // never cancelled
}
}
// GOOD
class MyController extends Cubit<MyStatus> {
late final StreamSubscription<int> _sub;
MyController(Stream<int> stream) : super(const MyInitial()) {
_sub = stream.listen((val) => emit(MyUpdated(val)));
}
@override
Future<void> close() { _sub.cancel(); return super.close(); }
}3. Blocking the main isolate
// BAD — 200ms of JSON parsing freezes the UI
final data = jsonDecode(hugeJsonString) as List;
// GOOD — offload to isolate
final data = await Isolate.run(() => jsonDecode(hugeJsonString) as List);4. Passing non-sendable objects to isolates
// BAD — Dio is not sendable
await Isolate.run(() => dio.get('/endpoint')); // 💥 runtime error
// GOOD — fetch on main, process on isolate
final response = await dio.get('/endpoint');
final processed = await Isolate.run(() => _heavyParse(response.data));5. FutureBuilder with inline Future
// BAD — re-creates the Future on every build
Widget build(BuildContext context) {
return FutureBuilder(
future: repository.load(), // called every rebuild!
builder: (ctx, snap) => ...,
);
}
// GOOD — cache the Future
late final _future = repository.load();
Widget build(BuildContext context) {
return FutureBuilder(future: _future, builder: (ctx, snap) => ...);
}Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

