Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions example/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,35 @@ The demo includes practical examples:
## Documentation

For detailed guides and real-world use cases, visit: **[docs.page/fluttercommunity/flutter_workmanager →](https://docs.page/fluttercommunity/flutter_workmanager)**

## Web Demo (experimental)

Run `flutter run -d chrome` (or `flutter build web` and serve over HTTPS or
localhost) to get the web-only demo. It demonstrates the experimental
`workmanager_web` package:

- **Background tasks in a Web Worker** — register one-off / periodic tasks
and watch them execute off the main thread (the UI stays responsive while
the task's CPU loop runs).
- **Worker chat** — a two-way `postMessage` conversation between the page and
the background worker (suggestions: watch a simulated BTC/ETH price, stop
the watch, or run an on-demand background check). Replies arrive on
`WorkmanagerWeb.workerMessages`; the same pattern applies to real data.
- **Service Worker execution** — install the PWA, trigger Periodic Background
Sync from DevTools, close the page, trigger it again and reopen: the task
ran inside the Service Worker (compiled Dart dispatcher) and the result is
replayed from IndexedDB into the event log.

The background handler lives in `lib/web/background_tasks.dart` — a
Flutter-free file compiled with plain `dart compile js` into
`web/background.dart.js` (see `tool/build_web_background.sh`). Prices in the
demo are simulated so it works offline; swap `_simulatedPrice()` for a real
fetch to see the same pattern with live data.

## Key Files

- `lib/main.dart` - Main app with task scheduling UI
- `lib/web/` - Web-only demo (Flutter-free dispatcher, worker chat, PWA install glue)
- `lib/callback_dispatcher.dart` - Background task execution logic
- `ios/Runner/AppDelegate.swift` - iOS background task registration
- `ios/Runner/Info.plist` - iOS background modes configuration
149 changes: 144 additions & 5 deletions example/lib/web/background_tasks.dart
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
// `web/background.dart.js` and executed both by the in-page Web Worker and by
// the Service Worker, neither of which can run the Flutter engine.

import 'dart:async';

import 'package:workmanager_web/execution.dart';

/// Dispatcher used on the web: wired into the compiled worker bundle
Expand All @@ -15,24 +17,161 @@ import 'package:workmanager_web/execution.dart';
@pragma('vm:entry-point')
void webCallbackDispatcher() {
WorkmanagerExecution.instance.executeTask(handleWebBackgroundTask);
WorkmanagerExecution.instance.messageHandler = handleWorkerMessage;
}

// ---------------------------------------------------------------------------
// Use case: a tiny "price watch".
//
// The demo simulates a market feed so it stays self-contained (no network, no
// API key). The same shape applies to any real background work:
//
// * the page sends a message to the worker -> messageHandler runs in the
// Web Worker (off the main thread),
// * the worker pushes updates back -> sendToPage surfaces them on
// `WorkmanagerWeb.workerMessages`,
// * background tasks (also while the page is closed, via the Service Worker)
// run the same handler and their results are replayed into the event log.
// ---------------------------------------------------------------------------

/// Base prices per ticker, in USD. Simulated.
const Map<String, double> _basePrices = <String, double>{
'btc': 60000,
'eth': 2500,
'ada': 0.60,
};

Timer? _watchTimer;

/// Handler for free-form messages sent by the page with
/// `WorkmanagerWeb().sendMessageToWorker(...)`.
///
/// Messages:
/// * `{'op': 'watch', 'ticker': 'btc', 'threshold': 58000}` — start pushing
/// simulated prices every few seconds; stops itself when the price drops
/// below [threshold].
/// * `{'op': 'stop'}` — stop the current watch.
void handleWorkerMessage(Object? payload) {
if (payload is! Map) {
return;
}
final op = payload['op'];
switch (op) {
case 'watch':
final ticker = (payload['ticker'] as String?)?.toLowerCase() ?? 'btc';
final threshold = (payload['threshold'] as num?)?.toDouble();
_watchTimer?.cancel();
_post(<String, Object?>{
'kind': 'watching',
'ticker': ticker,
'threshold': threshold,
});
_postTick(ticker, threshold);
_watchTimer = Timer.periodic(
const Duration(seconds: 3),
(_) => _postTick(ticker, threshold),
);
case 'stop':
_watchTimer?.cancel();
_watchTimer = null;
_post(<String, Object?>{'kind': 'stopped'});
case 'check':
// One-off background check on demand (same logic as the task path).
final ticker = (payload['ticker'] as String?)?.toLowerCase() ?? 'btc';
final threshold = (payload['threshold'] as num?)?.toDouble();
_post(<String, Object?>{'kind': 'task-start', 'ticker': ticker});
final price = _simulatedPrice(ticker);
final below = threshold != null && price < threshold;
_post(<String, Object?>{
'kind': 'task-done',
'ticker': ticker,
'price': price,
'below': below,
});
case 'text':
_post(<String, Object?>{'kind': 'echo', 'text': payload['text']});
}
}

void _postTick(String ticker, double? threshold) {
final price = _simulatedPrice(ticker);
final below = threshold != null && price < threshold;
_post(<String, Object?>{
'kind': below ? 'alert' : 'tick',
'ticker': ticker,
'price': price,
'threshold': threshold,
});
if (below) {
_watchTimer?.cancel();
_watchTimer = null;
}
}

/// Sends a free-form message back to the page (if a page is reachable).
void _post(Object? payload) {
WorkmanagerExecution.instance.sendToPage?.call(payload);
}

/// Deterministic, time-varying simulated price: stable within a 30s bucket so
/// consecutive ticks change, but the demo never needs the network.
double _simulatedPrice(String ticker) {
final base = _basePrices[ticker] ?? 100.0;
final bucket = DateTime.now().millisecondsSinceEpoch ~/ 30000;
final hash = _hash('$ticker:$bucket');
final wiggle = (hash % 1000) / 1000 * 0.10 - 0.05; // ±5%
return base * (1 + wiggle);
}

int _hash(String input) {
var hash = 0;
for (final codeUnit in input.codeUnits) {
hash = (hash * 31 + codeUnit) & 0x7fffffff;
}
return hash;
}

/// Pure-Dart background task handler.
///
/// The result is recorded by the runtime: when the page is open it appears in
/// the status panel immediately; when the Service Worker ran the task while
/// the page was closed, it is replayed on the next page load.
/// With `inputData['ticker']` it behaves like a background "price check":
/// it pushes progress messages to the page while running and returns the
/// price + alert state as the task result. The result is recorded by the
/// runtime: when the page is open it appears in the status panel immediately;
/// when the Service Worker ran the task while the page was closed, it is
/// replayed on the next page load.
Future<bool> handleWebBackgroundTask(
String taskName,
Map<String, dynamic>? inputData,
) async {
final input = inputData;
if (input != null && input['fail'] == true) {
return false;
}
final ticker = (input?['ticker'] as String?)?.toLowerCase() ?? 'btc';
final threshold = (input?['threshold'] as num?)?.toDouble();

// A small CPU loop so the Web Worker's parallel execution is observable:
// the UI stays responsive while this runs off the main thread.
// ignore: unused_local_variable
var checksum = 0;
for (var i = 0; i < 2000000; i++) {
checksum += i;
}
final input = inputData;
return input != null && input['fail'] == true ? false : true;

_post(<String, Object?>{
'kind': 'task-start',
'ticker': ticker,
'threshold': threshold,
});
final price = _simulatedPrice(ticker);
final below = threshold != null && price < threshold;
_post(<String, Object?>{
'kind': 'task-done',
'ticker': ticker,
'price': price,
'below': below,
});
// The task result itself stays a plain success/failure bool; the price
// detail is delivered via the chat messages above.
return true;
}
Loading
Loading