Online/offline availability toggle
Optimistic availability switch for couriers or drivers that rolls back on failure and sends periodic heartbeats while online.
FlutterUtilitiesflutterswitchpresenceheartbeat
Code
import 'dart:async';
import 'package:flutter/material.dart';
abstract class AvailabilityApi {
Future<void> setOnline(bool online);
Future<void> heartbeat();
}
class AvailabilityController extends ChangeNotifier {
AvailabilityController(this._api, {this.interval = const Duration(seconds: 30)});
final AvailabilityApi _api;
final Duration interval;
bool _online = false, _pending = false;
String? error;
Timer? _beat;
bool get online => _online;
bool get pending => _pending;
Future<void> toggle(bool next) async {
if (_pending) return;
final previous = _online;
_online = next; // optimistic: flip immediately
_pending = true;
error = null;
notifyListeners();
try {
await _api.setOnline(next);
next ? _startBeat() : _stopBeat();
} catch (_) {
_online = previous; // roll back so the UI tells the truth
error = 'Could not update your status. Try again.';
} finally {
_pending = false;
notifyListeners();
}
}
void _startBeat() {
_beat?.cancel();
_beat = Timer.periodic(interval, (_) => _api.heartbeat().catchError((_) {}));
}
void _stopBeat() {
_beat?.cancel();
_beat = null;
}
@override
void dispose() {
_stopBeat();
super.dispose();
}
}
class AvailabilityTile extends StatelessWidget {
const AvailabilityTile({super.key, required this.controller});
final AvailabilityController controller;
@override
Widget build(BuildContext context) {
return ListenableBuilder(
listenable: controller,
builder: (context, _) => Card(
child: SwitchListTile(
title: Text(controller.online ? 'You are online' : 'You are offline'),
subtitle: Text(controller.error ?? (controller.online ? 'Receiving requests' : 'Go online to receive requests')),
secondary: Icon(Icons.circle, color: controller.online ? Colors.green : Colors.grey, size: 14),
value: controller.online,
onChanged: controller.pending ? null : controller.toggle,
),
),
);
}
}
How it works
What it solves
People who deliver or drive switch availability many times a shift, often on poor mobile data. The switch must respond instantly, but it must never show "online" if the server never recorded it, otherwise requests are silently missed.
How it works
toggle flips the local value first for instant feedback, marks the call pending (which disables the switch to stop double taps) and awaits the API. If the call throws, the previous value is restored and an error message is exposed to the view.
While online, a periodic timer sends a lightweight heartbeat so the backend can expire sessions that vanish, for example when the app is killed. The timer starts only after the server confirms, and is cancelled on offline and on dispose. The tile uses ListenableBuilder, so only the card rebuilds.
Adapting it
Implement AvailabilityApi with your HTTP client. Add location sharing inside _startBeat if the heartbeat should carry coordinates. Persist the last state in local storage and re-send it after login if the user expects to stay online.
Pitfalls
- Timers pause or die when the OS suspends the app; use a foreground service on Android for guaranteed delivery.
- Heartbeat failures are swallowed here; surface repeated failures if connectivity matters.
- Keep the interval below the server-side expiry window.
How to use it
Copy the file into lib/availability/, implement AvailabilityApi, and create one controller for the signed-in user.
final c = AvailabilityController(MyAvailabilityApi());
AvailabilityTile(controller: c)
Call c.dispose() on logout.
Licence: DEV3 Snippet Licence. Free to use, change and ship in personal and commercial projects. Attribution is appreciated, not required. Please do not redistribute snippets as a collection.