Ably Flutter 插件

一个 Flutter 插件,封装了 Ably 的 ably-cocoa (iOS) 和 ably-java (Android) 客户端库 SDK,Ably 是实时同步数字体验的平台。

Ably 提供顶级的实时消息基础设施和 API,能够大规模驱动实时体验,每天向数百万最终用户传递数十亿条实时消息。我们处理实时消息的复杂性,让您可以专注于您的代码。

支持的平台

iOS

iOS 10 及更高版本。

Android

API Level 19 (Android 4.4, KitKat) 及更高版本。

本项目使用 Java 8 语言特性,利用 Desugaring
来支持较低版本的 Android 运行时(即 API Level 24 以下)

如果您的项目需要支持低于 24 的 SDK 版本,则必须使用 Android Gradle 插件 4.0.0+。
您可能还需要相应地升级 gradle 分发版

已知限制

我们目前不支持的功能,但计划在未来添加。

  • 对称加密 (#104)
  • Ably 令牌生成 (#105)
  • REST 和 Realtime 统计数据 (#106)
  • 自定义 transportParams (#108)
  • 推送通知管理 (#109)
  • 故障期间记住备用主机 (#47)

示例应用程序

运行示例应用程序

  • 要运行示例应用程序,您需要一个 Ably API 密钥。请在 ably.com 上创建免费帐户,然后在示例应用程序中使用您的 API 密钥。
  • 克隆项目

Android Studio / IntelliJ Idea

在 run/debug 配置下拉菜单下,点击 Edit Configurations...。复制 Example App (Duplicate and modify) 配置。将 "Store as project file" 取消勾选,以避免将您的 Ably API 密钥提交到存储库。更新此新运行配置的 additional run args,填入您的 ably API 密钥。运行或调试您新的 run/debug 配置。

run-configuration-1

run-configuration-2

Visual Studio Code

  • Run and Debug 中,
    • 选择齿轮图标以查看 launch.json
    • 将您的 Ably API 密钥添加到 configurations.args 中,即用您自己的 Ably API 密钥替换 replace_with_your_api_key
    • 当连接的设备多于一个时选择特定设备:要启动到特定设备,请确保它是唯一已连接的设备。当连接了多个设备时,要在特定设备上运行,请在 configuration.args 值中添加另一个元素,使用 --device-id=replace_with_device_id
      • 请确保使用 flutter devices 中的设备 ID 替换 replace_with_device_id
  • 选择 example 配置

使用 Flutter 工具通过命令行

  • 切换到示例应用程序目录:cd example
  • 安装依赖项:flutter pub get
  • 启动应用程序:flutter run --dart-define ABLY_API_KEY=put_your_ably_api_key_here,记得将 put_your_ably_api_key_here 替换为您自己的 API 密钥。
    • 当连接的设备多于一个时选择特定设备:使用 flutter devices 获取您的设备 ID,然后运行 flutter run --dart-define=ABLY_API_KEY=put_your_ably_api_key_here --device-id replace_with_device_id

推送通知

有关在示例应用程序中使 PN 生效的详细信息,请参阅 PushNotifications.md

故障排除

  • 在 M1 Mac 上于模拟器上运行
    • Flutter 已添加对在 M1 架构上运行的 iOS 模拟器的支持,但稳定分支上尚未提供。在此期间,您可以在 Xcode 中将 iOS 目标更改为构建 Mac。
  • fatal error: 'ruby/config.h' file not found:Ruby 是安装 cocoapods 和用于构建过程的其他工具所必需的,而您的机器可能没有受支持的版本。要安装最新版本的 Ruby,请

用法

指定依赖项

包主页
pub.dev/packages/ably_flutter

参阅
将包依赖项添加到应用程序

导入包

import 'package:ably_flutter/ably_flutter.dart' as ably;

配置 Client Options 对象

有关选择身份验证方法(基本身份验证还是令牌身份验证)的指南,请阅读 选择身份验证机制

使用 基本身份验证/ API 密钥进行身份验证(用于运行示例应用程序/测试应用程序,不用于生产)

// Specify your apiKey with `flutter run --dart-define=ABLY_API_KEY=replace_your_api_key`
final String ablyApiKey = const String.fromEnvironment(Constants.ablyApiKey);
final clientOptions = ably.ClientOptions.fromKey(ablyApiKey);
clientOptions.logLevel = ably.LogLevel.verbose;  // optional

使用 令牌身份验证进行身份验证

// Used to create a clientId when a client first doesn't have one. 
// Note: you should implement `createTokenRequest`, which makes a request to your server that uses your Ably API key directly.
final clientOptions = ably.ClientOptions()
// ..clientId = _clientId // Optionally set the clientId
  ..autoConnect = false
  ..authCallback = (TokenParams tokenParams) async {
    try {
      // If a clientId was set in ClientOptions, it will be available in Ably.TokenParams.
      final tokenRequestMap =
      await createTokenRequest(tokenParams: tokenParams);
      return ably.TokenRequest.fromMap(tokenRequestMap);
    } catch (e) {
      print("Something went wrong in the authCallback:");
      print(e);
    }
  };
this._ablyClient = new ably.Realtime(options: clientOptions);
await this._ablyClient.connect();

使用 REST API

创建 REST 客户端实例

ably.Rest rest = ably.Rest(options: clientOptions);

获取通道实例

ably.RestChannel channel = rest.channels.get('test');

使用 REST 发布消息

// both name and data
await channel.publish(name: "Hello", data: "Ably");

// just name
await channel.publish(name: "Hello");

// just data
await channel.publish(data: "Ably");

// an empty message
await channel.publish();

获取 REST 历史记录

void getHistory([ably.RestHistoryParams params]) async {
  // getting channel history, by passing or omitting the optional params
  var result = await channel.history(params);

  var messages = result.items;        // get messages
  var hasNextPage = result.hasNext(); // tells whether there are more results
  if (hasNextPage) {    
    result = await result.next();     // will fetch next page results
    messages = result.items;
  }
  if (!hasNextPage) {
    result = await result.first();    // will fetch first page results
    messages = result.items;
  }
}

// history with default params
getHistory();

// sorted and filtered history
getHistory(ably.RestHistoryParams(direction: 'forwards', limit: 10));

获取 REST 通道存在成员

void getPresence([ably.RestPresenceParams params]) async {
  // getting channel presence members, by passing or omitting the optional params
  var result = await channel.presence.get(params);

  var presenceMembers = result.items; // returns PresenceMessages
  var hasNextPage = result.hasNext(); // tells whether there are more results
  if (hasNextPage) {
    result = await result.next();     // will fetch next page results
    presenceMembers = result.items;
  }
  if (!hasNextPage) {
    result = await result.first();    // will fetch first page results
    presenceMembers = result.items;
  }
}

// getting presence members with default params
getPresence();

// filtered presence members
getPresence(ably.RestPresenceParams(
  limit: 10,
  clientId: '<clientId>',
  connectionId: '<connectionID>',
));

获取 REST 存在历史记录

void getPresenceHistory([ably.RestHistoryParams params]) async {

  // getting channel presence history, by passing or omitting the optional params
  var result = await channel.presence.history(params);

  var presenceHistory = result.items; // returns PresenceMessages
  var hasNextPage = result.hasNext(); // tells whether there are more results
  if (hasNextPage) {
    result = await result.next();     // will fetch next page results
    presenceHistory = result.items;
  }
  if (!hasNextPage) {
    result = await result.first();    // will fetch first page results
    presenceHistory = result.items;
  }
}

// getting presence members with default params
getPresenceHistory();

// filtered presence members
getPresenceHistory(ably.RestHistoryParams(direction: 'forwards', limit: 10));

使用 Realtime API

创建 Realtime 客户端实例

ably.Realtime realtime = ably.Realtime(options: clientOptions);

监听连接状态更改事件

realtime.connection
  .on()
  .listen((ably.ConnectionStateChange stateChange) async {
    print('Realtime connection state changed: ${stateChange.event}');
    setState(() {
      _realtimeConnectionState = stateChange.current;
    });
});

监听特定的连接状态更改事件(例如,connected

realtime.connection
  .on(ably.ConnectionEvent.connected)
  .listen((ably.ConnectionStateChange stateChange) async {
    print('Realtime connection state changed: ${stateChange.event}');
    setState(() {
      _realtimeConnectionState = stateChange.current;
    });
});

创建 Realtime 通道实例

ably.RealtimeChannel channel = realtime.channels.get('channel-name');

监听通道事件

channel.on().listen((ably.ChannelStateChange stateChange) {
  print("Channel state changed: ${stateChange.current}");
});

附加到通道

await channel.attach();

从通道分离

await channel.detach();

订阅通道上的消息

var messageStream = channel.subscribe();
var channelMessageSubscription = messageStream.listen((ably.Message message) {
  print("New message arrived ${message.data}");
});

使用 channel.subscribe(name: "event1")channel.subscribe(names: ["event1", "event2"]) 来收听特定名称的消息。

取消订阅通道消息接收

await channelMessageSubscription.cancel();

发布通道消息

// both name and data
await channel.publish(name: "event1", data: "hello world");
await channel.publish(name: "event1", data: {"hello": "world", "hey": "ably"});
await channel.publish(name: "event1", data: [{"hello": {"world": true}, "ably": {"serious": "realtime"}]);

// single message
await channel.publish(message: ably.Message()..name = "event1"..data = {"hello": "world"});

// multiple messages
await channel.publish(messages: [
  ably.Message()..name="event1"..data = {"hello": "ably"},
  ably.Message()..name="event1"..data = {"hello": "world"}
]);

获取 Realtime 历史记录

void getHistory([ably.RealtimeHistoryParams params]) async {
  var result = await channel.history(params);

  var messages = result.items;        // get messages
  var hasNextPage = result.hasNext(); // tells whether there are more results
  if (hasNextPage) {    
    result = await result.next();     // will fetch next page results
    messages = result.items;
  }
  if (!hasNextPage) {
    result = await result.first();    // will fetch first page results
    messages = result.items;
  }
}

// history with default params
getHistory();

// sorted and filtered history
getHistory(ably.RealtimeHistoryParams(direction: 'forwards', limit: 10));

进入 Realtime 存在成员

await channel.presence.enter();

// with data
await channel.presence.enter("hello");
await channel.presence.enter([1, 2, 3]);
await channel.presence.enter({"key": "value"});

// with Client ID
await channel.presence.enterClient("user1");

// with Client ID and data
await channel.presence.enterClient("user1", "hello");
await channel.presence.enterClient("user1", [1, 2, 3]);
await channel.presence.enterClient("user1", {"key": "value"});

更新 Realtime 存在成员

await channel.presence.update();

// with data
await channel.presence.update("hello");
await channel.presence.update([1, 2, 3]);
await channel.presence.update({"key": "value"});

// with Client ID
await channel.presence.updateClient("user1");

// with Client ID and data
await channel.presence.updateClient("user1", "hello");
await channel.presence.updateClient("user1", [1, 2, 3]);
await channel.presence.updateClient("user1", {"key": "value"});

离开 Realtime 存在成员

await channel.presence.leave();

// with data
await channel.presence.leave("hello");
await channel.presence.leave([1, 2, 3]);
await channel.presence.leave({"key": "value"});

// with Client ID
await channel.presence.leaveClient("user1");

// with Client ID and data
await channel.presence.leaveClient("user1", "hello");
await channel.presence.leaveClient("user1", [1, 2, 3]);
await channel.presence.leaveClient("user1", {"key": "value"});

获取 Realtime 存在成员

var presenceMessages = await channel.presence.get();

// filter by Client Id
var presenceMessages = await channel.presence.get(
  ably.RealtimePresenceParams(
    clientId: 'clientId',
  ),
);

// filter by Connection Id
var presenceMessages = await channel.presence.get(
  ably.RealtimePresenceParams(
    connectionId: 'connectionId',
  ),
);

获取 Realtime 存在历史记录

void getPresenceHistory([ably.RealtimeHistoryParams params]) async {
  var result = await channel.presence.history(params);

  var messages = result.items;        // get messages
  var hasNextPage = result.hasNext(); // tells whether there are more results
  if (hasNextPage) {    
    result = await result.next();     // will fetch next page results
    messages = result.items;
  }
  if (!hasNextPage) {
    result = await result.first();    // will fetch first page results
    messages = result.items;
  }
}

// presence history with default params
getPresenceHistory();

// sorted and filtered history
getPresenceHistory(ably.RealtimeHistoryParams(direction: 'forwards', limit: 10));

订阅 Realtime 存在消息

// subscribe for all presence actions
channel
  .presence
  .subscribe()
  .listen((presenceMessage) {
    print(presenceMessage);
  },
);

// subscribe for specific action
channel
  .presence
  .subscribe(action: PresenceAction.enter)
  .listen((presenceMessage) {
    print(presenceMessage);
  },
);

// subscribe for multiple actions
channel
  .presence
  .subscribe(actions: [
    PresenceAction.enter,
    PresenceAction.update,
  ])
  .listen((presenceMessage) {
    print(presenceMessage);
  },
);

推送通知

有关使用此插件的 PN 的详细信息,请参阅 PushNotifications.md

注意事项

RTE6a 合规性

使用基于 Streams 的方法不完全符合
RTE6a
来自我们的
客户端库功能规范.

问题

StreamSubscription subscriptionToBeCancelled;

// Listener registered 1st
realtime.connection.on().listen((ably.ConnectionStateChange stateChange) async {
  if (stateChange.event == ably.ConnectionEvent.connected) {
    await subscriptionToBeCancelled.cancel();       // Cancelling 2nd listener
  }
});

// Listener registered 2nd
subscriptionToBeCancelled = realtime.connection.on().listen((ably.ConnectionStateChange stateChange) async {
  print('State changed');
});

在上面的示例中,当第一个监听器收到“connected”事件的通知时,第二个监听器将被取消。
根据
RTE6a,
第二个监听器也应该被触发。
它不会被触发,因为第二个监听器是在第一个监听器之后注册的,并且在第一个监听器触发后立即取消了流订阅。

如果第二个监听器在第一个监听器之前注册,就不会发生这种情况。

但是,使用一个巧妙的变通方法可以解决这个问题……

变通方法 - 延迟取消

await subscriptionToBeCancelled.cancel(); 替换为

Future.delayed(Duration.zero, () {
    subscriptionToBeCancelled.cancel();
});

GitHub

https://github.com/ably/ably-flutter