mdns_bloc 0.3.0
mdns_bloc: ^0.3.0 copied to clipboard
Bloc wrapper over 'multicast_dns' which performs service discovery over multicast DNS (mDNS), Bonjour and Avahi.

mdns_bloc #
Dart library to perform service discovery over multicast DNS (mDNS, also
known as Bonjour or Avahi), using Bloc. It wraps
multicast_dns behind a small bloc so your UI only has
to react to states. Pure Dart — usable from Flutter apps (via
flutter_bloc) and from command-line or server
applications alike.
Features #
- Searches for services by type (e.g.
_http._tcp), resolving PTR → SRV → A/AAAA and TXT records into typed, immutableMDnsServicevalues, with duplicates removed. - Progressive results: while the search runs, each
searchingstate carries a snapshot of the services discovered so far. - Optionally matches a specific service instance name (case-insensitively).
- Configurable per-lookup timeout, retry count, and network interfaces.
- Cancellable: stop a search with an event (keeping what was found), or start a new search to supersede the one in flight.
- Errors — including socket errors reported mid-search — surface as an
errorstate carrying the original exception, never as unhandled zone errors. - Each search runs on its own
MDnsClient, injectable for tests.
Usage #
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
import 'package:mdns_bloc/mdns_bloc.dart';
BlocProvider(
create: (_) => MDnsBloc()
..add(const MDnsEventStartSearch(
serviceType: '_http._tcp',
serviceName: 'example._http._tcp.local', // optional instance to match
)),
child: BlocBuilder<MDnsBloc, MDnsState>(
builder: (context, state) {
switch (state.status) {
case MDnsStatus.initial:
case MDnsStatus.searching:
return const CircularProgressIndicator();
case MDnsStatus.noneFound:
return const Text('No services found');
case MDnsStatus.stopped:
return const Text('Search stopped');
case MDnsStatus.found:
case MDnsStatus.matched:
return Text('Found ${state.services.length} services');
case MDnsStatus.error:
return Text('Error: ${state.errorMessage}');
}
},
),
);
Stop a running search with:
context.read<MDnsBloc>().add(const MDnsEventStopSearch());
MDnsEventStartSearch also accepts retries (extra attempts when nothing is
found, default 3) and timeout (how long each individual lookup waits,
default 5 seconds). Starting a new search while one is running cancels the
old one.
On multi-homed machines (VPNs, virtual adapters) you can control which network interfaces searches use:
MDnsBloc(
interfacesFactory: (type) async => (await NetworkInterface.list(
type: type,
))
.where((interface) => !interface.name.startsWith('utun'))
.toList(),
);
States #
MDnsStatus |
Meaning |
|---|---|
initial |
No search started yet. |
searching |
A search is in flight; the state carries the services discovered so far. |
noneFound |
The search completed without resolving any services. |
found |
Services were resolved (no instance name matched/requested). |
matched |
A service matching the requested instance name was found; see state.match. |
stopped |
The search was cancelled by MDnsEventStopSearch; services found before the cancellation are retained. |
error |
The search failed; see state.error / state.errorMessage. |
Discovered services are in state.services, a list of immutable
MDnsService values: instance name, host, port, SRV
priority/weight, the resolved IPv4/IPv6 addresses, and the TXT
attributes as both raw strings (txt) and a parsed map (txtAttributes).
state.discoveredNames additionally lists every instance name announced
over the wire, including ones that never resolved. On failure state.error
holds the original error object (often a SocketException), so you can
match on its type rather than its message.
Platform setup #
mDNS needs platform permissions; without them searches silently find nothing or fail.
iOS (and macOS 11+) #
Since iOS 14 the local network is permission-gated. Add to your app's
Info.plist the reason for local-network access and the Bonjour service
types you query:
<key>NSLocalNetworkUsageDescription</key>
<string>This app uses the local network to discover services advertised over mDNS/Bonjour.</string>
<key>NSBonjourServices</key>
<array>
<string>_http._tcp</string>
</array>
On physical iOS 14+ devices the two Info.plist keys are not sufficient on
their own: multicast_dns sends and receives raw UDP multicast rather than
using the Bonjour APIs, so the app must additionally be signed with the
restricted com.apple.developer.networking.multicast
entitlement, which you request from Apple. Without
it, discovery works in the simulator but on a device fails (typically
SocketException: No route to host, errno = 65) or silently finds nothing.
Note: discovery on physical iOS devices with the multicast entitlement has not yet been verified with this package — reports welcome on the issue tracker.
For sandboxed macOS apps also enable the network client/server entitlements
(com.apple.security.network.client and com.apple.security.network.server);
the multicast entitlement is not required on macOS.
Android #
Add to 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"/>
Some devices only deliver multicast packets while a multicast lock is held; if discovery finds nothing on a device, acquire one (e.g. through a plugin) before searching.
Testing #
MDnsBloc takes a clientFactory, so you can substitute a mock
MDnsClient (e.g. with mocktail) and test your UI without
touching the network:
final bloc = MDnsBloc(clientFactory: () => myMockClient);
See test/mdns_bloc_test.dart for examples.