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