fourb_mdns 0.5.0
fourb_mdns: ^0.5.0 copied to clipboard
Let people pick a server off the local network from inside your Flutter app, and remember which one they chose. Scans mDNS/Bonjour natively — no server.
fourb_mdns #
Let people pick a server off the local network from inside your Flutter app, and remember which one they chose.
Unlike a browser, a Flutter app can speak mDNS itself — so there is no server to run. Discovery uses two backends at once, because each covers the other's blind spot:
nsddrives the platform's own stack (AndroidNsdManager, AppleNetService). It resolves reliably and satisfies the iOS/macOS Local Network permission the way the OS expects — but every platform API demands a concrete service type, so it only finds what you ask it for.multicast_dnsspeaks the protocol directly, which lets it run the wildcard sweep that discovers types nobody declared.
Both run concurrently and merge by fully-qualified name. On one real network that was 5 services
from nsd, 50 from the wildcard sweep, and 64 merged.
The companion package for React apps is
4b-react-mdns; the two share the same storage keys
and record shape.
Quick start #
dependencies:
fourb_mdns: ^0.1.0
Then wire it up:
dart run fourb_mdns:config --http-only
+ entry point (lib/main.dart) — wrapped runApp in MdnsScope
+ iOS plist (ios/Runner/Info.plist) — added NSLocalNetworkUsageDescription + NSBonjourServices
+ macOS plist (macos/Runner/Info.plist) — added NSLocalNetworkUsageDescription + NSBonjourServices
+ macOS debug entitlements — added network client + server
+ macOS release entitlements — added network client + server
+ Android manifest — added multicast + network permissions
That wraps runApp() in MdnsScope and writes every platform permission mDNS needs — the
part that otherwise makes a scan silently find nothing. Platforms your project doesn't have are
skipped, and anything already done reports =, so it is safe to re-run.
| Flag | |
|---|---|
--http-only |
Write httpOnly: true — only _http._tcp / _https._tcp |
--no-launcher |
Write launcher: false — open the picker from your own button |
--dry-run |
Show the plan without writing |
Full restart afterwards (not hot reload — the permission changes are native). A Scan network pill appears bottom-right. Tap it, pick a server, done.
Wiring it by hand instead
import 'package:fourb_mdns/fourb_mdns.dart';
void main() => runApp(
MdnsScope(
httpOnly: true,
child: MaterialApp(home: HomePage()),
),
);
Plus the platform permissions below.
Reading the pick #
Anywhere below the scope:
final mdns = MdnsScope.of(context);
if (mdns.address == null) {
return TextButton(
onPressed: () => showMdnsPicker(context),
child: const Text('Choose a server'),
);
}
return Text('Server at ${mdns.address}');
Outside the widget tree — in an API client with no BuildContext:
final address = await MdnsStore.savedAddress(); // "192.168.0.224:3000"
final base = Uri.parse('http://$address');
Stored in SharedPreferences under the same keys the React package uses:
| Key | |
|---|---|
4b_mDNS_ip |
the plain ip:port string |
4b_mDNS_selection |
the full record as JSON |
Platform setup #
dart run fourb_mdns:config writes all of this for you. Here is what it adds, if you would rather
do it by hand or want to check its work.
mDNS needs explicit permission on Apple platforms, and multicast on Android. Skip these and the scan just finds nothing — there is no error dialog.
iOS #
ios/Runner/Info.plist:
<key>NSLocalNetworkUsageDescription</key>
<string>Finds servers on your network so you can pick one.</string>
<key>NSBonjourServices</key>
<array>
<string>_services._dns-sd._udp</string>
<string>_http._tcp</string>
<string>_https._tcp</string>
</array>
NSBonjourServices is an allow-list: iOS only returns types you declare. _services._dns-sd._udp
is what enumerates the rest, so keep it and add any other type you care about.
macOS #
Both macos/Runner/DebugProfile.entitlements and Release.entitlements:
<key>com.apple.security.network.client</key>
<true/>
<key>com.apple.security.network.server</key>
<true/>
macOS 15+ also wants the same NSLocalNetworkUsageDescription and NSBonjourServices keys in
macos/Runner/Info.plist.
Android #
android/app/src/main/AndroidManifest.xml:
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<uses-permission android:name="android.permission.CHANGE_WIFI_MULTICAST_STATE"/>
Flutter Web #
Not supported — browsers can't speak mDNS. Use
4b-react-mdns, which runs discovery in Node and
serves it over HTTP.
MdnsScope #
| Parameter | Default | |
|---|---|---|
child |
— | Your app |
httpOnly |
false |
Show only _http._tcp / _https._tcp |
filterToggle |
false when httpOnly is set |
Show the HTTP-only switch in the picker |
launcher |
true |
Show the built-in floating button |
launcherLabel |
Scan network |
Its text |
alignment |
Alignment.bottomRight |
Where it sits |
scanOnStart |
true |
Sweep once on mount |
timeout |
6s |
How long each query phase listens |
useNsd |
true |
Use the platform's native discovery |
useMulticastDns |
true |
Run the raw wildcard sweep |
serviceTypes |
_http._tcp, _https._tcp |
Types the native backend browses |
onSelect |
— | Called when the pick changes |
Passing httpOnly hides the picker's own switch: the app has decided, so a control that
contradicts it only confuses. Pass filterToggle: true to bring it back.
MdnsScope.of(context) #
Returns the [MdnsController] and rebuilds the caller when the scan or the pick changes. Use
MdnsScope.read(context) in callbacks that only need to act.
selection // MdnsService? the pick
address, ip, url
services // filtered + sorted
allServices // everything found
webServerCount // how many are _http/_https
isScanning, isRestored, error
scan({fresh}), select(service), clear()
httpOnly // get/set
isRestored tells "nothing selected" apart from "still reading storage", so you can avoid
flashing an empty state on launch.
Your own button #
MdnsScope(
launcher: false,
child: MaterialApp(home: HomePage()),
)
IconButton(
icon: const Icon(Icons.wifi_find),
onPressed: () => showMdnsPicker(context),
)
showMdnsPicker returns whatever is selected when the picker closes. Picking a server saves it
but leaves the picker open, so the choice is visibly confirmed and can be changed; the ✕ closes it.
Using the scanner directly #
No widgets involved — useful for a CLI or a background refresh:
final scanner = MdnsScanner(timeout: const Duration(seconds: 4));
scanner.stream.listen((services) => print('${services.length} so far'));
await scanner.scan();
scanner.dispose();
MdnsService gives you name, type, protocol, host, port, addresses, txt, plus
derived ip (IPv4 first, then IPv6, then the .local host), address (ip:port, IPv6 bracketed),
url, kind and isWebServer.
How the scan works #
Native (nsd) browses each type in serviceTypes through the OS and gets back resolved
services. Simple and dependable, but it can't tell you which types exist.
Wildcard (multicast_dns) fills that gap. The library only answers questions you ask, so the
browse is five steps:
- PTR on
_services._dns-sd._udp.local— which types are in use - PTR on each type — instances of it
- SRV on each instance — host and port
- A / AAAA on the host — addresses
- TXT on the instance — metadata
Types resolve concurrently, and results stream out as they arrive rather than after the whole
sweep, so the list fills in progressively. mDNS has no "done" signal, so every phase is bounded by
timeout.
Merging. Both backends see some of the same services. Records are keyed by fully-qualified name; where both resolved one, addresses are unioned (the two sometimes see different interfaces of the same host) and the record that actually carries addresses wins — an entry you can't dial is no use.
Either backend can be turned off:
MdnsScope(
useNsd: true, // native discovery
useMulticastDns: true, // wildcard sweep — off makes scans faster and quieter
serviceTypes: ['_http._tcp', '_https._tcp', '_myapp._tcp'],
child: MyApp(),
)
If you only care about your own service type, useMulticastDns: false with an explicit
serviceTypes is the fastest, most reliable configuration.
Example #
cd example
flutter run -d macos
Or check discovery against the real network with no UI:
cd example
flutter test integration_test/scan_test.dart -d macos
54 service(s), 4 web
mDNS: advertising "streaming-server" at Mac.local:3000 (_http._tcp.local)
-> 192.168.0.224:3000 http://192.168.0.224:3000
It runs as an integration test rather than a dart run script because the scanner depends on
nsd, a platform plugin — only the Flutter engine can compile it.
License #
MIT