Flutter PeerLink Plugin

A Flutter plugin for peer-to-peer communication between nearby devices using platform-native APIs. Supports both Android (Nearby Connections API) and iOS (MultipeerConnectivity framework) for local network discovery, connections, and data transfer.

Features

Device Discovery - Discover nearby devices on the same network ✅ Advertising - Make your device discoverable to others ✅ Peer-to-Peer Connections - Connect directly to discovered devices ✅ Data Transfer - Send bytes and streaming data between devices ✅ Connection Strategies - Support for Star, Point-to-Point, and Cluster topologies ✅ Thread-Safe - Efficient background processing for large data transfers ✅ Stream Protocol - Custom chunked streaming for large file transfers ✅ Event-Based - Reactive streams for device discovery and data reception

Platform Support

Platform API Version
Android Nearby Connections Android 5.0+ (API 21+)
iOS MultipeerConnectivity iOS 13.0+

⚠️ Important: Android and iOS devices cannot discover or connect to each other. They use different underlying protocols (Nearby Connections vs MultipeerConnectivity).

Installation

Add this to your pubspec.yaml:

dependencies:
  flutter_peerlink_plugin: ^0.0.2

Or install from your local repository:

dependencies:
  flutter_peerlink_plugin:
    path: ../flutter_peerlink_plugin

Then run:

flutter pub get

Platform Setup

Android Setup

1. Update AndroidManifest.xml

Add required permissions in android/app/src/main/AndroidManifest.xml:

<manifest xmlns:android="http://schemas.android.com/apk/res/android">
    <!-- Bluetooth permissions -->
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />

    <!-- Android 12+ (API 31+) Bluetooth permissions -->
    <uses-permission android:name="android.permission.BLUETOOTH_SCAN"
        android:usesPermissionFlags="neverForLocation" />
    <uses-permission android:name="android.permission.BLUETOOTH_ADVERTISE" />
    <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />

    <!-- WiFi permissions -->
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.CHANGE_WIFI_STATE" />

    <!-- Location permissions (required for Nearby Connections) -->
    <uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
    <uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />

    <!-- Android 10+ (API 29+) -->
    <uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />

    <!-- Android 13+ (API 33+) Nearby WiFi Devices -->
    <uses-permission android:name="android.permission.NEARBY_WIFI_DEVICES"
        android:usesPermissionFlags="neverForLocation" />

    <!-- Foreground service permission -->
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
    <uses-permission android:name="android.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE" />

    <application>
        <!-- Your app content -->

        <!-- Foreground service for background connections -->
        <service
            android:name="com.peerlink.flutter_peerlink_plugin.service.PeerlinkForegroundService"
            android:enabled="true"
            android:exported="false"
            android:foregroundServiceType="connectedDevice" />
    </application>
</manifest>

2. Update build.gradle

Ensure minimum SDK version in android/app/build.gradle:

android {
    defaultConfig {
        minSdkVersion 21  // Android 5.0+
        targetSdkVersion 34
    }
}

3. Google Play Services

The plugin uses Google Play Services Nearby Connections API. Most devices have this pre-installed, but you may need to ensure Play Services are up to date on test devices.

iOS Setup

1. Update Info.plist

Add required permissions and configurations in ios/Runner/Info.plist:

<dict>
    <!-- Bluetooth Usage Description -->
    <key>NSBluetoothAlwaysUsageDescription</key>
    <string>This app uses Bluetooth to discover and connect to nearby devices</string>

    <key>NSBluetoothPeripheralUsageDescription</key>
    <string>This app uses Bluetooth to discover and connect to nearby devices</string>

    <!-- Local Network Usage Description (iOS 14+) -->
    <key>NSLocalNetworkUsageDescription</key>
    <string>This app uses the local network to discover nearby devices</string>

    <!-- Bonjour Services (required for MultipeerConnectivity) -->
    <key>NSBonjourServices</key>
    <array>
        <!-- Replace with your service type (max 15 chars) -->
        <string>_peerlink._tcp</string>
    </array>

    <!-- Background Modes (optional, for background connections) -->
    <key>UIBackgroundModes</key>
    <array>
        <string>nearby-interaction</string>
    </array>
</dict>

⚠️ Important: Update the NSBonjourServices array with your actual service type used in initialize(). iOS requires the service type to be:

  • 1-15 characters
  • Lowercase alphanumeric and hyphens only
  • Format: _servicename._tcp

2. Deployment Target

Ensure minimum iOS version in ios/Podfile:

platform :ios, '13.0'

3. Service Type Limitations

iOS has strict service type requirements:

  • Maximum 15 characters
  • Lowercase letters, numbers, and hyphens only
  • The plugin automatically sanitizes Android-style service IDs

Example conversions:

  • my_app_servicemy-app-service
  • MyAppServiceWithLongNamemyappservicewi ✅ (truncated)
  • com.example.appcom-example-ap ✅ (sanitized)

Usage

1. Initialize the Plugin

import 'package:flutter_peerlink_plugin/flutter_peerlink_plugin.dart';

final plugin = FlutterPeerlinkPlugin.instance;

await plugin.initialize(
  serviceId: 'my-app',  // Use short, simple names (iOS: max 15 chars)
  deviceName: 'My Device',
  strategy: ConnectionStrategy.cluster,  // or .star, .pointToPoint
  autoAcceptOutgoingConnections: true,
  autoAcceptIncomingConnections: true,
);

2. Request Permissions

// Check permissions
bool hasPermissions = await plugin.checkPermissions();

if (!hasPermissions) {
  // Request permissions
  bool granted = await plugin.requestPermissions();

  if (!granted) {
    print('Permissions denied');
    return;
  }
}

3. Start Discovery & Advertising

// Start advertising (make this device discoverable)
await plugin.startAdvertising();

// Start discovering nearby devices
await plugin.startDiscovery();

// Listen for discovered devices
plugin.discoveryStream.listen((devices) {
  print('Discovered ${devices.length} devices');
  for (var device in devices) {
    print('- ${device.name} (${device.id})');
  }
});

4. Connect to a Device

// Connect to a discovered device
await plugin.connect(device.id);

// Listen for connection changes
plugin.connectionStream.listen((connections) {
  for (var conn in connections) {
    if (conn.state == LinkedDeviceState.connected) {
      print('Connected to ${conn.name}');
    } else if (conn.state is LinkedDeviceStateFailed) {
      print('Connection failed: ${(conn.state as LinkedDeviceStateFailed).errorCode}');
    }
  }
});

5. Accept/Reject Incoming Connections

// Automatically accept outgoing connections (set in initialize)
// For incoming connections, manually accept/reject:

plugin.connectionStream.listen((connections) {
  for (var conn in connections) {
    if (conn.isIncoming && conn.state == LinkedDeviceState.connecting) {
      // User decides to accept or reject
      await plugin.acceptConnection(conn.id);
      // or
      await plugin.rejectConnection(conn.id);
    }
  }
});

6. Send Bytes (Simple Data)

// Send small data payloads
final data = Uint8List.fromList([1, 2, 3, 4, 5]);
final payloadId = await plugin.sendBytes(device.id, data);
print('Sent bytes with payloadId: $payloadId');

// Receive bytes on the other device
plugin.onBytePayload.listen((payload) {
  print('Received ${payload.chunk.length} bytes from ${payload.deviceId}');
});

7. Stream Large Data

For large data transfers (files, media, etc.), use streaming:

// Start a stream
final payloadId = await plugin.startStream(device.id);

// Send data in chunks
const chunkSize = 8192;  // 8 KB chunks
for (int i = 0; i < largeData.length; i += chunkSize) {
  final end = (i + chunkSize < largeData.length) ? i + chunkSize : largeData.length;
  final chunk = largeData.sublist(i, end);
  await plugin.sendChunk(payloadId, chunk);
}

// Finish the stream
await plugin.finishStream(payloadId);

8. Receive Streams

plugin.onStreamPayload.listen((payload) {
  if (payload.isActive && payload.chunk.isEmpty) {
    print('Stream started: ${payload.id}');
    // Initialize buffer, open file, etc.
  } else if (payload.isActive && payload.chunk.isNotEmpty) {
    print('Received chunk: ${payload.chunk.length} bytes');
    // Append to buffer, write to file, etc.
  } else if (payload.isCompleted) {
    print('Stream completed: ${payload.id}');
    // Finalize file, close buffer, etc.
  } else if (payload.isFailed) {
    print('Stream failed: ${payload.id}');
  }
});

9. Cleanup

// Stop discovery/advertising
await plugin.stopDiscovery();
await plugin.stopAdvertising();

// Disconnect from a device
await plugin.disconnect(device.id);

// Dispose the plugin
plugin.dispose();

Connection Strategies

The plugin supports three connection strategies:

Cluster (Default)

ConnectionStrategy.cluster
  • All-to-all mesh network
  • Best for: Group chat, collaborative apps
  • Max peers: 8 devices

Star

ConnectionStrategy.star
  • One central hub, others connect to it
  • Best for: Multiplayer games with a host
  • Max peers: 8 devices

Point-to-Point

ConnectionStrategy.pointToPoint
  • Direct 1-to-1 connection
  • Best for: File transfers, private chat
  • Max peers: 1 device

Foreground Service (Android Only)

To keep connections alive in the background on Android:

// Start foreground service
await plugin.startForegroundService(
  title: 'PeerLink Active',
  message: 'Connected to nearby devices',
);

// Stop foreground service
await plugin.stopForegroundService();

Note: Foreground service is not supported on iOS. iOS requires background modes in Info.plist, but connections may still drop when the app is backgrounded.

API Reference

Methods

Method Description Returns
initialize(...) Initialize the plugin Future<void>
dispose() Clean up resources void
checkPermissions() Check if permissions granted Future<bool>
requestPermissions() Request required permissions Future<bool>
startAdvertising() Start advertising Future<void>
stopAdvertising() Stop advertising Future<void>
startDiscovery() Start discovering devices Future<void>
stopDiscovery() Stop discovering Future<void>
connect(deviceId) Connect to device Future<void>
acceptConnection(deviceId) Accept incoming connection Future<void>
rejectConnection(deviceId) Reject incoming connection Future<void>
disconnect(deviceId) Disconnect from device Future<void>
sendBytes(deviceId, bytes) Send bytes payload Future<int> (payloadId)
startStream(deviceId) Start stream transfer Future<int> (payloadId)
sendChunk(payloadId, chunk) Send stream chunk Future<void>
finishStream(payloadId) Finish stream normally Future<void>
startForegroundService(...) Start foreground service (Android) Future<void>
stopForegroundService() Stop foreground service (Android) Future<void>

Properties

Property Type Description
discoveredDevices List<PeerDevice> Current discovered devices
connectedDevices List<LinkedDevice> Current connections
discoveryStream Stream<List<PeerDevice>> Device discovery events
connectionStream Stream<List<LinkedDevice>> Connection state events
onBytePayload Stream<BytePayload> Incoming bytes data
onStreamPayload Stream<StreamPayload> Incoming stream data

Models

PeerDevice

class PeerDevice {
  final String id;        // Device identifier
  final String name;      // Device name
}

LinkedDevice

class LinkedDevice {
  final String id;              // Device identifier
  final String name;            // Device name
  final LinkedDeviceState state; // Connection state
  final bool isIncoming;        // True if remote initiated
}

LinkedDeviceState

enum LinkedDeviceState {
  disconnected,
  connecting,
  connected,
  failed(String errorCode),
}

BytePayload

class BytePayload {
  final String deviceId;     // Sender device ID
  final Uint8List chunk;     // Data received
}

StreamPayload

class StreamPayload {
  final int id;              // Stream payloadId
  final String deviceId;     // Sender device ID
  final List<int> chunk;     // Data chunk (empty for state changes)
  final bool isCompleted;    // Stream finished successfully
  final bool isFailed;       // Stream failed
  bool get isActive;         // True if stream is ongoing (not in terminal state)
}

Platform Differences

Feature Android iOS
Discovery Protocol Google Nearby Connections MultipeerConnectivity
Service ID Flexible string Max 15 chars, alphanumeric + hyphen
Cross-Platform ❌ No ❌ No
Foreground Service ✅ Yes ❌ No (returns ERROR)
Background Connections ✅ Reliable ⚠️ Limited
Auto-Chunking ✅ Yes Custom implementation
Max Peers 8 8

Android-Specific Notes

  • Requires Google Play Services
  • Location permission required (even though not used for location)
  • Foreground service keeps connections alive in background
  • Works over Bluetooth and WiFi Direct

iOS-Specific Notes

  • Service type automatically sanitized from Android format
  • Foreground service not supported (use background modes)
  • Connections may drop when app backgrounds
  • Works over Bluetooth LE and WiFi Direct
  • Requires NSBonjourServices in Info.plist

Troubleshooting

Android

Problem: Discovery not working Solution:

  • Ensure all permissions granted (especially location)
  • Check Bluetooth and WiFi are enabled
  • Verify Google Play Services are installed and up to date

Problem: Connections drop in background Solution:

  • Use startForegroundService() to keep connections alive

iOS

Problem: "Service type invalid" error Solution:

  • Service ID must be max 15 characters
  • Only lowercase alphanumeric and hyphens allowed
  • The plugin auto-sanitizes, but very long IDs get truncated

Problem: Permission prompts not appearing Solution:

  • Add all required keys to Info.plist
  • Ensure descriptions are user-friendly
  • Check NSBonjourServices includes your service type

Problem: Devices not discovering each other Solution:

  • Ensure both devices use the same service ID
  • Check WiFi and Bluetooth are enabled
  • Verify both devices on same network (for WiFi Direct)

Example App

See the example/ directory for a complete demo app that shows:

  • Device discovery and advertising
  • Connection management
  • Bytes and stream transfers
  • Permission handling
  • Progress tracking for large transfers

Run the example:

cd example
flutter run

Security Considerations

  • Encryption: Both platforms use encrypted connections (TLS on Android, built-in on iOS)
  • Authentication: No built-in authentication - implement your own handshake
  • Validation: Always validate received data before use
  • Privacy: Avoid including sensitive data in device names or service IDs

Limitations

  1. No Cross-Platform: Android ↔ iOS devices cannot communicate
  2. Network Required: Devices must be on same network or within Bluetooth range
  3. Max Peers: Limited to 8 simultaneous connections
  4. Background (iOS): Connections may drop when app backgrounds
  5. Service ID (iOS): Max 15 characters

Contributing

Contributions are welcome! Please:

  1. Fork the repository
  2. Create a feature branch
  3. Make your changes
  4. Add tests if applicable
  5. Submit a pull request

License

This project is licensed under the MIT License - see the LICENSE file for details.

Support

Acknowledgments