Skip to content

源码: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',
                      ),
                    );
                  },
                ),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

站点构建时间:2026/8/24 23:43:17