Flutter自定义功能插件adhoc_plugin的使用

Flutter自定义功能插件adhoc_plugin的使用

Ad Hoc Library (adhoc_plugin)

Pub Package

adhoc_plugin 是一个用于处理Android移动设备上的Ad Hoc网络操作的Flutter插件。

该库是Dart版本的AdHocLibrary项目的移植版。原项目由Gaulthier Gain开发,支持蓝牙和Wi-Fi Direct,而移植版本仅支持蓝牙低功耗(Bluetooth Low Energy)和Wi-Fi Direct。一些类保持不变,仅进行了少量修改,例如Android Wi-Fi Direct API。原始项目可以在以下链接找到:AdHocLib

该版本是为我在列日大学(Université de Liège)蒙泰菲奥里研究所(Montefiore Institute)的硕士学位论文设计的。除了移植原始项目外,还添加了一些新的安全功能,如向远程目标发送加密数据的能力。

使用方法

初始化库

要初始化库,可以按如下方式执行:

bool verbose = true;
TransferManager transferManager = TransferManager(verbose);

也可以通过配置Config对象来修改库的行为:

bool verbose = false;
Config config = Config();

config.label = "Example name"; // 用于通信
config.public = true; // 可以加入任何组

TransferManager transferManager = TransferManager(verbose, config);

监听事件

由于在Ad Hoc网络中会发生不同的事件,可以通过监听TransferManager暴露的广播流来获取这些事件。

TransferManager transferManager = TransferManager(false);

void _listen() {
  _manager.eventStream.listen((event) {
    switch (event.type) {
      case AdHocType.onDeviceDiscovered:
        var device = event.device as AdHocDevice;
        break;
      case AdHocType.onDiscoveryStarted:
        break;
      case AdHocType.onDiscoveryCompleted:
        var discovered = event.data as Map<String?, AdHocDevice?>;
        break;
      case AdHocType.onDataReceived:
        var data = event.data as Object;
        break;
      case AdHocType.onForwardData:
        var data = event.data as Object;
        break;
      case AdHocType.onConnection:
        var device = event.device as AdHocDevice;
        break;
      case AdHocType.onConnectionClosed:
        var device = event.device as AdHocDevice;
        break;
      case AdHocType.onInternalException:
        var exception = event.data as Exception;
        break;
      case AdHocType.onGroupInfo:
        var info = event.data as int;
        break;
      case AdHocType.onGroupDataReceived:
        var data = event.data as Object;
        break;
      default:
    }
  });
}

应用示例

音乐应用共享

这是一个展示如何使用库API的示例。

源代码

import 'dart:async';
import 'dart:collection';
import 'dart:io';
import 'dart:typed_data';

import 'package:adhoc_plugin/adhoc_plugin.dart';
import 'package:analyzer_plugin/utilities/pair.dart';
import 'package:file_picker/file_picker.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:path_provider/path_provider.dart';

import 'search_bar.dart';

void main() => runApp(AdHocMusicClient());

enum MenuOptions { add, search, display }

const platform = MethodChannel('adhoc.music.player/main');

class AdHocMusicClient extends StatefulWidget {
  [@override](/user/override)
  _AdHocMusicClientState createState() => _AdHocMusicClientState();
}

class _AdHocMusicClientState extends State<AdHocMusicClient> {
  static const PLAYLIST = 0;
  static const REQUEST = 1;
  static const REPLY = 2;
  static const TRANSFER = 3;

  static const NONE = 'none';

  final TransferManager _manager = TransferManager(true);
  final List<AdHocDevice> _discovered = List.empty(growable: true);
  final List<AdHocDevice> _peers = List.empty(growable: true);
  final List<Pair<String, String>> _playlist = List.empty(growable: true);
  final HashMap<String, HashMap<String, PlatformFile?>> _globalPlaylist = HashMap();
  final HashMap<String, PlatformFile?> _localPlaylist = HashMap();
  final HashMap<String, bool> _isTransfering = HashMap();
  final Set<String> timestamps = <String>{};

  bool _requested = false;
  bool _display = false;
  String? _selected = NONE;

  [@override](/user/override)
  void initState() {
    super.initState();
    _manager.enableBle(3600);
    _manager.eventStream.listen(_processAdHocEvent);
    _manager.open = true;
  }

  [@override](/user/override)
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Builder(
        builder: (context) => Scaffold(
          appBar: AppBar(
            centerTitle: true,
            title: const Text('Ad Hoc Music Client'),
            actions: <Widget>[
              PopupMenuButton<MenuOptions>(
                onSelected: (result) async {
                  switch (result) {
                    case MenuOptions.add:
                      await _openFileExplorer();
                      break;

                    case MenuOptions.search:
                      var songs = List<String>.empty(growable: true);
                      _localPlaylist.entries.map((entry) => songs.add(entry.key));

                      _selected = (await showSearch(
                        context: context,
                        delegate: SearchBar(songs),
                      ))!;

                      if (_selected == null) {
                        _selected = NONE;
                      }
                      break;

                    case MenuOptions.display:
                      setState(() => _display = !_display);
                      break;
                  }
                },
                itemBuilder: (context) => <PopupMenuEntry<MenuOptions>>[
                  const PopupMenuItem<MenuOptions>(
                    value: MenuOptions.add,
                    child: ListTile(
                      leading: Icon(Icons.playlist_add),
                      title: Text('Add song to playlist'),
                    ),
                  ),
                  const PopupMenuItem<MenuOptions>(
                    value: MenuOptions.search,
                    child: ListTile(
                      leading: Icon(Icons.search),
                      title: Text('Search song'),
                    ),
                  ),
                  const PopupMenuItem<MenuOptions>(
                    value: MenuOptions.display,
                    child: ListTile(
                      leading: Icon(Icons.music_note),
                      title: Text('Switch view'),
                    ),
                  ),
                ],
              ),
            ],
          ),
          body: Column(
            mainAxisSize: MainAxisSize.min,
            children: <Widget>[
              Expanded(
                child: Column(
                  children: <Widget>[
                    if (!_display) ...<Widget>[
                      Card(child: ListTile(title: Center(child: Text('Ad Hoc Peers')))),

                      ElevatedButton(
                        child: Center(child: Text('Search for nearby devices')),
                        onPressed: _manager.discovery,
                      ),

                      Expanded(
                        child: ListView(
                          children: _discovered.map((device) {
                            var type = device.mac.ble == '' ? 'Wi-Fi' : 'BLE';
                            var mac = device.mac.ble == '' ? device.mac.wifi : device.mac.ble;
                            if (device.mac.ble != '' && device.mac.wifi != '') {
                              type = 'Wi-Fi/BLE';
                              mac = '${device.mac.wifi}/${device.mac.ble}';
                            }

                            return Card(
                              child: ListTile(
                                title: Center(child: Text(device.name!)),
                                subtitle: Center(child: Text('$type: $mac')),
                                onTap: () async {
                                  await _manager.connect(device);
                                  setState(() => _discovered.removeWhere((element) => (element.mac == device.mac)));
                                },
                              ),
                            );
                          }).toList(),
                        ),
                      ),
                    ] else ...<Widget>[
                      Card(child: Stack(
                        children: <Widget> [
                          ListTile(
                            title: Center(child: Text('$_selected')),
                            subtitle: Row(
                              mainAxisAlignment: MainAxisAlignment.center,
                              children: <Widget>[
                                IconButton(
                                  icon: Icon(Icons.play_arrow_rounded),
                                  onPressed: _play,
                                ),
                                IconButton(
                                  icon: Icon(Icons.pause_rounded),
                                  onPressed: _pause,
                                ),
                                IconButton(
                                  icon: Icon(Icons.stop_rounded),
                                  onPressed: _stop,
                                ),
                                if (_requested)
                                  Container(child: Center(child: CircularProgressIndicator()))
                                else
                                  Container()
                              ],
                            ),
                          ),
                        ],
                      )),

                      Card(
                        color: Colors.blue,
                        child: ListTile(
                          title: Center(
                            child: const Text('Ad Hoc Playlist', style: TextStyle(color: Colors.white)),
                          ),
                        ),
                      ),

                      Expanded(
                        child: ListView(
                          children: _playlist.map((pair) {
                            return Card(
                              child: ListTile(
                                title: Center(child: Text(pair.last)),
                                subtitle: Center(child: Text(pair.first)),
                                onTap: () => setState(() => _selected = pair.last),
                              ),
                            );
                          }).toList(),
                        ),
                      ),
                    ],
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }

  void _processAdHocEvent(Event event) {
    switch (event.type) {
      case AdHocType.onDeviceDiscovered:
        break;
      case AdHocType.onDiscoveryStarted:
        break;
      case AdHocType.onDiscoveryCompleted:
        setState(() {
          for (final discovered in (event.data as Map).values) {
            _discovered.add(discovered as AdHocDevice);
          }
        });
        break;
      case AdHocType.onDataReceived:
        _processDataReceived(event);
        break;
      case AdHocType.onForwardData:
        _processDataReceived(event);
        break;
      case AdHocType.onConnection:
        _peers.add(event.device as AdHocDevice);
        break;
      case AdHocType.onConnectionClosed:
        break;
      case AdHocType.onInternalException:
        break;
      case AdHocType.onGroupInfo:
        break;
      case AdHocType.onGroupDataReceived:
        break;
      default:
    }
  }

  Future<void> _processDataReceived(Event event) async {
    var peer = event.device;
    var data = event.data as Map;

    switch (data['type'] as int) {
      case PLAYLIST:
        var peers = data['peers'] as List;
        var songs = data['songs'] as List;
        var timestamp = data['timestamp'] as String;
        if (timestamps.contains(timestamp)) {
          break;
        } else {
          timestamps.add(timestamp);
        }

        var peerName = peers.first as String;
        var entry = _globalPlaylist[peerName];
        if (entry == null) {
          entry = HashMap();
        }

        for (var i = 0; i < peers.length; i++) {
          if (peerName == peers[i]) {
            entry!.putIfAbsent(songs[i] as String, () => PlatformFile(name: songs[i] as String, size: 0));
          } else {
            _globalPlaylist[peerName] = entry!;

            peerName = peers[i] as String;
            entry = _globalPlaylist[peerName];
            if (entry == null) {
              entry = HashMap();
            }

            entry.putIfAbsent(songs[i] as String, () => PlatformFile(name: songs[i] as String, size: 0));
          }

          var pair = Pair<String, String>(peerName, songs[i] as String);
          if (!_playlist.contains(pair)) {
            _playlist.add(pair);
          }
        }

        _globalPlaylist[peerName] = entry!;

        setState(() {});

        _manager.broadcastExcept(data, peer!);
        break;

      case REQUEST:
        var name = data['name'] as String;
        var found = false;
        Uint8List? bytes;
        PlatformFile? file;

        if (_localPlaylist.containsKey(name)) {
          found = true;
          bytes = _localPlaylist[name]!.bytes!;
        } else {
          for (final entry in _globalPlaylist.entries) {
            var _playlist = entry.value;
            if (_playlist.containsKey(name)) {
              file = _playlist[name];
              if (file == null && file!.bytes == null) {
                found = false;
                break;
              } else {
                bytes = file.bytes;
                if (bytes != null) {
                  found = true;
                } else {
                  found = false;
                }
                break;
              }
            }
          }
        }

        if (found == false) {
          break;
        } else {
          var message = HashMap<String, dynamic>();
          message = HashMap<String, dynamic>();
          message.putIfAbsent('type', () => TRANSFER);
          message.putIfAbsent('name', () => name);
          _manager.sendMessageTo(message, peer!.label!);

          message.clear();

          message.putIfAbsent('type', () => REPLY);
          message.putIfAbsent('name', () => name);
          message.putIfAbsent('song', () => bytes);
          _manager.sendMessageTo(message, peer.label!);
        }

        break;

      case REPLY:
        var name = data['name'] as String;
        var song = Uint8List.fromList((data['song'] as List<dynamic>).cast<int>());

        var tempDir = await getTemporaryDirectory();
        var tempFile = File('${tempDir.path}/$name');
        await tempFile.writeAsBytes(song, flush: true);

        var entry = HashMap<String, PlatformFile>();
        entry.putIfAbsent(
          name, () => PlatformFile(
            bytes: song, name: name, path: tempFile.path, size: song.length
          )
        );

        _globalPlaylist.update(peer!.label!, (value) => entry, ifAbsent: () => entry);
        setState(() => _requested = false);
        break;

      case TRANSFER:
        var name = data['name'] as String;
        _isTransfering.update(name, (value) => true, ifAbsent: () => true);
        break;

      default:
    }
  }

  Future<void> _openFileExplorer() async {
    var result = await FilePicker.platform.pickFiles(
      allowMultiple: true,
      type: FileType.audio,
    );

    if(result != null) {
      for (var file in result.files) {
        var bytes = await File(file.path!).readAsBytes();
        var song = PlatformFile(
          name: file.name,
          path: file.path,
          bytes: bytes, 
          size: bytes.length,
        );

        _localPlaylist.putIfAbsent(file.name, () => song);
        var pair = Pair<String, String>(_manager.ownAddress, file.name);
        if (!_playlist.contains(pair)) {
          _playlist.add(pair);
        }
      }
    }

    _updatePlaylist();
  }

  void _updatePlaylist() async {
    var peers = List<String>.empty(growable: true);
    var songs = List<String>.empty(growable: true);

    _globalPlaylist.forEach((peer, song) {
      peers.add(peer);
      song.forEach((key, value) {
        songs.add(key);
      });
    });

    _localPlaylist.forEach((name, file) {
      peers.add(_manager.ownAddress);
      songs.add(name);
    });

    var message = HashMap<String, dynamic>();
    message.putIfAbsent('type', () => PLAYLIST);
    message.putIfAbsent('peers', () => peers);
    message.putIfAbsent('songs', () => songs);
    message.putIfAbsent('timestamp', () => DateTime.now().toIso8601String());
    _manager.broadcast(message);
  }

  void _play() {
    if (_selected!.compareTo(NONE) == 0) {
      return;
    }

    PlatformFile? file;
    if (_localPlaylist.containsKey(_selected)) {
      file = _localPlaylist[_selected];
    } else {
      _globalPlaylist.forEach((peerName, playlist) {
        if (playlist.containsKey(_selected)) {
          file = playlist[_selected];
          if (file == null || file!.size == 0) {
            var message = HashMap<String, dynamic>();
            message.putIfAbsent('type', () => REQUEST);
            message.putIfAbsent('name', () => _selected);
            _manager.broadcast(message);

            setState(() => _requested = true);
            _isTransfering.putIfAbsent(_selected!, () => false);

            Timer(Duration(seconds: 30), () {
              if (_requested == true && _isTransfering[_selected] == false) {
                _manager.sendMessageTo(message, peerName);
              }
            });
          }
        }
      });
    }

    if (_requested == false) {
      platform.invokeMethod('play', file!.path);
    }
  }

  void _pause() {
    if (_selected!.compareTo(NONE) == 0) {
      return;
    }

    platform.invokeMethod('pause');
  }

  void _stop() {
    if (_selected!.compareTo(NONE) == 0) {
      return;
    }

    _selected = NONE;
    platform.invokeMethod('stop');

    setState(() {});
  }
}

更多关于Flutter自定义功能插件adhoc_plugin的使用的实战教程也可以访问 https://www.itying.com/category-92-b0.html

1 回复

更多关于Flutter自定义功能插件adhoc_plugin的使用的实战系列教程也可以访问 https://www.itying.com/category-92-b0.html


adhoc_plugin 是一个用于在 Flutter 应用中集成 AdHoc SDK 的自定义插件。AdHoc 是一个基于云端的 A/B 测试和功能开关管理平台,允许开发者在不发布新版本的情况下,动态调整应用的功能和配置。

要使用 adhoc_plugin,你需要按照以下步骤进行设置和使用:

1. 添加依赖

首先,你需要在 pubspec.yaml 文件中添加 adhoc_plugin 依赖:

dependencies:
  flutter:
    sdk: flutter
  adhoc_plugin: ^最新版本号 # 请替换为最新的版本号

然后,运行 flutter pub get 来获取依赖。

2. 初始化 AdHoc SDK

在你的 Flutter 应用的 main.dart 文件中,初始化 AdHoc SDK。通常,你会在 main 函数中进行初始化:

import 'package:adhoc_plugin/adhoc_plugin.dart';

void main() async {
  WidgetsFlutterBinding.ensureInitialized();

  // 初始化 AdHoc SDK
  await AdhocPlugin.initialize(
    appKey: '你的AppKey', // 从 AdHoc 平台获取的 AppKey
    channel: 'your_channel', // 渠道名称
    deviceId: 'your_device_id', // 设备 ID,可选
  );

  runApp(MyApp());
}

3. 使用 AdHoc 功能

3.1 获取实验变量

你可以通过 AdhocPlugin.getFlag 方法获取某个实验变量的值:

bool isFeatureEnabled = await AdhocPlugin.getFlag('your_flag_name', false);
print('Feature enabled: $isFeatureEnabled');

getFlag 方法的第一个参数是实验变量的名称,第二个参数是默认值(如果实验未启用或未找到变量时返回的值)。

3.2 设置用户属性

你可以通过 AdhocPlugin.setUserProperty 方法设置用户属性:

await AdhocPlugin.setUserProperty('user_id', '12345');

3.3 记录事件

你可以通过 AdhocPlugin.track 方法记录用户事件:

await AdhocPlugin.track('your_event_name');

4. 处理实验更新

你可以监听实验变量的更新,并在变量发生变化时执行相应的逻辑:

AdhocPlugin.onExperimentUpdate((experiment) {
  print('Experiment updated: $experiment');
});

5. 调试和测试

在开发过程中,你可以启用 AdHoc 的调试模式,以便在控制台中查看日志:

await AdhocPlugin.setDebugEnabled(true);
回到顶部