Appearance
源码:labs/flutter/host/lib/topics/backend_data/download_service_update_case_demo.dart
- 原始路径:
labs/flutter/host/lib/topics/backend_data/download_service_update_case_demo.dart - 类型:
dart
dart
import 'dart:math';
import 'package:flutter/material.dart';
enum DownloadStage {
idle,
fetchingMetadata,
downloading,
verifying,
promoting,
completed,
failed,
}
class DownloadServiceUpdateCaseDemoPage extends StatefulWidget {
const DownloadServiceUpdateCaseDemoPage({super.key});
static const routeName = '/backend_data/download_service_update_case';
@override
State<DownloadServiceUpdateCaseDemoPage> createState() =>
_DownloadServiceUpdateCaseDemoPageState();
}
class _DownloadServiceUpdateCaseDemoPageState
extends State<DownloadServiceUpdateCaseDemoPage> {
DownloadStage _stage = DownloadStage.idle;
final List<String> _logs = <String>['[idle] 当前 activeVersion=v41'];
int _activeVersion = 41;
int _remoteVersion = 42;
bool _hashMatches = true;
bool _promotionSucceeds = true;
double _progress = 0;
void _log(String message) {
if (!mounted) return;
setState(() {
_logs.add(message);
});
}
Future<void> _runUpdate() async {
setState(() {
_stage = DownloadStage.fetchingMetadata;
_progress = 0;
});
_log('[fetchingMetadata] 拉取远端元信息: version=v$_remoteVersion, sha256=ok');
await Future<void>.delayed(const Duration(milliseconds: 250));
if (!mounted) return;
setState(() {
_stage = DownloadStage.downloading;
});
for (var step = 1; step <= 4; step++) {
await Future<void>.delayed(const Duration(milliseconds: 200));
if (!mounted) return;
setState(() {
_progress = step / 4;
});
_log('[downloading] staging/bundle_v$_remoteVersion.zip.part ${(100 * _progress).round()}%');
}
setState(() {
_stage = DownloadStage.verifying;
});
_log('[verifying] 校验 size / sha256 / manifest');
await Future<void>.delayed(const Duration(milliseconds: 300));
if (!mounted) return;
if (!_hashMatches) {
setState(() {
_stage = DownloadStage.failed;
});
_log('[failed] hashMismatch:删除 staging,activeVersion 仍保持 v$_activeVersion');
return;
}
setState(() {
_stage = DownloadStage.promoting;
});
_log('[promoting] 准备把 staging_v$_remoteVersion 原子切换为 active');
await Future<void>.delayed(const Duration(milliseconds: 300));
if (!mounted) return;
if (!_promotionSucceeds) {
setState(() {
_stage = DownloadStage.failed;
});
_log('[failed] promotionFailure:指针未切换,继续回退到 activeVersion=v$_activeVersion');
return;
}
setState(() {
_activeVersion = _remoteVersion;
_stage = DownloadStage.completed;
});
_log('[completed] current_version -> v$_activeVersion,旧版本进入 backup');
}
void _bumpRemoteVersion() {
setState(() {
_remoteVersion += 1 + Random().nextInt(2);
_logs.add('[server] 远端版本更新为 v$_remoteVersion');
});
}
void _reset() {
setState(() {
_stage = DownloadStage.idle;
_activeVersion = 41;
_remoteVersion = 42;
_hashMatches = true;
_promotionSucceeds = true;
_progress = 0;
_logs
..clear()
..add('[idle] 当前 activeVersion=v41');
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('download_service 更新闭环')),
body: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text('activeVersion=v$_activeVersion / remoteVersion=v$_remoteVersion'),
const SizedBox(height: 8),
LinearProgressIndicator(value: _stage == DownloadStage.downloading ? _progress : null),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 8,
children: [
FilterChip(
label: const Text('hash 校验通过'),
selected: _hashMatches,
onSelected: (value) => setState(() => _hashMatches = value),
),
FilterChip(
label: const Text('原子切换成功'),
selected: _promotionSucceeds,
onSelected: (value) => setState(() => _promotionSucceeds = value),
),
],
),
const SizedBox(height: 12),
Wrap(
spacing: 8,
runSpacing: 8,
children: [
ElevatedButton(onPressed: _runUpdate, child: const Text('执行更新')),
OutlinedButton(onPressed: _bumpRemoteVersion, child: const Text('提升远端版本')),
TextButton(onPressed: _reset, child: const Text('重置')),
],
),
const SizedBox(height: 12),
Expanded(
child: ListView(
children: [
Card(
child: Column(
children: DownloadStage.values
.map(
(stage) => ListTile(
dense: true,
leading: Icon(
stage == _stage ? Icons.play_circle_fill : Icons.check_circle_outline,
),
title: Text(stage.name),
subtitle: Text(_labelFor(stage)),
),
)
.toList(),
),
),
const SizedBox(height: 12),
Card(
color: Colors.black,
child: ListView.builder(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
padding: const EdgeInsets.all(12),
itemCount: _logs.length,
itemBuilder: (context, index) => Text(
_logs[index],
style: const TextStyle(
color: Colors.greenAccent,
fontFamily: 'monospace',
fontSize: 12,
),
),
),
),
],
),
),
Text(
'观察重点:下载完成不等于更新成功,只有 verify + promote 都通过,activeVersion 才能切换。',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
);
}
String _labelFor(DownloadStage stage) {
return switch (stage) {
DownloadStage.idle => '当前未在更新',
DownloadStage.fetchingMetadata => '先拉 version/size/hash',
DownloadStage.downloading => '只写 staging/.part,不碰 active',
DownloadStage.verifying => '校验长度、sha256、manifest',
DownloadStage.promoting => '原子替换 active 目录与版本指针',
DownloadStage.completed => '新版本已生效,旧版本进入 backup',
DownloadStage.failed => '失败后保留旧版本继续服务',
};
}
}