Block 网络的核心 Flutter SDK,提供网络通信、加密传输、数据模型和缓存能力。
新项目只需依赖此包即可接入 Block 网络,无需重复实现加密、路由拼装等底层逻辑。
Block 网络是一个去中心化的数据网络,所有数据以"Block"为单位存储和传输。
- 每个 Block 有唯一的 BID(21字符标识符)
- Block 通过
model字段区分类型(document、article、set、file、service、user、record、gps 等) - 所有网络请求走同一个 HTTP 端点
/bridge/ins,通过routing字段区分操作 - 请求和响应均使用 AES-CBC + Base64 对称加密
连接到 Block 网络节点需要两个核心信息:
address:节点的 HTTP 地址,例如http://192.168.1.100:8080keyBase64:与该节点共享的 AES 对称密钥(Base64 编码)
所有 API 类都接受 ConnectionModel 作为构造参数。
protocol 字段有两个值:
cert(默认):需要节点证书验证的请求,用于大多数数据操作open:无需证书的开放请求,用于节点信息查询(如getSignature)
# pubspec.yaml
dependencies:
block_flutter:
path: ./block_flutter # 本地路径,或发布后使用版本号import 'package:block_flutter/block_flutter.dart';final connection = ConnectionModel(
name: '我的节点',
address: 'http://192.168.1.100:8080',
keyBase64: 'your-base64-encoded-aes-key==',
status: ConnectionStatus.connected,
);
final blockApi = BlockApi(connection: connection);
final response = await blockApi.getBlock(bid: 'abc123def456ghi789012');block_flutter/lib/
├── block_flutter.dart # 统一导出入口
└── src/
├── models/
│ ├── connection_model.dart # 节点连接信息
│ ├── block_model.dart # Block 数据模型
│ ├── result.dart # Result<T> 封装
│ └── api_error.dart # API 错误类型
├── network/
│ ├── bridge_transport.dart # 底层加密 HTTP 传输
│ ├── api_client.dart # 通用请求客户端
│ ├── block_api.dart # Block CRUD 操作
│ └── node_api.dart # 节点信息查询
├── cache/
│ ├── block_cache.dart # Block 元数据缓存(内存+磁盘)
│ ├── image_cache.dart # 图片缓存(多尺寸变体)
│ └── audio_cache.dart # 音频文件缓存
└── crypto/
└── crypto_util.dart # AES-CBC / AES-GCM 加解密工具
所有公开类均通过 package:block_flutter/block_flutter.dart 统一导出,无需引用内部路径。
描述一个 Block 网络节点的连接信息。
const connection = ConnectionModel(
name: '主节点',
address: 'http://192.168.1.100:8080',
keyBase64: 'base64encodedkey==',
status: ConnectionStatus.connected,
isActive: true,
enableIpfsStorage: false, // 是否用于 IPFS 存储
);| 字段 | 类型 | 说明 |
|---|---|---|
name |
String |
节点显示名称 |
address |
String |
节点 HTTP 地址 |
keyBase64 |
String |
AES 对称密钥(Base64) |
status |
ConnectionStatus |
connected / connecting / offline |
isActive |
bool |
是否为当前活跃连接 |
nodeData |
Map? |
节点元数据(由 /node/node 返回) |
signatureData |
Map? |
节点签名数据 |
enableIpfsStorage |
bool |
是否启用 IPFS 存储 |
序列化:
// 持久化
final json = connection.toJson();
// 恢复
final connection = ConnectionModel.fromJson(json);
// 更新字段(不可变模式)
final updated = connection.copyWith(status: ConnectionStatus.offline);所有 Block 数据的统一容器,内部是一个 Map<String, dynamic>,提供类型安全的读取方法。
final block = BlockModel(data: responseMap);
// 类型安全读取
final bid = block.getString('bid'); // String,不存在返回 ''
final name = block.maybeString('name'); // String?,空字符串返回 null
final count = block.getInt('count'); // int,不存在返回 0
final flag = block.getBool('is_public'); // bool
final tags = block.getList<String>('tags'); // List<String>
final meta = block.getMap('metadata'); // Map<String, dynamic>
final created = block.getDateTime('created_at'); // DateTime?
// 便捷属性
block.bid // maybeString('bid')
block.typeId // maybeString('model')
block.title // maybeString('name')
block.intro // maybeString('intro')
block.createdAt // getDateTime('created_at')
block.updatedAt // getDateTime('updated_at')
// 判断字段是否存在且非空
if (block.has('cover_cid')) { ... }
// 函数调用语法(等同于 getString)
final title = block('name');
// 更新字段
final updated = block.copyWith({'name': '新标题'});操作结果的密封类封装,用于需要显式处理成功/失败的场景。
// 创建
final ok = Success<String>('data');
final err = Failure<String>(Exception('something went wrong'));
// 消费
result.when(
success: (data) => print(data),
failure: (error) => print(error),
);
// 映射
final mapped = result.map((s) => s.length);
// 快速判断
if (result.isSuccess) { ... }
final data = result.dataOrNull;API 层抛出的结构化错误。
try {
await blockApi.getBlock(bid: bid);
} on ApiError catch (e) {
print(e.message); // 错误描述
print(e.code); // 业务错误码(可为 null)
print(e.statusCode); // HTTP 状态码(可为 null)
}调用方
│
▼
BlockApi / NodeApi ← 高层 API,封装路由和参数
│
▼
ApiClient ← 通用请求客户端,组装 payload
│
▼
BridgeTransport ← 加密 + HTTP POST /bridge/ins
│
▼
Block 网络节点
底层传输层,负责加密、发送、解密。通常不需要直接使用,由 ApiClient 调用。
final response = await BridgeTransport.post(
connection: connection,
payload: {
'protocol': 'cert',
'routing': '/block/block/get',
'data': {'bid': 'abc123...'},
'receiver': '',
'wait': true,
'timeout': 60,
},
);传输流程:
- 将 payload JSON 序列化
- 用
CryptoUtil.encryptBase64加密(AES-CBC,随机 IV) - POST
{"text": "<密文>"}到{address}/bridge/ins - 解析响应中的
text字段并解密 - 返回解密后的
Map<String, dynamic>
通用请求客户端,接受 ConnectionModel,封装 payload 组装逻辑。
final client = ApiClient(connection: connection);
final response = await client.postToBridge(
routing: '/block/block/get',
data: {'bid': 'abc123...'},
// 以下均有默认值
protocol: 'cert', // 'cert' 或 'open'
receiver: '', // 接收节点 BID,'' 表示默认
wait: true, // 是否等待响应
timeout: 60, // 超时秒数
receiverBid: null, // 指定接收节点前缀(优先于 receiver)
);payload 字段说明:
| 字段 | 说明 |
|---|---|
routing |
Block 网络内部路由,类似 HTTP path |
data |
业务数据,随路由不同而变化 |
protocol |
cert(需认证)或 open(公开) |
receiver |
接收节点标识,"" 默认,"*" 广播 |
wait |
true 等待响应数据,false 仅发送 |
timeout |
等待超时秒数 |
Block 数据的 CRUD 操作,自动处理路由拼装和 BID 规范化。
final api = BlockApi(connection: connection);// 支持传入 String BID 或 BlockModel 实例
final response = await api.getBlock(bid: 'abc123def456ghi789012');
final response = await api.getBlock(bid: someBlockModel);final response = await api.saveBlock(
data: {
'bid': 'abc123def456ghi789012',
'model': 'document',
'name': '标题',
'content': '正文内容',
'created_at': '2025-01-01T00:00:00+08:00',
},
receiverBid: null, // 可选,不传则从 data['bid'] 前10位推断
);
data中若包含node_bid字段,会自动作为receiverBid使用。
final response = await api.deleteBlock(bid: 'abc123def456ghi789012');
// 删除 Block 本身及其所有关联 Link 和 Tagfinal response = await api.getBlocksByTag(
name: 'flutter',
page: 1,
limit: 20,
);// 获取以指定 BID 为 target 的所有 Link
final response = await api.getLinksByTarget(
bid: 'abc123...',
page: 1,
limit: 10,
order: 'desc', // 可选
);// 获取以指定 BID 为 main 的所有 Link,支持按 model/tag 过滤
final response = await api.getLinksByMain(
bid: 'abc123...',
page: 1,
limit: 10,
model: 'document', // 可选
tag: 'flutter', // 可选
order: 'asc', // 可选
);// 批量查询多个 target BID 的 Link
final response = await api.getLinksByTargets(
bids: ['bid1', 'bid2', 'bid3'],
page: 1,
limit: 10,
);final response = await api.getRecentBlocks(limit: 20, page: 1);final response = await api.getMultipleBlocks(
bids: ['bid1', 'bid2', 'bid3'],
);final response = await api.getAllBlocks(
page: 1,
limit: 20,
order: 'asc', // 'asc' 或 'desc'
model: 'document', // 可选,按类型过滤
tag: 'flutter', // 可选,按标签过滤
excludeModels: ['service', 'user'], // 可选,排除类型
receiverBid: null, // 可选
);节点信息查询。
final nodeApi = NodeApi(connection: connection);
// 获取节点签名信息(使用 open 协议,无需认证)
final response = await nodeApi.getSignature();Block 元数据的两级缓存(内存 LRU + 磁盘 SharedPreferences),TTL 3天,内存最多100条。
final cache = BlockCache.instance; // 单例
// 读取(内存 → 磁盘 → null)
final block = await cache.get('abc123...');
if (block == null) {
// 缓存未命中,从 API 获取
final response = await blockApi.getBlock(bid: 'abc123...');
final fetched = BlockModel(data: response);
await cache.put('abc123...', fetched);
}
// 写入
await cache.put('abc123...', blockModel);
// 删除
await cache.remove('abc123...');
// 清空全部
await cache.clear();
// 统计
final stats = cache.getStats();
// {'memorySize': 42, 'maxMemorySize': 100, 'ttlHours': 72}图片的多级缓存,支持 small(240px)、medium(480px)、original 三种尺寸变体,磁盘上限 2GiB,内存上限 120MiB。
// 检查磁盘缓存
final file = await ImageCacheHelper.getCachedImage(
cid,
variant: ImageVariant.medium,
);
// 保存到缓存
await ImageCacheHelper.saveImageToCache(cid, bytes, variant: ImageVariant.original);
// 内存缓存读写
final bytes = ImageCacheHelper.getMemoryImage(cid, variant: ImageVariant.small);
ImageCacheHelper.cacheMemoryImage(cid, bytes, variant: ImageVariant.medium);
// 一体化加载(内存 → 磁盘 → fetcher)
final bytes = await ImageCacheHelper.getOrLoadImage(
cid: cid,
endpoint: 'http://ipfs-gateway.example.com',
variant: ImageVariant.medium,
fetcher: ({required cid, required endpoint, variant = ImageVariant.original}) async {
// 实现具体的网络获取逻辑
return await myIpfsFetch(cid, endpoint);
},
);
// 生成缩略图变体(异步,去重并发)
final thumbnail = await ImageCacheHelper.ensureVariant(
cid,
originalBytes,
variant: ImageVariant.small,
);
// 删除所有变体
await ImageCacheHelper.removeFromCache(cid);ImageVariant 说明:
| 变体 | 目标宽度 | 用途 |
|---|---|---|
small |
240px | 列表缩略图 |
medium |
480px | 预览图 |
original |
原始 | 详情页全图 |
音频文件的磁盘缓存,按 CID + 扩展名存储。
// 检查缓存
final file = await AudioCacheHelper.getCachedAudio(cid, '.mp3');
if (file != null) {
// 使用缓存文件
}
// 保存
final cachedFile = await AudioCacheHelper.saveAudioToCache(cid, bytes, '.mp3');
// 删除
await AudioCacheHelper.deleteCachedAudio(cid, '.mp3');
// 清空全部
await AudioCacheHelper.clearAllCache();
// 统计
final sizeBytes = await AudioCacheHelper.getCacheSize();
final count = await AudioCacheHelper.getCacheCount();
final readable = AudioCacheHelper.formatCacheSize(sizeBytes); // '12.34 MB'AES-CBC(主要用于网络传输)和 AES-GCM(用于文件加密)的工具类。
// AES-CBC 加密(用于 BridgeTransport)
// 输出格式:Base64(随机16字节IV + 密文)
final encrypted = CryptoUtil.encryptBase64(plainText, base64Key);
final decrypted = CryptoUtil.decryptBase64(encrypted, base64Key);
// Base64 规范化(去除空白,补齐填充)
final normalized = CryptoUtil.normalizeBase64(rawBase64String);
// AES-GCM 加密(用于文件内容加密)
// 输出格式:Hex(16字节nonce + 密文 + 16字节认证tag)
final keyBytes = CryptoUtil.generateRandomKey(32); // 256-bit key
final hexEncrypted = CryptoUtil.encryptAesGcm(dataBytes, keyBytes);
final decryptedBytes = CryptoUtil.decryptAesGcm(hexEncrypted, keyBytes);
// Hex 工具
final bytes = CryptoUtil.hexToBytes('deadbeef');密钥长度: 支持 16(AES-128)、24(AES-192)、32(AES-256)字节。
在 Flutter 应用中,连接信息通常由状态管理层(如 Provider)持有,而 SDK 的 API 类接受 ConnectionModel。推荐使用适配层模式桥接两者:
// 适配层示例:从 Provider 获取连接,委托给 SDK
class BlockApi {
BlockApi({required ConnectionProvider connectionProvider})
: _connectionProvider = connectionProvider;
final ConnectionProvider _connectionProvider;
Future<Map<String, dynamic>> getBlock({required dynamic bid}) {
final connection = _connectionProvider.activeConnection;
if (connection == null) {
throw StateError('No active connection available. Cannot perform request.');
}
// 委托给 SDK,每次调用动态获取最新连接
return sdk.BlockApi(connection: connection).getBlock(bid: bid);
}
}为什么每次调用都新建 SDK 实例?
连接可能在运行时切换(用户切换节点),每次调用时动态获取 activeConnection 可确保始终使用最新的连接信息,而不是缓存了旧连接的实例。
无活跃连接时的约定:
所有适配层方法在 activeConnection == null 时统一抛出:
throw StateError('No active connection available. Cannot perform request.');import 'package:block_flutter/block_flutter.dart';
Future<void> example() async {
final connection = ConnectionModel(
name: '节点',
address: 'http://192.168.1.100:8080',
keyBase64: 'your-key==',
status: ConnectionStatus.connected,
);
final blockApi = BlockApi(connection: connection);
// 创建 Block
final saveResponse = await blockApi.saveBlock(data: {
'bid': 'abc123def456ghi789012',
'model': 'document',
'name': '我的文档',
'content': '正文',
'created_at': DateTime.now().toIso8601String(),
});
// 读取 Block
final getResponse = await blockApi.getBlock(bid: 'abc123def456ghi789012');
final block = BlockModel(data: getResponse);
print(block.title); // '我的文档'
// 查询节点签名
final nodeApi = NodeApi(connection: connection);
final signature = await nodeApi.getSignature();
}Future<BlockModel> fetchWithCache(BlockApi api, String bid) async {
final cache = BlockCache.instance;
// 先查缓存
final cached = await cache.get(bid);
if (cached != null) return cached;
// 缓存未命中,从网络获取
final response = await api.getBlock(bid: bid);
final block = BlockModel(data: response);
// 写入缓存
await cache.put(bid, block);
return block;
}如果需要调用 SDK 未封装的路由,直接使用 ApiClient:
final client = ApiClient(connection: connection);
final response = await client.postToBridge(
routing: '/custom/my/route',
data: {'key': 'value'},
protocol: 'cert',
timeout: 30,
);SDK 不捕获或包装异常,所有错误直接向上传播:
| 异常类型 | 触发场景 |
|---|---|
HttpException |
HTTP 状态码非 200 |
FormatException |
响应 JSON 解析失败 |
ArgumentError |
AES 解密失败(密钥错误、数据损坏) |
StateError |
适配层中无活跃连接(由应用层抛出) |
推荐的错误处理模式:
try {
final response = await blockApi.getBlock(bid: bid);
// 处理成功响应
} on StateError catch (e) {
// 无活跃连接,引导用户配置节点
showConnectionError(e.message);
} on HttpException catch (e) {
// 网络或服务端错误
showNetworkError(e.message);
} catch (e) {
// 其他错误(解密失败等)
showGenericError(e.toString());
}