Skip to content

源码:labs/flutter/host/lib/topics/background_tasks/flutter_background_boundary/background_boundary_demo_page.dart ​

  • 原始路径:labs/flutter/host/lib/topics/background_tasks/flutter_background_boundary/background_boundary_demo_page.dart
  • 类型:dart
dart
import 'dart:async';

import 'package:flutter/material.dart';

/// ============================================================================
/// Flutter 后台边界 Demo
///
/// 对应 docs/03-background-tasks/02-flutter-background-boundary.md
/// 验证点:
/// 1. WidgetsBindingObserver 能收到 App 前后台信号(inactive / paused / resumed)
/// 2. App 进入 paused 后,Dart 定时器被冻结 —— 「Dart 任务还活着」≠「还在执行」
/// 3. 回到 resumed 时用「墙钟时间 vs 实际 tick 次数」量化错过的时间片
///
/// 结论:Flutter 层只能感知生命周期,不能凭 Dart 代码获得后台执行窗口。
/// ============================================================================

class FlutterBackgroundBoundaryDemoPage extends StatefulWidget {
  const FlutterBackgroundBoundaryDemoPage({super.key});

  static const routeName = '/flutter_background_boundary';

  @override
  State<FlutterBackgroundBoundaryDemoPage> createState() =>
      _FlutterBackgroundBoundaryDemoPageState();
}

class _FlutterBackgroundBoundaryDemoPageState
    extends State<FlutterBackgroundBoundaryDemoPage>
    with WidgetsBindingObserver {
  final List<String> _logs = <String>[];

  Timer? _ticker;
  int _tickCount = 0;
  DateTime? _taskStartedAt;
  bool get _isTaskRunning => _ticker != null;

  void _log(String message) {
    setState(() {
      _logs.insert(0, '[${_now()}] $message');
    });
  }

  static String _now() {
    final t = DateTime.now();
    return '${t.hour.toString().padLeft(2, '0')}:'
        '${t.minute.toString().padLeft(2, '0')}:'
        '${t.second.toString().padLeft(2, '0')}';
  }

  @override
  void initState() {
    super.initState();
    // 必须先注册 observer,didChangeAppLifecycleState 才会被回调
    WidgetsBinding.instance.addObserver(this);
    _log('initState:已注册 WidgetsBindingObserver');
  }

  @override
  void didChangeAppLifecycleState(AppLifecycleState state) {
    switch (state) {
      case AppLifecycleState.resumed:
        _log('AppLifecycleState.resumed:回前台');
        _reportMissedTicks();
        break;
      case AppLifecycleState.inactive:
        _log('AppLifecycleState.inactive:失去焦点(过渡态)');
        break;
      case AppLifecycleState.paused:
        _log('AppLifecycleState.paused:进后台 → Dart 定时器即将被冻结');
        break;
      case AppLifecycleState.detached:
        _log('AppLifecycleState.detached:应用即将终止(部分平台才会出现)');
        break;
      case AppLifecycleState.hidden:
        _log('AppLifecycleState.hidden:所有视图隐藏(paused 的前置状态)');
        break;
    }
  }

  /// 用墙钟时间与实际 tick 数的差值,量化后台期间被冻结的执行时间。
  ///
  /// 如果 Dart 任务真的能在后台继续跑,missed 应该恒为 0;
  /// 真机上 missed ≈ 后台停留秒数 —— 这就是「isolate 不等于保活」的可观测证据。
  void _reportMissedTicks() {
    final startedAt = _taskStartedAt;
    if (startedAt == null || !_isTaskRunning) return;

    final wallSeconds = DateTime.now().difference(startedAt).inSeconds;
    final missed = wallSeconds - _tickCount;
    if (missed > 1) {
      _log('量化:墙钟走了约 $wallSeconds 秒,任务只执行了 $_tickCount 次,'
          '约 $missed 秒被系统挂起吞掉');
    } else {
      _log('量化:前台运行正常,未发现明显挂起(wall≈$wallSeconds, ticks=$_tickCount)');
    }
    _taskStartedAt = DateTime.now();
    _tickCount = 0;
  }

  void _startTask() {
    if (_isTaskRunning) return;
    _taskStartedAt = DateTime.now();
    _tickCount = 0;
    // Timer.periodic 是典型的「Dart 运行时任务」:前台每秒触发一次,
    // 进 paused 后不再触发 —— 它不会变成后台服务。
    _ticker = Timer.periodic(const Duration(seconds: 1), (_) {
      _tickCount++;
      _log('模拟同步任务 tick #$_tickCount');
    });
    _log('已启动模拟后台同步任务(Timer.periodic 1s)');
  }

  void _stopTask() {
    _ticker?.cancel();
    _ticker = null;
    _log('已停止模拟任务');
  }

  @override
  void dispose() {
    WidgetsBinding.instance.removeObserver(this);
    _ticker?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('Flutter 后台边界演示')),
      body: Padding(
        padding: const EdgeInsets.all(16),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: <Widget>[
            Text(
              '验证目标:App 切后台后,Dart 定时器是否还在跑?',
              style: Theme.of(context).textTheme.titleMedium,
            ),
            const SizedBox(height: 8),
            const Text(
              '操作步骤:\n'
              '1. 点击「启动模拟后台任务」,确认日志每秒追加一条 tick\n'
              '2. 把 App 切到后台(Home 键 / 切换应用),停留 5~10 秒\n'
              '3. 回到前台,对照「墙钟秒数」与「tick 次数」的差值',
            ),
            const SizedBox(height: 12),
            Wrap(
              spacing: 12,
              runSpacing: 12,
              children: <Widget>[
                ElevatedButton(
                  onPressed: _isTaskRunning ? null : _startTask,
                  child: const Text('启动模拟后台任务'),
                ),
                OutlinedButton(
                  onPressed: _isTaskRunning ? _stopTask : null,
                  child: const Text('停止任务'),
                ),
                Chip(
                  label: Text(_isTaskRunning
                      ? '任务运行中 (ticks=$_tickCount)'
                      : '任务未启动'),
                ),
              ],
            ),
            const SizedBox(height: 16),
            const Text(
              '预期现象:切后台期间没有任何新 tick;回到前台后 resumed 日志里'
              '「被挂起吞掉的秒数」约等于你在后台停留的时长。\n'
              '结论:Dart 代码不能给自己争取后台执行窗口,可靠后台要落到原生能力。',
              style: TextStyle(fontSize: 12),
            ),
            const SizedBox(height: 16),
            const Text(
              '事件日志 (最新在前)',
              style: TextStyle(fontWeight: FontWeight.bold),
            ),
            const SizedBox(height: 8),
            Expanded(
              child: ListView.builder(
                itemCount: _logs.length,
                itemBuilder: (context, index) => Text('• ${_logs[index]}'),
              ),
            ),
          ],
        ),
      ),
    );
  }
}

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