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 配置。


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,请- 运行
brew install rbenv ruby-build - 安装 rbenv
- 运行
rbenv init(并按照其建议的说明进行操作) - 运行
rbenv install 3.0.1
- 运行
- 运行
sudo gem install cocoapods ffi
- 运行
用法
指定依赖项
包主页
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();
});