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.
example/lib/main.dart
import 'package:flutter/material.dart';
import 'package:fourb_mdns/fourb_mdns.dart';
void main() => runApp(const ExampleApp());
class ExampleApp extends StatefulWidget {
const ExampleApp({super.key});
@override
State<ExampleApp> createState() => _ExampleAppState();
}
class _ExampleAppState extends State<ExampleApp> {
bool _httpOnly = true;
bool _launcher = true;
@override
Widget build(BuildContext context) {
return MdnsScope(
httpOnly: _httpOnly,
launcher: _launcher,
onSelect: (s) => debugPrint('[mdns] selected: ${s?.address}'),
child: MaterialApp(
title: 'fourb_mdns',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorSchemeSeed: const Color(0xFF4F8CFF),
useMaterial3: true,
),
home: HomePage(
httpOnly: _httpOnly,
launcher: _launcher,
onHttpOnly: (v) => setState(() => _httpOnly = v),
onLauncher: (v) => setState(() => _launcher = v),
),
),
);
}
}
class HomePage extends StatelessWidget {
const HomePage({
super.key,
required this.httpOnly,
required this.launcher,
required this.onHttpOnly,
required this.onLauncher,
});
final bool httpOnly;
final bool launcher;
final ValueChanged<bool> onHttpOnly;
final ValueChanged<bool> onLauncher;
@override
Widget build(BuildContext context) {
final mdns = MdnsScope.of(context);
final theme = Theme.of(context);
return Scaffold(
appBar: AppBar(
title: const Text('fourb_mdns'),
actions: [
Padding(
padding: const EdgeInsets.only(right: 16),
child: Center(
child: Text(
mdns.isScanning ? 'scanning…' : '${mdns.allServices.length} found',
style: theme.textTheme.bodySmall,
),
),
),
],
),
body: ListView(
padding: const EdgeInsets.all(16),
children: [
Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('Selection', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
if (!mdns.isRestored)
const Text('reading saved pick…')
else if (mdns.selection == null)
const Text('Nothing selected yet.')
else ...[
_kv(context, 'name', mdns.selection!.name),
_kv(context, 'address', mdns.address ?? '—'),
_kv(context, 'ip', mdns.ip ?? '—'),
_kv(context, 'port', '${mdns.selection!.port}'),
_kv(context, 'url', mdns.url ?? '—'),
],
const SizedBox(height: 12),
Wrap(
spacing: 8,
children: [
FilledButton.tonal(
onPressed: () => showMdnsPicker(context),
child: const Text('Open picker'),
),
OutlinedButton(
onPressed: mdns.isScanning ? null : () => mdns.scan(),
child: const Text('Rescan'),
),
OutlinedButton(
onPressed: mdns.selection == null ? null : () => mdns.clear(),
child: const Text('Clear'),
),
],
),
if (mdns.error != null) ...[
const SizedBox(height: 8),
Text('${mdns.error}',
style: TextStyle(color: theme.colorScheme.error)),
],
],
),
),
),
const SizedBox(height: 12),
Card(
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('httpOnly'),
subtitle: const Text('_http._tcp / _https._tcp only'),
value: httpOnly,
onChanged: onHttpOnly,
),
SwitchListTile(
contentPadding: EdgeInsets.zero,
title: const Text('launcher'),
subtitle: const Text('built-in floating button'),
value: launcher,
onChanged: onLauncher,
),
],
),
),
),
const SizedBox(height: 12),
Text('Scanned network', style: theme.textTheme.titleSmall),
const SizedBox(height: 8),
if (mdns.services.isEmpty)
const Padding(
padding: EdgeInsets.all(24),
child: Center(child: Text('nothing yet')),
)
else
...mdns.services.map(
(s) => Card(
margin: const EdgeInsets.only(bottom: 6),
child: ListTile(
dense: true,
selected: mdns.selection?.id == s.id,
title: Text(s.name),
subtitle: Text(
'${s.kind} · ${s.host}:${s.port} · ${s.ip ?? "—"}',
style: const TextStyle(fontFamily: 'monospace', fontSize: 11),
),
trailing:
mdns.selection?.id == s.id ? const Icon(Icons.check) : null,
onTap: () => mdns.select(s),
),
),
),
const SizedBox(height: 80),
],
),
);
}
Widget _kv(BuildContext context, String k, String v) => Padding(
padding: const EdgeInsets.symmetric(vertical: 2),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 80,
child: Text(k, style: Theme.of(context).textTheme.bodySmall),
),
Expanded(
child: SelectableText(
v,
style: const TextStyle(fontFamily: 'monospace', fontSize: 12),
),
),
],
),
);
}