Getting started
Dartmole has two parts: the Dartmole app on your Mac, which shows the traffic, and dartmole_plugin in your Flutter app, which sends the traffic there. This page sets up both and gets the first call on screen.
Dartmole gives you insight into your internal builds: debug builds, and the builds your team and testers install. Weighing it against DevTools, Charles or Proxyman? See Why Dartmole.
Not adding the plugin? Dartmole also works with a phone that sends its traffic through it by Wi-Fi proxy: see Native traffic. That way shows native code and the browser, but not your Dart code's calls.
Note
Dartmole is in beta. Until 1.0, an update can change the mock file format or the plugin's API in a way that needs changes on your side. The release notes say when it does.
You need:
- a Mac with Apple silicon, on macOS 12 or later;
- a Flutter app on Flutter 3.29 or later, running on a phone or simulator;
- the phone and the Mac on the same network.
1. Install Dartmole
Download Dartmole, open the DMG, and drag Dartmole to Applications. When you open it, it starts its proxy on port 8080. If something else has that port, Settings → Proxy says what, and lets you pick another.
Dartmole updates itself: it checks for a new version once a day, and Dartmole → Check for Updates… checks right away.
2. Add the plugin
In your app's pubspec.yaml:
dependencies:
dartmole_plugin: ^0.2.0Or run flutter pub add dartmole_plugin.
iOS
Pairing scans a QR code, so the app needs the camera. Add this key to
ios/Runner/Info.plist; without it, iOS closes the app as soon as the scanner
opens:
<key>NSCameraUsageDescription</key>
<string>Scans the QR code in Dartmole to pair with it.</string>iOS also asks for access to the local network, where your Mac is: at the first pairing in a release build, and already at launch in debug and profile builds, because Flutter's tooling uses the local network too. It asks either way, but this key tells your testers why, so they are more likely to tap Allow:
<key>NSLocalNetworkUsageDescription</key>
<string>Connects to Dartmole on this network to inspect the app's traffic.</string>Android
Debug builds can already reach the network. For a release build that should
pair too, such as an internal build for testers, add this to
android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET"/>3. Send your HTTP through it
Dartmole().createHttpClient() returns a dart:io HttpClient. Until the
app is paired and interception is on, it is an ordinary client, so it is safe
to use everywhere. Give it to your HTTP library once: it follows the pairing
from then on, so a client created before pairing goes through Dartmole as soon
as you pair, and goes direct again when you switch interception off.
With Dio:
import 'package:dartmole_plugin/dartmole_plugin.dart';
import 'package:dio/dio.dart';
import 'package:dio/io.dart';
final dio = Dio()
..httpClientAdapter = IOHttpClientAdapter(
createHttpClient: Dartmole().createHttpClient,
);Do the same for every Dio you create, including ones you create again
later.
With package:http:
import 'package:dartmole_plugin/dartmole_plugin.dart';
import 'package:http/http.dart' as http;
import 'package:http/io_client.dart';
final http.Client client = IOClient(Dartmole().createHttpClient());With a plain HttpClient, use Dartmole().createHttpClient() wherever you
would write HttpClient().
This covers what your Dart code sends. Plugins that do their networking in
native code, like flutter_appauth, Firebase or a WebView, bypass Dart's
HttpClient, and so the plugin. Native traffic
explains why, and how to see those calls too.
To pair once rather than on every launch, remember the pairing at startup:
Future<void> main() async {
WidgetsFlutterBinding.ensureInitialized();
await Dartmole().rememberPairing();
runApp(const MyApp());
}It reconnects to the Mac you paired with last time, waiting at most two seconds so your app never hangs on it, and trusts nothing until that Mac answers.
Note
We recommend only using Dartmole in internal builds: the versions you and
your testers install, not the one you publish on the App Store or Google
Play. A separate entry point keeps them apart: main_debug.dart for
internal builds uses Dartmole().createHttpClient() and the pairing panel,
and main.dart for the store never imports Dartmole, so none of its Dart
code ends up in the store version.
4. Add the pairing panel
DartmoleSettings pairs the device, shows which Mac it is paired with, and
switches interception on and off. Put it where testers can reach it and store
users cannot, such as a debug menu:
Scaffold(
appBar: AppBar(title: const Text('My debug menu')),
body: const SingleChildScrollView(
child: DartmoleSettings(),
),
);5. Pair and watch
- In Dartmole, open the Devices tab. It shows a QR code.
- In your app, open the pairing panel and tap Scan QR code. On a simulator, or a device without a camera, type the host and port Dartmole shows instead.
- Use your app. Every call it makes appears on Dartmole's Proxy tab, with its headers and body.
TLS stays on throughout: the plugin trusts Dartmole's certificate authority for its own client only, and never switches certificate checks off.
So there is no certificate to install for this. You only install Dartmole's certificate on the device to see native traffic.
Next: mock a call
Mocks are files in the repository of the app you are debugging, in
.dartmole/mocks/, so they are reviewed and shared like the rest of your
code.
- On the Mocks tab, choose Import project and pick your app's folder.
- On the Proxy tab, select a call and choose Save as mock. Change the response, the status or a delay, and save.
- Switch the mock on. The next matching call gets the mock's response, and the backend never sees it.
Mocks arrive switched off. Whether one is on is remembered on your Mac and never changes the files, so pulling someone's mocks cannot change your traffic by surprise.
Projects and mocks has the rest: what gets captured, which mock wins when several match, and how to share them. The mock file format is the reference for writing one by hand.
When nothing shows up
- Pairing says it cannot reach Dartmole. On iPhone, check Settings →
Privacy & Security → Local Network first: if your app is switched off
there, someone tapped Don't Allow. Then check that the address
matches the one on Dartmole's Devices tab, and open
http://<that address>/certificatein the phone's browser. If that page does not load either, the network is keeping the phone and the Mac apart, even though they are on the same Wi-Fi. A common cause is the Wi-Fi 6E (6 GHz) band: on the Mac, open System Settings → Wi-Fi, choose Details… next to the network, and set Wi-Fi 6E mode to Off. Client isolation or a guest network on the router does the same; turn it off for this network. On Android, a USB cable gets around the network: runadb reverse tcp:50080 tcp:50080andadb reverse tcp:8080 tcp:8080, then pair by typing host127.0.0.1and port50080. - No calls on the Proxy tab. Check that the pairing panel says paired and
interception is on, and that the phone and the Mac are on the same network.
Check too that the call's client came from
Dartmole().createHttpClient(), as in step 3. - The proxy does not start. Settings → Proxy shows its state and, if the port is taken, by what. Pick another port there; paired devices then pair again.
- Calls from native code are missing. The plugin covers Dart's
HttpClient. Calls made withURLSession, OkHttp or in a WebView need the device's proxy set and Dartmole's certificate installed: see Native traffic. The pairing panel's Also intercept native traffic opens the steps on the device.
The dartmole_plugin page on pub.dev has the full reference: remembering a pairing, native traffic, checking that interception works, and testing your app against a paired Dartmole.