Appearance
源码:labs/flutter/host/lib/topics/backend_data/wal_offline_sync_demo.dart
- 原始路径:
labs/flutter/host/lib/topics/backend_data/wal_offline_sync_demo.dart - 类型:
dart
dart
import 'dart:async';
import 'dart:math';
import 'package:flutter/material.dart';
import '../../l10n/l10n.dart';
/// 移动弱网长连接保活、指数退避与 WAL 离线同步验证 Lab
///
/// 对应文档:docs/06-backend-data/08-weak-network-websocket-grpc-reconnect.md
class WalLogEntry {
final String clientMsgId;
final String content;
final DateTime createdAt;
bool isSynced;
int retryCount;
WalLogEntry({
required this.clientMsgId,
required this.content,
required this.createdAt,
this.isSynced = false,
this.retryCount = 0,
});
}
class WalOfflineSyncDemoScreen extends StatefulWidget {
const WalOfflineSyncDemoScreen({super.key});
@override
State<WalOfflineSyncDemoScreen> createState() =>
_WalOfflineSyncDemoScreenState();
}
class _WalOfflineSyncDemoScreenState extends State<WalOfflineSyncDemoScreen> {
final List<WalLogEntry> _walQueue = [];
final List<String> _debugLogs = [];
bool _initLogAdded = false;
bool _isNetworkOnline = false; // 初始设为无网弱网状态
bool _isSyncing = false;
int _consecutiveFailures = 0;
Timer? _reconnectTimer;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (!_initLogAdded) {
_initLogAdded = true;
_debugLogs.add(context.l10n.walLogInit);
}
}
@override
void dispose() {
_reconnectTimer?.cancel();
super.dispose();
}
// 1. 用户点击发消息:写前日志 (WAL) + 乐观 UI
void _onSubmitUserAction(String text) {
final clientMsgId =
"msg_${DateTime.now().millisecondsSinceEpoch}_${Random().nextInt(999)}";
final entry = WalLogEntry(
clientMsgId: clientMsgId,
content: text,
createdAt: DateTime.now(),
);
setState(() {
_walQueue.add(entry);
_debugLogs.add(context.l10n.walLogWrite(text, clientMsgId));
});
// 如果处于联网状态,立即尝试发包
if (_isNetworkOnline) {
_triggerSyncQueue();
} else {
setState(() {
_debugLogs.add(context.l10n.walLogOfflineWarning);
});
}
}
// 2. 模拟网络开关切换
void _toggleNetwork(bool value) {
setState(() {
_isNetworkOnline = value;
_debugLogs.add(
_isNetworkOnline
? context.l10n.walLogNetworkOnline
: context.l10n.walLogNetworkOffline,
);
});
if (_isNetworkOnline) {
_consecutiveFailures = 0;
_scheduleReconnectWithJitter();
} else {
_reconnectTimer?.cancel();
}
}
// 3. 指数退避加随机抖动 (Exponential Backoff with Jitter) 调度算法
void _scheduleReconnectWithJitter() {
_reconnectTimer?.cancel();
// Base 1s, max 16s
final baseDelayMs = min(16000, 1000 * pow(2, _consecutiveFailures).toInt());
// Jitter 0~500ms 随机抖动打散并发
final jitterMs = Random().nextInt(500);
final totalDelayMs = baseDelayMs + jitterMs;
setState(() {
_debugLogs.add(
context.l10n.walLogBackoff(totalDelayMs, baseDelayMs, jitterMs),
);
});
_reconnectTimer = Timer(Duration(milliseconds: totalDelayMs), () {
_triggerSyncQueue();
});
}
// 4. 批量拉取同步
Future<void> _triggerSyncQueue() async {
if (_isSyncing || !_isNetworkOnline) return;
final pendingItems = _walQueue.where((e) => !e.isSynced).toList();
if (pendingItems.isEmpty) return;
setState(() {
_isSyncing = true;
_debugLogs.add(context.l10n.walLogSyncStart(pendingItems.length));
});
// 模拟 RPC 耗时
await Future.delayed(const Duration(milliseconds: 800));
// 异步间隙保护:等待期间页面可能已销毁(如退避 Timer 在 pop 后触发),
// 避免对已 dispose 的 State 调用 setState
if (!mounted) return;
if (_isNetworkOnline) {
// 成功同步
setState(() {
for (var item in pendingItems) {
item.isSynced = true;
}
_isSyncing = false;
_consecutiveFailures = 0;
_debugLogs.add(context.l10n.walLogSyncSuccess);
});
} else {
// 同步失败,累加重试与退避
setState(() {
_isSyncing = false;
_consecutiveFailures++;
_debugLogs.add(context.l10n.walLogSyncFail(_consecutiveFailures));
});
_scheduleReconnectWithJitter();
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: Text(context.l10n.walAppBarTitle)),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// 模拟开关
SwitchListTile(
title: Text(context.l10n.walNetworkSwitch),
subtitle: Text(
_isNetworkOnline
? context.l10n.walNetworkOnline
: context.l10n.walNetworkOffline,
),
value: _isNetworkOnline,
onChanged: _toggleNetwork,
),
const Divider(),
Row(
children: [
Expanded(
child: ElevatedButton.icon(
icon: const Icon(Icons.send),
label: Text(context.l10n.walBtnSubmit),
onPressed: () => _onSubmitUserAction(
"Order_Pay_Req_${Random().nextInt(99)}",
),
),
),
const SizedBox(width: 8),
OutlinedButton(
onPressed: _triggerSyncQueue,
child: Text(context.l10n.walBtnManualSync),
),
],
),
const SizedBox(height: 12),
// WAL 队列视图
Align(
alignment: Alignment.centerLeft,
child: Text(
context.l10n.walQueueTitle,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 6),
SizedBox(
height: 120,
child: ListView.builder(
itemCount: _walQueue.length,
itemBuilder: (context, index) {
final item = _walQueue[index];
return Card(
color: item.isSynced
? Colors.green.shade50
: Colors.orange.shade50,
child: ListTile(
dense: true,
title: Text(item.content),
subtitle: Text(
context.l10n.walClientUuid(item.clientMsgId),
),
trailing: Chip(
label: Text(
item.isSynced
? context.l10n.walStatusAck
: context.l10n.walStatusPending,
style: const TextStyle(fontSize: 10),
),
backgroundColor: item.isSynced
? Colors.green.shade200
: Colors.orange.shade200,
),
),
);
},
),
),
const SizedBox(height: 12),
// 调试日志
Align(
alignment: Alignment.centerLeft,
child: Text(
context.l10n.walLogTitle,
style: const TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 6),
Expanded(
child: Container(
padding: const EdgeInsets.all(8),
color: Colors.black87,
child: ListView.builder(
itemCount: _debugLogs.length,
itemBuilder: (context, index) {
return Text(
_debugLogs[index],
style: const TextStyle(
color: Colors.greenAccent,
fontSize: 11,
fontFamily: 'monospace',
),
);
},
),
),
),
],
),
),
);
}
}