Nearby Connections in Flutter: Peer-to-Peer Without the Internet
Build offline file sharing and local multiplayer with nearby_connections and flutter_nearby_connections: strategies, payloads, and the Android permissions.
Published on • November 10, 2026
AI Assistant

Offline features like file sharing and local multiplayer break the moment the network is gone, yet the other phone is usually sitting a few meters away. Google’s Nearby Connections API bridges that gap by combining Bluetooth, BLE, and Wi-Fi into one fully encrypted peer-to-peer transport. This post shows how to drive it from Flutter, which advertising strategy to pick, how payloads and the connection lifecycle behave, and the permissions that decide whether anything connects at all.
Two plugins, one Google API
pub.dev has two plugins wrapping Nearby Connections, and they are not interchangeable:
- nearby_connections (current 4.3.0) is an Android-only plugin for the Nearby Connections API. Its README states it “Currently supports Bytes and Files” and warns that Location/GPS must be turned on or devices disconnect immediately.
- flutter_nearby_connections (current 1.1.2) covers Android and iOS. On Android it uses Nearby Connections (plus an optional
Strategy.Wi_Fi_P2Pmode); on iOS it maps to Apple’s Multipeer Connectivity framework, so the Dart API differs slightly between platforms.
dependencies:
flutter:
sdk: flutter
nearby_connections: ^4.3.0
permission_handler: ^11.0.0
Pick a strategy before you write code
Google’s strategies doc defines three topologies, all working within roughly 100 m of radio range:
| Strategy | Topology | Bandwidth | Fits |
|---|---|---|---|
P2P_CLUSTER | M-to-N mesh, every device can connect both ways | Lower | Multiplayer games, chat |
P2P_STAR | 1-to-N hub or spoke, not both at once | Higher | One phone sharing to a group |
P2P_POINT_TO_POINT | 1-to-1 only | Highest | Moving one large file |
const strategy = Strategy.P2P_CLUSTER; // mesh: many devices, small payloads
// const strategy = Strategy.P2P_STAR; // one hub, N spokes
// const strategy = Strategy.P2P_POINT_TO_POINT; // single link, max throughput
Both sides must advertise or discover with the same strategy, or endpoints never match. Cluster is the safe default for local multiplayer; switch to point-to-point when you are pushing a video to one device.
Advertising, discovery, and the connection lifecycle
Usage splits into a pre-connection phase (advertise, discover, request, accept) and a post-connection phase where the API is symmetric - there is no longer an advertiser or discoverer, and links are full-duplex.
final nearby = Nearby();
await nearby.startAdvertising(
userName,
strategy,
serviceId: "com.yourdomain.appname", // unique to your app
onConnectionInitiated: (id, info) => nearby.acceptConnection(
id,
onPayLoadRecieved: (endpointId, payload) => handle(payload),
onPayloadTransferUpdate: (endpointId, update) => showProgress(update),
),
onConnectionResult: (id, status) => print("connected: $status"),
onDisconnected: (id) => print("peer left: $id"),
);
await nearby.startDiscovery(
userName,
strategy,
serviceId: "com.yourdomain.appname",
onEndpointFound: (id, peerName, serviceId) =>
nearby.requestConnection(userName, id,
onConnectionInitiated: (_, __) {},
onConnectionResult: (_, __) {},
onDisconnected: (_) {}),
onEndpointLost: (id) => print("lost: $id"),
);
A connection only succeeds when both sides accept through onConnectionInitiated, then both get onConnectionResult. Call stopDiscovery() once you have found the peer you want - the plugin README notes that stopping reduces disconnection chances, while already discovered endpoints stay available for connecting.
Payloads: bytes, stream, and file
Google documents three payload types: bytes (arrays capped at 32 KB, for metadata and control messages), file (any size, streamed from the app to the network interface with minimal copying), and stream (data generated on the fly, such as recorded audio, with no known final size). nearby_connections exposes the first two:
// Small control message
await nearby.sendBytesPayload(endpointId, utf8.encode("start:42"));
// Large file - the plugin writes it to Downloads/.nearby/ with a generic name,
// so send the real filename as a separate bytes payload tagged with its payload id.
final payloadId = await nearby.sendFilePayload(endpointId, filePath);
await nearby.sendBytesPayload(endpointId, utf8.encode("$payloadId:report.pdf"));
// Receiver side: copy it somewhere sensible once the transfer reports success
await nearby.copyFileAndDeleteOriginal(uri, "$destDir/report.pdf");
onPayloadTransferUpdate reports status (success / failure / in_progress) plus bytes transferred and total bytes, which is where your progress bar lives. flutter_nearby_connections additionally supports streaming payloads on both platforms; nearby_connections does not.
Permissions: the part that breaks builds
Add these to android/app/src/main/AndroidManifest.xml (copied from the plugin README):
<uses-permission android:maxSdkVersion="31" android:name="android.permission.ACCESS_WIFI_STATE" />
<uses-permission android:maxSdkVersion="31" android:name="android.permission.CHANGE_WIFI_STATE" />
<uses-permission android:maxSdkVersion="30" android:name="android.permission.BLUETOOTH" />
<uses-permission android:maxSdkVersion="30" android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:maxSdkVersion="28" android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:minSdkVersion="29" android:maxSdkVersion="31" android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:minSdkVersion="31" android:name="android.permission.BLUETOOTH_ADVERTISE" />
<uses-permission android:minSdkVersion="31" android:name="android.permission.BLUETOOTH_CONNECT" />
<uses-permission android:minSdkVersion="31" android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:minSdkVersion="32" android:name="android.permission.NEARBY_WIFI_DEVICES" />
Location, Bluetooth, storage, and nearby-Wi-Fi permissions are dangerous permissions: request them at runtime with permission_handler, and make sure the location service itself is enabled (Permission.location.serviceStatus.isEnabled plus a prompt to open settings). Android 12 introduced the three BLUETOOTH_* runtime permissions, and you may need compileSdkVersion 32 for NEARBY_WIFI_DEVICES. Skip any of this and startAdvertising or startDiscovery throws a platform exception about Bluetooth or insufficient permissions.
iOS, emulators, and where it fits
flutter_nearby_connections runs on iOS through Multipeer Connectivity, which uses infrastructure Wi-Fi, peer-to-peer Wi-Fi, and Bluetooth PAN over UDP. iOS 14+ requires NSBonjourServices entries in Info.plist matching your service type (the plugin example uses mp-connection), plus UIRequiresPersistentWiFi and NSBluetoothAlwaysUsageDescription. The plugin also notes that its Android side does not run on emulators - test on real devices. Practical targets for this stack are offline photo/video transfer, local multiplayer lobbies, and controller-to-TV play; anything needing a server or internet is outside what Nearby Connections solves.
Takeaway: Choose nearby_connections for Android-only byte/file transfers or flutter_nearby_connections when iOS matters, pick the strategy (P2P_CLUSTER, P2P_STAR, P2P_POINT_TO_POINT) to match your topology before coding, handle bytes vs file payloads with progress callbacks, and budget real effort for the Android 12+ Bluetooth, location, and NEARBY_WIFI_DEVICES runtime permissions.