Flutter近距离服务插件nearby_service的使用

发布于 1周前 作者 vueper 来自 Flutter

Flutter近距离服务插件nearby_service的使用

Overview

logo

Nearby Service 是一个用于在Flutter中创建P2P网络连接的插件。它支持发送文本消息和文件,可以在没有互联网连接的情况下轻松创建任何类型的信息共享应用程序。该插件目前仅支持Android-Android和iOS-iOS设备之间的通信。

访问Xenikii官网 | 查看LICENSE | Pub包页面

Table of Contents

About

Peer-to-peer (P2P) 网络是一种去中心化的网络架构,每个参与者(称为peer)都可以作为客户端和服务器。这意味着P2P网络可以直接交换资源(如文件、数据或服务),而无需中央权威或服务器。

About Android Plugin

对于Android,nearby_service 使用Wi-Fi Direct作为P2P网络,通过android.net.wifi.p2p模块实现。需要权限ACCESS_FINE_LOCATIONNEARBY_WIFI_DEVICES(针对Android 13+)。确保设备上的Wi-Fi已启用。

About iOS Plugin

对于iOS,P2P连接通过Multipeer Connectivity框架实现。此框架会自动选择最佳网络技术——如果两个设备在同一网络上,则使用Wi-Fi;否则使用对等Wi-Fi或蓝牙。

Features

  • Device Preparation

    • 请求使用Wi-Fi Direct的权限(仅限Android)
    • 检查Wi-Fi状态(仅限Android)
    • 打开设置以启用Wi-Fi(Android和iOS)
    • 角色选择 - 浏览器或广告商(仅限iOS)
  • Connecting to the Device from a P2P Network

    • 监听发现的设备(peers)
    • 在P2P网络上创建连接
    • 监控连接设备的状态
  • The Data Transmission Channel

    • 建立数据交换通道
    • 监控通道状态
  • Typed Data Exchange

    • 在P2P网络上传输文本数据
    • 接收成功传输文本消息的确认
    • 在P2P网络上传输文件
    • 确认或拒绝接收文件

Setup

Android Setup

所有必要的Android权限已经在插件的AndroidManifest.xml中配置好,因此无需额外添加。

iOS Setup

Info.plist中添加以下内容:

<key>NSBonjourServices</key>
  <array>
    <string>_mp-connection._tcp</string>
  </array>
<key>UIRequiresPersistentWiFi</key>
<true/>
<key>NSLocalNetworkUsageDescription</key>
<string>[Your description here]</string>

Usage

以下是使用nearby_service插件的基本步骤:

  1. 导入包:

    import 'package:nearby_service/nearby_service.dart';
    
  2. 创建NearbyService实例:

    final _nearbyService = NearbyService.getInstance();
    
  3. 初始化插件:

    await _nearbyService.initialize(
      data: NearbyInitializeData(iosDeviceName: iosDeviceName),
    );
    
  4. 对于Android,请求权限并检查Wi-Fi是否开启:

    final granted = await _nearbyService.android?.requestPermissions();
    if (granted ?? false) {
      // 继续下一步
    }
    
    final isWifiEnabled = await _nearbyService.android?.checkWifiService();
    if (isWifiEnabled ?? false) {
      // 继续下一步
    }
    
  5. 对于iOS,让用户选择是浏览器还是广告商:

    _nearbyService.ios?.setIsBrowser(value: isBrowser);
    
  6. 开始发现P2P网络:

    final result = await _nearbyService.discover();
    if (result) {
      // 继续监听peers
    }
    
  7. 监听peers:

    _nearbyService.getPeersStream().listen((event) => peers = event);
    
  8. 连接到设备:

    final result = await _nearbyService.connect(device);
    if (result) {
      // 继续监听设备
    }
    
  9. 监听连接设备:

    _connectedDeviceSubscription = _nearbyService.getConnectedDeviceStream(device).listen(
      (event) async {
        final wasConnected = connectedDevice?.status.isConnected ?? false;
        final nowConnected = event?.status.isConnected ?? false;
        if (wasConnected && !nowConnected) {
          // 返回到发现状态
        }
        connectedDevice = event;
      },
    );
    
  10. 启动通信通道:

    final messagesListener = NearbyServiceMessagesListener(
      onData: (message) {
        // 处理从NearbyServiceMessagesListener接收到的消息
      },
    );
    final filesListener = NearbyServiceFilesListener(
      onData: (pack) async {
        // 处理从NearbyServiceFilesListener接收到的文件包
      }, 
    );
    
    await _nearbyService.startCommunicationChannel(
      NearbyCommunicationChannelData(
        connectedDevice.info.id,
        messagesListener: messagesListener,
        filesListener: filesListener,
      ),
    );
    
  11. 发送消息:

    _nearbyService.send(
      OutgoingNearbyMessage(
        content: NearbyMessageTextRequest.create(value: message),
        receiver: connectedDevice.info,
      ),
    );
    

Data Sharing

Text Messages

描述操作逻辑:

  1. 设备A发送消息:

    _nearbyService.send(
      OutgoingNearbyMessage(
        content: NearbyMessageTextRequest.create(value: message),
        receiver: connectedDevice.info,
      ),
    );
    
  2. 设备B接收消息:

    final messagesListener = NearbyServiceMessagesListener(
      onData: (message) {
        // message是ReceivedNearbyMessage,内容为NearbyMessageTextRequest
      },
    );
    
  3. 设备B发送响应:

    _nearbyService.send(
      OutgoingNearbyMessage(
        receiver: connectedDevice.info,
        content: NearbyMessageTextResponse(id: requestId),
      ),
    );
    
  4. 设备A接收响应:

    final messagesListener = NearbyServiceMessagesListener(
      onData: (message) {
        // message是ReceivedNearbyMessage,内容为NearbyMessageTextResponse
      },
    );
    

Resource Messages

描述操作逻辑:

  1. 设备A发送文件请求:

    _nearbyService.send(
      OutgoingNearbyMessage(
        content: NearbyMessageFilesRequest.create(files: files),
        receiver: connectedDevice.info,
      ),
    );
    
  2. 设备B接收文件请求:

    final messagesListener = NearbyServiceMessagesListener(
      onData: (message) {
        // message是ReceivedNearbyMessage,内容为NearbyMessageFilesRequest
      },
    );
    
  3. 设备B确认接收文件:

    _nearbyService.send(
      OutgoingNearbyMessage(
        receiver: connectedDevice!.info,
        content: NearbyMessageFilesResponse(
          id: request.id,
          isAccepted: isAccepted,
        ),
      ),
    );
    
  4. 设备A接收确认:

    final messagesListener = NearbyServiceMessagesListener(
      onData: (message) {
        // message是ReceivedNearbyMessage,内容为NearbyMessageFilesResponse
      },
    );
    

Exceptions

  • NearbyServiceUnsupportedPlatformException: 插件不支持的平台
  • NearbyServiceUnsupportedDecodingException: 解码错误
  • NearbyServiceInvalidMessageException: 发送无效消息

Additional Options

  • 获取当前设备信息:getCurrentDeviceInfo()
  • 获取通信通道状态:communicationChannelState
  • 设置日志级别:logLevel

Demo

Android

android_demo

iOS

IOS_demo

示例代码

import 'dart:async';
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:nearby_service/nearby_service.dart';

enum AppState { idle, discovering, connected }

class App extends StatelessWidget {
  const App({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Nearby Service Example',
      home: Scaffold(
        appBar: AppBar(title: const Text('Nearby Service Example')),
        body: Container(
          alignment: Alignment.topCenter,
          padding: const EdgeInsets.all(20),
          child: const AppBody(),
        ),
      ),
    );
  }
}

class AppBody extends StatefulWidget {
  const AppBody({super.key});

  @override
  State<AppBody> createState() => _AppBodyState();
}

class _AppBodyState extends State<AppBody> {
  late final _nearbyService = NearbyService.getInstance(logLevel: NearbyServiceLogLevel.debug);

  AppState _state = AppState.idle;
  bool _isIosBrowser = true;
  List<NearbyDevice> _peers = [];
  StreamSubscription? _peersSubscription;
  CommunicationChannelState _communicationChannelState = CommunicationChannelState.notConnected;

  Timer? _connectionCheckTimer;
  NearbyDevice? _connectedDevice;

  @override
  void initState() {
    _initialize();
    super.initState();
  }

  @override
  void dispose() {
    _peersSubscription?.cancel();
    _connectionCheckTimer?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    if (_state == AppState.idle) {
      return Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          if (Platform.isIOS)
            IOSRoleSelector(
              isIosBrowser: _isIosBrowser,
              onSelect: (value) => setState(() => _isIosBrowser = value),
            ),
          ElevatedButton(
            onPressed: _startProcess,
            child: const Text('Start'),
          ),
        ],
      );
    } else if (_state == AppState.discovering) {
      return ListView(
        children: [
          if (Platform.isIOS)
            Text('You are ${_isIosBrowser ? 'Browser' : 'Advertiser'}'),
          if (_peers.isEmpty) const Text('Searching for peers...'),
          ..._peers.map(
            (e) => PeerWidget(
              device: e,
              isIosBrowser: _isIosBrowser,
              onConnect: _connect,
              communicationChannelState: _communicationChannelState,
            ),
          ),
        ],
      );
    } else if (_state == AppState.connected) {
      return SingleChildScrollView(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.start,
          crossAxisAlignment: CrossAxisAlignment.stretch,
          children: [
            ConnectedDeviceView(
              device: _connectedDevice!,
              onDisconnect: _disconnect,
            ),
            const SizedBox(height: 50),
            TextMessagingView(
              onSend: (message) => _send(NearbyMessageTextRequest.create(value: message)),
            ),
            const SizedBox(height: 50),
            FilesMessagingView(
              onSend: (files) {
                _send(
                  NearbyMessageFilesRequest.create(files: files),
                );
              },
            ),
          ],
        ),
      );
    }
    return Container();
  }

  Future<void> _initialize() async {
    await _nearbyService.initialize();
  }

  Future<void> _startProcess() async {
    final platformsReady = await _checkPlatforms();
    if (platformsReady) {
      await _discover();
    }
  }

  Future<bool> _checkPlatforms() async {
    if (Platform.isAndroid) {
      final isGranted = await _nearbyService.android?.requestPermissions();
      final wifiEnabled = await _nearbyService.android?.checkWifiService();
      return (isGranted ?? false) && (wifiEnabled ?? false);
    } else if (Platform.isIOS) {
      _nearbyService.ios?.setIsBrowser(value: _isIosBrowser);
      return true;
    } else {
      return false;
    }
  }

  Future<void> _discover() async {
    final result = await _nearbyService.discover();
    if (result) {
      setState(() {
        _state = AppState.discovering;
      });
      _peersSubscription = _nearbyService.getPeersStream().listen((event) {
        setState(() {
          _peers = event;
        });
      });
    }
  }

  Future<void> _connect(NearbyDevice device) async {
    final result = await _nearbyService.connectById(device.info.id);
    if (result || device.status.isConnected) {
      final channelStarting = _tryCommunicate(device);
      if (!channelStarting) {
        _connectionCheckTimer = Timer.periodic(
          const Duration(seconds: 3),
          (_) => _tryCommunicate(device),
        );
      }
    }
  }

  bool _tryCommunicate(NearbyDevice device) {
    NearbyDevice? selectedDevice;

    try {
      selectedDevice = _peers.firstWhere(
        (element) => element.info.id == device.info.id,
      );
    } catch (_) {
      return false;
    }

    if (selectedDevice.status.isConnected) {
      try {
        _startCommunicationChannel(device);
      } finally {
        _connectionCheckTimer?.cancel();
        _connectionCheckTimer = null;
      }
      return true;
    }
    return false;
  }

  void _startCommunicationChannel(NearbyDevice device) {
    if (_communicationChannelState != CommunicationChannelState.notConnected) {
      return;
    }
    _nearbyService.getCommunicationChannelStateStream().listen((event) {
      _communicationChannelState = event;
    });
    _nearbyService.startCommunicationChannel(
      NearbyCommunicationChannelData(
        device.info.id,
        filesListener: NearbyServiceFilesListener(
          onData: (pack) => _filesListener(Scaffold.of(context).context, pack),
        ),
        messagesListener: NearbyServiceMessagesListener(
          onData: _messagesListener,
          onCreated: () {
            setState(() {
              _connectedDevice = device;
              _state = AppState.connected;
            });
          },
          onError: (e, [StackTrace? s]) {
            setState(() {
              _connectedDevice = null;
              _state = AppState.idle;
            });
          },
          onDone: () {
            setState(() {
              _connectedDevice = null;
              _state = AppState.idle;
            });
          },
        ),
      ),
    );
  }

  Future<void> _disconnect() async {
    try {
      await _nearbyService.disconnectById(_connectedDevice!.info.id);
    } finally {
      await _nearbyService.endCommunicationChannel();
      await _nearbyService.stopDiscovery();
      await _peersSubscription?.cancel();
      setState(() {
        _peers = [];
        _state = AppState.idle;
      });
    }
  }

  void _messagesListener(ReceivedNearbyMessage<NearbyMessageContent> message) {
    if (_connectedDevice == null) return;
    message.content.byType(
      onTextRequest: (request) {
        AppSnackBar.show(
          context,
          title: request.value,
          subtitle: message.sender.displayName,
        ).whenComplete(
          () => _send(NearbyMessageTextResponse(id: request.id)),
        );
      },
      onTextResponse: (response) {
        AppSnackBar.show(
          context,
          title: 'Message ${response.id} was delivered',
          subtitle: message.sender.displayName,
        );
      },
      onFilesRequest: (request) {
        AppSnackBar.show(
          context,
          title: 'Request to receive ${request.files.length} files',
          subtitle: message.sender.displayName,
          actionName: 'Accept the files?',
          onAcceptAction: () {
            _send(
              NearbyMessageFilesResponse(id: request.id, isAccepted: true),
            );
          },
        );
      },
      onFilesResponse: (response) {
        AppSnackBar.show(
          context,
          title: 'Response ${response.id} for the files '
              '${response.isAccepted ? 'is accepted' : 'was denied'}',
          subtitle: message.sender.displayName,
        );
      },
    );
  }

  Future<void> _filesListener(
    BuildContext context,
    ReceivedNearbyFilesPack pack,
  ) async {
    await FilesSaver.savePack(pack);
    if (context.mounted) {
      AppSnackBar.show(context, title: 'Files pack was saved');
    }
  }

  Future<void> _send(NearbyMessageContent content) async {
    if (_connectedDevice == null) return;
    await _nearbyService.send(
      OutgoingNearbyMessage(
        content: content,
        receiver: _connectedDevice!.info,
      ),
    );
  }
}

希望这个详细的指南能帮助你更好地理解和使用nearby_service插件。如果有任何问题或建议,请随时联系我!


更多关于Flutter近距离服务插件nearby_service的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter近距离服务插件nearby_service的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


当然,以下是一个关于如何在Flutter项目中使用nearby_service插件的示例代码。这个插件允许你的应用使用Google Play服务的近距离服务功能(Nearby Connections API)来与其他设备进行通信。

首先,确保你已经在pubspec.yaml文件中添加了nearby_service依赖:

dependencies:
  flutter:
    sdk: flutter
  nearby_service: ^x.y.z  # 请替换为最新版本号

然后运行flutter pub get来安装依赖。

接下来,我们编写一个基本的Flutter应用,展示如何使用nearby_service插件。

主应用代码(main.dart

import 'package:flutter/material.dart';
import 'package:nearby_service/nearby_service.dart';

void main() {
  runApp(MyApp());
}

class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      title: 'Nearby Service Demo',
      theme: ThemeData(
        primarySwatch: Colors.blue,
      ),
      home: NearbyServiceDemo(),
    );
  }
}

class NearbyServiceDemo extends StatefulWidget {
  @override
  _NearbyServiceDemoState createState() => _NearbyServiceDemoState();
}

class _NearbyServiceDemoState extends State<NearbyServiceDemo> {
  NearbyService? _nearbyService;
  String _status = "Not started";
  List<String> _endpoints = [];

  @override
  void initState() {
    super.initState();
    initNearbyService();
  }

  void initNearbyService() async {
    _nearbyService = NearbyService();

    _nearbyService!.startAdvertising(
      serviceName: "MyService",
      strategy: Strategy.P2P_CLUSTER,
      callback: (endpointId, endpointInfo, status) {
        setState(() {
          if (status == Status.CONNECTED) {
            _endpoints.add(endpointId);
            _status = "Connected to ${_endpoints.length} devices";
          } else if (status == Status.DISCONNECTED) {
            _endpoints.remove(endpointId);
            _status = "Connected to ${_endpoints.length} devices";
          }
        });
      },
    );

    _nearbyService!.startDiscovery(
      serviceName: "MyService",
      strategy: Strategy.P2P_CLUSTER,
      callback: (endpointId, endpointInfo, status) {
        setState(() {
          if (status == Status.CONNECTED) {
            _endpoints.add(endpointId);
            _status = "Connected to ${_endpoints.length} devices";
          } else if (status == Status.DISCONNECTED) {
            _endpoints.remove(endpointId);
            _status = "Connected to ${_endpoints.length} devices";
          }
        });
      },
    );
  }

  void sendMessage(String endpointId, String message) async {
    await _nearbyService!.sendPayload(
      endpointId: endpointId,
      payload: Payload(
        type: PayloadType.BYTES,
        content: Uint8List.fromList(message.codeUnits),
      ),
    );
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Nearby Service Demo'),
      ),
      body: Padding(
        padding: const EdgeInsets.all(16.0),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text('Status: $_status'),
            SizedBox(height: 16),
            if (_endpoints.isNotEmpty)
              ListView.builder(
                shrinkWrap: true,
                itemCount: _endpoints.length,
                itemBuilder: (context, index) {
                  return ListTile(
                    title: Text('Endpoint: ${_endpoints[index]}'),
                    trailing: IconButton(
                      icon: Icon(Icons.send),
                      onPressed: () {
                        sendMessage(_endpoints[index], "Hello, Nearby!");
                      },
                    ),
                  );
                },
              ),
          ],
        ),
      ),
    );
  }

  @override
  void dispose() {
    _nearbyService?.stopAllEndpoints();
    super.dispose();
  }
}

注意事项

  1. 权限:确保在AndroidManifest.xml中添加了必要的权限,例如BLUETOOTH, BLUETOOTH_ADMIN, ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION等。

  2. Google Play服务:该插件依赖于Google Play服务,因此你的设备或模拟器需要安装Google Play服务。

  3. 策略选择:根据你的需求选择合适的策略(Strategy.P2P_CLUSTER, Strategy.P2P_POINT_TO_POINT, Strategy.P2P_STAR)。

  4. 错误处理:在实际应用中,添加更多的错误处理和用户反馈,以提高应用的健壮性和用户体验。

这个示例展示了如何启动广告、发现其他设备、连接设备以及发送消息。你可以根据实际需求进一步扩展和修改这个示例。

回到顶部