Dartmole

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:

YAML
dependencies:
  dartmole_plugin: ^0.2.0

Or 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:

XML
<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:

XML
<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:

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:

Dart
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:

Dart
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:

Dart
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:

Dart
Scaffold(
  appBar: AppBar(title: const Text('My debug menu')),
  body: const SingleChildScrollView(
    child: DartmoleSettings(),
  ),
);

5. Pair and watch

  1. In Dartmole, open the Devices tab. It shows a QR code.
  2. 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.
  3. 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.

  1. On the Mocks tab, choose Import project and pick your app's folder.
  2. On the Proxy tab, select a call and choose Save as mock. Change the response, the status or a delay, and save.
  3. 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>/certificate in 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: run adb reverse tcp:50080 tcp:50080 and adb reverse tcp:8080 tcp:8080, then pair by typing host 127.0.0.1 and port 50080.
  • 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 with URLSession, 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.