flutter_network_throttler
Simulate slow, unreliable, or failing network conditions in your Flutter app so you can test loading spinners, timeouts, retries, and error states β without depending on a real flaky server.
Route your real HTTP traffic through a throttling adapter, then tune everything live from a drop-in debug control panel.
Features
- π’ Conditions β latency, jitter, bandwidth cap, and packet loss.
- π± Presets β
Offline,2G,3G,4G,WiFi, plus saved custom presets. - π₯ Failure injection β fail a configurable fraction of requests with a
timeout,500,403, orno-connection. - π― Per-endpoint rules β glob-match a method + path and slow it, fail it, or let it pass through untouched β edited from a built-in rule editor.
- π‘ Live request log β captured with outcome, timing, metrics, filters, pause/clear, and tap-to-inspect.
- π Real traffic β a
package:httpclient wrapper, apackage:diointerceptor, aWebSocketChannelwrapper, and a genericStreamtransformer share one engine. - ποΈ Control panel β
NetworkThrottlerPanel, a debug UI to drive it all. - πΎ Persistence β save/restore configuration via any store you plug in.
- π¬ Scenario scripting β script timed conditions ("offline for 5s, then 3G").
- π οΈ DevTools β drive it from Flutter DevTools via service extensions.
- π¦ Toggleable & deterministic β flip
enabledat runtime; pass aseedfor repeatable tests.
Getting started
dependencies:
flutter_network_throttler: ^1.0.0
Everything is driven by a single ThrottleController:
import 'package:flutter_network_throttler/flutter_network_throttler.dart';
final controller = ThrottleController();
Throttle real HTTP traffic
package:http
import 'package:http/http.dart' as http;
final client = ThrottleClient(http.Client(), controller: controller);
// Now behaves according to the controller's profile, and is logged.
final response = await client.get(Uri.parse('https://api.example.com/v1/feed'));
package:dio
import 'package:dio/dio.dart';
import 'package:flutter_network_throttler/dio.dart';
final dio = Dio()..interceptors.add(ThrottleInterceptor(controller));
WebSockets
Wrap a WebSocketChannel to throttle the handshake and every frame. The same
conditions apply: latency/jitter delay each frame, the bandwidth cap slows large
frames, packet loss drops individual frames, and failure injection (or an
offline condition) fails the connection. Frames show up in the same live log,
tagged WS, WSβ (sent), and WSβ (received).
import 'package:flutter_network_throttler/web_socket.dart';
// One-liner: connect and throttle in a single call.
final socket = ThrottleWebSocketChannel.connect(
Uri.parse('wss://example.com/socket'),
controller: controller,
);
socket.stream.listen(handleMessage);
socket.sink.add('ping');
Already have a channel? Wrap it instead:
import 'package:web_socket_channel/web_socket_channel.dart';
final raw = WebSocketChannel.connect(Uri.parse('wss://example.com/socket'));
final socket = ThrottleWebSocketChannel(raw, controller: controller);
The control panel
NetworkThrottlerPanel is just the content (no app bar) so you can present
it however you like. Three ready-made ways:
// 1. Bottom sheet β dismiss by dragging the handle down or tapping the scrim.
showNetworkThrottlerPanel(context, controller);
// 2. Full-screen page β gets a standard back button to pop it.
showNetworkThrottlerPage(context, controller);
// (or push NetworkThrottlerPage(controller: controller) yourself)
// 3. Embedded β drop the panel into any layout (tab, drawer, split view).
Expanded(child: NetworkThrottlerPanel(controller: controller));
Pushing the bare
NetworkThrottlerPanelas a route won't give you a back button β it has noScaffold/app bar by design. UseNetworkThrottlerPage(or wrap the panel in your ownScaffold+AppBar) for a page with a back button.
The NetworkThrottlerButton launcher can open either form:
NetworkThrottlerButton(
controller: controller,
presentation: ThrottlerPresentation.page, // default is .sheet
)
The panel exposes the master switch, preset chips, condition sliders, failure injection, per-endpoint rules, and the live request log β all bound to the same controller your client reads from.
How users open it (debug-only)
You don't ship the panel to real users β you expose a trigger that only exists
in debug builds. NetworkThrottlerButton does exactly that: it renders only
when kDebugMode is true and opens the panel as a modal sheet.
Floating button over any screen β drop it into a Scaffold:
Scaffold(
floatingActionButton: NetworkThrottlerButton(controller: controller),
body: MyHomePage(),
)
A row in your Settings screen β pass your own trigger as child:
NetworkThrottlerButton(
controller: controller,
child: const ListTile(
leading: Icon(Icons.network_check),
title: Text('Network throttler'),
subtitle: Text('Simulate slow / failing network'),
trailing: Icon(Icons.chevron_right),
),
)
Or wire it yourself β gate any widget with kDebugMode and call the helper:
if (kDebugMode)
IconButton(
icon: const Icon(Icons.network_check),
onPressed: () => showNetworkThrottlerPanel(context, controller),
),
In release builds NetworkThrottlerButton returns an empty widget, so there's
nothing to strip out.
Enabling it in release builds (for testers)
Sometimes you need the throttler in a release/profile build β QA on a real
device, a TestFlight/internal track, reproducing a field issue. Set
showInReleaseMode: true to allow it.
To keep it out of production while still letting testers flip it on, gate that
flag behind a build-time --dart-define, so only builds compiled with the flag
expose it:
// Only true when the build was compiled with the flag below.
const kThrottlerEnabled = bool.fromEnvironment('ENABLE_NET_THROTTLER');
NetworkThrottlerButton(
controller: controller,
showInReleaseMode: kThrottlerEnabled,
)
Build a tester version with it on, and a normal store build with it off:
# Testers get the throttler:
flutter build apk --release --dart-define=ENABLE_NET_THROTTLER=true
# Production build β flag absent, button never renders:
flutter build apk --release
In debug builds the button always shows regardless of the flag, so day-to-day development is unaffected.
Configure in code
// Apply a presetβ¦
controller.applyPreset(NetworkCondition.threeG);
// β¦or tune individual conditions.
controller
..setLatency(const Duration(milliseconds: 200))
..setPacketLoss(0.1); // 10%
// Inject failures.
controller
..toggleFailure()
..setFailureType(FailureType.http500)
..setFailureProbability(0.25);
// Per-endpoint rules (first match wins).
controller.addRule(const EndpointRule(
method: 'GET',
pattern: '/v1/feed',
action: DelayAction(Duration(milliseconds: 800)),
));
controller.addRule(const EndpointRule(
pattern: '*.cdn.img/*',
action: PassThroughAction(),
));
Generic streams (gRPC, SSE, event buses)
Throttle any Stream with ThrottleStreamTransformer β latency delays each
event, the bandwidth cap slows large events, packet loss drops events, and an
injected connection failure errors the stream:
final throttled = source.transform(
ThrottleStreamTransformer<MyEvent>(
controller,
label: 'grpc:Updates',
byteSizeOf: (e) => e.estimatedBytes,
),
);
Persistence
The package depends on no storage plugin β implement ThrottleStorage (or use
CallbackThrottleStorage) against shared_preferences, hive, a file, etc.,
and the controller restores on startup and saves on every change:
final controller = ThrottleController(
storage: CallbackThrottleStorage(
read: () async => prefs.getString('throttler'),
write: (data) => prefs.setString('throttler', data),
),
);
await controller.loaded; // optional: wait for restore before building UI
Every model is JSON-serialisable (toJson / fromJson) if you prefer to manage
state yourself.
Scenario scripting
Script a timeline of conditions to reproduce flaky-network bugs or drive integration tests deterministically:
final scenario = ThrottleScenario.offlineFor(
const Duration(seconds: 5),
recover: NetworkCondition.threeG,
)..start(controller);
// β¦or build your own steps:
ThrottleScenario([
ScenarioStep(Duration.zero, (c) => c.applyPreset(NetworkCondition.twoG)),
ScenarioStep(const Duration(seconds: 3),
(c) => c.setPacketLoss(0.5)),
], loop: true).start(controller);
DevTools
Drive the throttler from Flutter DevTools (or any VM-service client) without on-screen UI β registers no-op in release builds:
registerThrottleServiceExtensions(controller);
// ext.flutter_network_throttler.enable / preset / failure / clearLog / state
Without HTTP
Need to throttle an arbitrary Future (a repository call, a mock data source)?
Use the standalone NetworkThrottler:
final throttler = NetworkThrottler(condition: NetworkCondition.threeG, seed: 42);
final value = await throttler.throttle(
() => repository.fetchProfile(),
responseBytes: 20 * 1024,
);
It throws SimulatedNetworkException when a request is dropped.
See the example/ app for the panel wired to a demo client.
Additional information
- Issues & feature requests: the issue tracker.
- Contributing: see CONTRIBUTING.md.
- License: MIT.
Libraries
- dio
- Dio adapter for
flutter_network_throttler. - flutter_network_throttler
- Simulate slow, unreliable, or failing network conditions in Flutter apps.
- web_socket
- WebSocket adapter for
flutter_network_throttler.