724 lines
22 KiB
Dart
724 lines
22 KiB
Dart
import 'dart:async';
|
||
|
||
import 'package:flutter/material.dart';
|
||
import 'package:yx_tracking_flutter/yx_tracking_flutter.dart';
|
||
|
||
final AnalyticsConfig _defaultConfig = AnalyticsConfig(
|
||
systemCode: 'SDK-TEST-FLUTTER',
|
||
endpointBaseUrl: 'http://192.168.2.7:18828',
|
||
// clientType: 3, // Auto-detected
|
||
enableDebug: true,
|
||
batchSize: 30,
|
||
flushInterval: 30,
|
||
allowInsecureHttp: true,
|
||
);
|
||
|
||
Future<void> main() async {
|
||
WidgetsFlutterBinding.ensureInitialized();
|
||
|
||
await Analytics.init(_defaultConfig);
|
||
Analytics.bindLifecycleObserver();
|
||
|
||
runApp(const DemoApp());
|
||
}
|
||
|
||
class DemoApp extends StatelessWidget {
|
||
const DemoApp({super.key});
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return MaterialApp(
|
||
home: const DemoPage(),
|
||
theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
|
||
);
|
||
}
|
||
}
|
||
|
||
class DemoPage extends StatefulWidget {
|
||
const DemoPage({super.key});
|
||
|
||
@override
|
||
State<DemoPage> createState() => _DemoPageState();
|
||
}
|
||
|
||
class _DemoPageState extends State<DemoPage> {
|
||
int _cacheCount = 0;
|
||
List<RecentEventSummary> _recent = const <RecentEventSummary>[];
|
||
final List<String> _logs = <String>[];
|
||
final Set<String> _running = <String>{};
|
||
bool _verboseStressLogs = false;
|
||
bool _sdkDebugEnabled = true;
|
||
final TextEditingController _mockBaseUrlController = TextEditingController();
|
||
final ScrollController _logScrollController = ScrollController();
|
||
Timer? _pollTimer;
|
||
late AnalyticsConfig _activeConfig;
|
||
|
||
@override
|
||
void initState() {
|
||
super.initState();
|
||
_activeConfig = _defaultConfig;
|
||
_sdkDebugEnabled = _activeConfig.enableDebug;
|
||
_refreshCount();
|
||
_pollTimer = Timer.periodic(const Duration(seconds: 2), (_) {
|
||
_refreshCount();
|
||
});
|
||
}
|
||
|
||
@override
|
||
void dispose() {
|
||
_pollTimer?.cancel();
|
||
_mockBaseUrlController.dispose();
|
||
_logScrollController.dispose();
|
||
super.dispose();
|
||
}
|
||
|
||
bool _isRunning(String key) => _running.contains(key);
|
||
|
||
Future<void> _runAction(
|
||
String key,
|
||
String label,
|
||
Future<void> Function() action, {
|
||
bool suppressSdkLogs = false,
|
||
}) async {
|
||
if (_isRunning(key)) {
|
||
return;
|
||
}
|
||
setState(() {
|
||
_running.add(key);
|
||
});
|
||
final start = DateTime.now();
|
||
_addLog('▶ $label');
|
||
final previousDebug = _sdkDebugEnabled;
|
||
if (suppressSdkLogs) {
|
||
_setSdkDebug(false, reason: '压力测试静默');
|
||
}
|
||
try {
|
||
await action();
|
||
final elapsed = DateTime.now().difference(start).inMilliseconds;
|
||
_addLog('✅ $label (${elapsed}ms)');
|
||
} on Object catch (e) {
|
||
_addLog('❌ $label: $e');
|
||
} finally {
|
||
if (suppressSdkLogs) {
|
||
_setSdkDebug(previousDebug, reason: '恢复日志');
|
||
}
|
||
if (mounted) {
|
||
setState(() {
|
||
_running.remove(key);
|
||
});
|
||
}
|
||
await _refreshCount();
|
||
}
|
||
}
|
||
|
||
void _addLog(String message) {
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
final now = DateTime.now();
|
||
final ts =
|
||
'${now.hour.toString().padLeft(2, '0')}:${now.minute.toString().padLeft(2, '0')}:${now.second.toString().padLeft(2, '0')}';
|
||
setState(() {
|
||
_logs.add('[$ts] $message');
|
||
if (_logs.length > 200) {
|
||
_logs.removeRange(0, _logs.length - 200);
|
||
}
|
||
});
|
||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||
if (_logScrollController.hasClients) {
|
||
_logScrollController.jumpTo(
|
||
_logScrollController.position.maxScrollExtent,
|
||
);
|
||
}
|
||
});
|
||
}
|
||
|
||
Future<void> _refreshCount() async {
|
||
final results = await Future.wait(<Future<Object>>[
|
||
Analytics.cachedEventCount(),
|
||
Analytics.cachedRecentEvents(limit: 20),
|
||
]);
|
||
final count = results[0] as int;
|
||
final recent = results[1] as List<RecentEventSummary>;
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
setState(() {
|
||
_cacheCount = count;
|
||
_recent = recent;
|
||
});
|
||
}
|
||
|
||
Map<String, dynamic> _requiredParams({
|
||
required String page,
|
||
required String url,
|
||
required String buttonId,
|
||
Map<String, dynamic>? extra,
|
||
}) {
|
||
final params = <String, dynamic>{
|
||
'Page': page,
|
||
'Url': url,
|
||
'ButtonId': buttonId,
|
||
};
|
||
if (extra != null && extra.isNotEmpty) {
|
||
params.addAll(extra);
|
||
}
|
||
return params;
|
||
}
|
||
|
||
AnalyticsConfig _buildConfig({
|
||
String? endpointBaseUrl,
|
||
Duration? maxEventAge,
|
||
bool? allowInsecureHttp,
|
||
bool? useIsolateStorage,
|
||
Duration? connectTimeout,
|
||
Duration? readTimeout,
|
||
}) {
|
||
final base = _activeConfig;
|
||
return AnalyticsConfig(
|
||
systemCode: base.systemCode,
|
||
endpointBaseUrl: endpointBaseUrl ?? base.endpointBaseUrl,
|
||
clientType: base.clientType,
|
||
enableDebug: base.enableDebug,
|
||
batchSize: base.batchSize,
|
||
flushInterval: base.flushInterval,
|
||
maxCacheSize: base.maxCacheSize,
|
||
maxRetryCount: base.maxRetryCount,
|
||
connectTimeout: connectTimeout ?? base.connectTimeout,
|
||
readTimeout: readTimeout ?? base.readTimeout,
|
||
maxEventAge: maxEventAge ?? base.maxEventAge,
|
||
useIsolateStorage: useIsolateStorage ?? base.useIsolateStorage,
|
||
allowInsecureHttp: allowInsecureHttp ?? base.allowInsecureHttp,
|
||
enableMetrics: base.enableMetrics,
|
||
metricsReportInterval: base.metricsReportInterval,
|
||
blockOnValidationError: base.blockOnValidationError,
|
||
);
|
||
}
|
||
|
||
Future<void> _reinitialize(AnalyticsConfig config, {String? reason}) async {
|
||
await Analytics.init(config);
|
||
if (!mounted) {
|
||
return;
|
||
}
|
||
setState(() {
|
||
_activeConfig = config;
|
||
_sdkDebugEnabled = config.enableDebug;
|
||
});
|
||
if (reason != null && reason.isNotEmpty) {
|
||
_addLog('SDK 重新初始化: $reason');
|
||
}
|
||
}
|
||
|
||
void _setSdkDebug(bool enabled, {required String reason}) {
|
||
if (_sdkDebugEnabled == enabled) {
|
||
return;
|
||
}
|
||
Analytics.setDebug(enabled: enabled);
|
||
setState(() {
|
||
_sdkDebugEnabled = enabled;
|
||
});
|
||
_addLog('SDK 日志${enabled ? '开启' : '关闭'}($reason)');
|
||
}
|
||
|
||
Future<void> _trackDemoEvent() async {
|
||
await Analytics.track(
|
||
'DEMO_BUTTON_CLICK',
|
||
eventParams: _requiredParams(
|
||
page: 'demo',
|
||
url: 'https://example.com/demo',
|
||
buttonId: 'demo_btn_01',
|
||
),
|
||
customTags: const <String, dynamic>{'tenantId': 't1', 'feature': 'demo'},
|
||
);
|
||
}
|
||
|
||
Future<void> _flushNow() async {
|
||
await Analytics.flush(force: true);
|
||
}
|
||
|
||
Future<void> _refreshConfig() async {
|
||
await Analytics.refreshConfig(force: true);
|
||
}
|
||
|
||
Future<void> _runStressSequential() async {
|
||
for (var i = 0; i < 1000; i += 1) {
|
||
await Analytics.track(
|
||
'STRESS_SEQ',
|
||
eventParams: _requiredParams(
|
||
page: 'stress_seq',
|
||
url: 'https://example.com/stress',
|
||
buttonId: 'stress_seq',
|
||
extra: <String, dynamic>{'index': i},
|
||
),
|
||
customTags: const <String, dynamic>{'feature': 'stress_seq'},
|
||
);
|
||
}
|
||
}
|
||
|
||
Future<void> _runStressConcurrent() async {
|
||
Future<void> trackBatch(int batch, int count) async {
|
||
for (var i = 0; i < count; i += 1) {
|
||
await Analytics.track(
|
||
'STRESS_CONCURRENT',
|
||
eventParams: _requiredParams(
|
||
page: 'stress_concurrent',
|
||
url: 'https://example.com/stress',
|
||
buttonId: 'stress_concurrent',
|
||
extra: <String, dynamic>{'batch': batch, 'index': i},
|
||
),
|
||
customTags: const <String, dynamic>{'feature': 'stress_concurrent'},
|
||
);
|
||
}
|
||
}
|
||
|
||
final tasks = List<Future<void>>.generate(
|
||
10,
|
||
(index) => trackBatch(index, 100),
|
||
);
|
||
await Future.wait(tasks);
|
||
}
|
||
|
||
Future<void> _runContinuousTrackFlush() async {
|
||
final endAt = DateTime.now().add(const Duration(seconds: 30));
|
||
final trackTimer = Timer.periodic(const Duration(milliseconds: 200), (_) {
|
||
unawaited(
|
||
Analytics.track(
|
||
'CONTINUOUS_EVENT',
|
||
eventParams: _requiredParams(
|
||
page: 'continuous',
|
||
url: 'https://example.com/continuous',
|
||
buttonId: 'continuous',
|
||
extra: <String, dynamic>{
|
||
'ts': DateTime.now().millisecondsSinceEpoch,
|
||
},
|
||
),
|
||
customTags: const <String, dynamic>{'feature': 'continuous'},
|
||
),
|
||
);
|
||
});
|
||
final flushTimer = Timer.periodic(const Duration(seconds: 1), (_) {
|
||
unawaited(Analytics.flush(force: true));
|
||
});
|
||
|
||
while (DateTime.now().isBefore(endAt)) {
|
||
await Future<void>.delayed(const Duration(milliseconds: 500));
|
||
}
|
||
|
||
trackTimer.cancel();
|
||
flushTimer.cancel();
|
||
}
|
||
|
||
Future<void> _trackInvalidEvent() async {
|
||
await Analytics.track(
|
||
'DEMO_BUTTON_CLICK',
|
||
eventParams: _requiredParams(
|
||
page: 'demo_invalid',
|
||
url: 'https://example.com/invalid',
|
||
buttonId: 'demo_invalid',
|
||
),
|
||
customTags: const <String, dynamic>{'tenantId': 't1'},
|
||
);
|
||
}
|
||
|
||
Future<void> _simulateTimeout() async {
|
||
final original = _activeConfig;
|
||
final tempConfig = _buildConfig(
|
||
endpointBaseUrl: 'http://10.255.255.1:18828',
|
||
allowInsecureHttp: true,
|
||
connectTimeout: const Duration(seconds: 1),
|
||
readTimeout: const Duration(seconds: 1),
|
||
);
|
||
await _reinitialize(tempConfig, reason: '网络超时模拟');
|
||
await Analytics.track(
|
||
'EVENT_TIMEOUT_TEST',
|
||
eventParams: _requiredParams(
|
||
page: 'timeout',
|
||
url: 'https://example.com/timeout',
|
||
buttonId: 'timeout',
|
||
extra: const <String, dynamic>{'case': 'timeout'},
|
||
),
|
||
);
|
||
await Analytics.flush(force: true);
|
||
await _reinitialize(original, reason: '恢复正常配置');
|
||
}
|
||
|
||
Future<void> _simulateServerError() async {
|
||
final mock = _mockBaseUrlController.text.trim();
|
||
if (mock.isEmpty) {
|
||
_addLog('⚠️ 请先填写 5xx/Mock BaseUrl');
|
||
return;
|
||
}
|
||
final original = _activeConfig;
|
||
final tempConfig = _buildConfig(
|
||
endpointBaseUrl: mock,
|
||
allowInsecureHttp: mock.startsWith('http://'),
|
||
);
|
||
await _reinitialize(tempConfig, reason: '5xx 模拟');
|
||
await Analytics.track(
|
||
'EVENT_5XX_TEST',
|
||
eventParams: _requiredParams(
|
||
page: 'server_5xx',
|
||
url: 'https://example.com/5xx',
|
||
buttonId: 'server_5xx',
|
||
extra: const <String, dynamic>{'case': 'server_5xx'},
|
||
),
|
||
);
|
||
await Analytics.flush(force: true);
|
||
await _reinitialize(original, reason: '恢复正常配置');
|
||
}
|
||
|
||
Future<void> _trackEmptyParams() async {
|
||
await Analytics.track('EMPTY_PARAMS_EVENT');
|
||
}
|
||
|
||
Future<void> _trackLargePayload() async {
|
||
final payload = List<String>.filled(100 * 1024, 'a').join();
|
||
await Analytics.track(
|
||
'LARGE_PAYLOAD_EVENT',
|
||
eventParams: _requiredParams(
|
||
page: 'large_payload',
|
||
url: 'https://example.com/large',
|
||
buttonId: 'large_payload',
|
||
extra: <String, dynamic>{'payload': payload, 'size': payload.length},
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _trackSpecialChars() async {
|
||
await Analytics.track(
|
||
'SPECIAL_空格_😀_!@#',
|
||
eventParams: _requiredParams(
|
||
page: 'special_chars',
|
||
url: 'https://example.com/special',
|
||
buttonId: 'special_chars',
|
||
extra: const <String, dynamic>{'feature': 'special_chars'},
|
||
),
|
||
);
|
||
}
|
||
|
||
Future<void> _rapidInitDispose() async {
|
||
for (var i = 0; i < 5; i += 1) {
|
||
await Analytics.dispose();
|
||
await Analytics.init(_activeConfig);
|
||
}
|
||
}
|
||
|
||
Future<void> _testExpiration() async {
|
||
final original = _activeConfig;
|
||
final tempConfig = _buildConfig(maxEventAge: const Duration(seconds: 1));
|
||
await _reinitialize(tempConfig, reason: '过期清理测试');
|
||
await Analytics.track(
|
||
'EXPIRATION_EVENT',
|
||
eventParams: _requiredParams(
|
||
page: 'expiration',
|
||
url: 'https://example.com/expiration',
|
||
buttonId: 'expiration',
|
||
extra: const <String, dynamic>{'case': 'expiration'},
|
||
),
|
||
);
|
||
await Future<void>.delayed(const Duration(seconds: 2));
|
||
await Analytics.flush(force: true);
|
||
await _reinitialize(original, reason: '恢复正常配置');
|
||
}
|
||
|
||
Widget _buildSection(String title, List<Widget> children) {
|
||
return Card(
|
||
margin: const EdgeInsets.symmetric(vertical: 8),
|
||
child: Padding(
|
||
padding: const EdgeInsets.all(12),
|
||
child: Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: <Widget>[
|
||
Text(title, style: Theme.of(context).textTheme.titleMedium),
|
||
const SizedBox(height: 12),
|
||
...children,
|
||
],
|
||
),
|
||
),
|
||
);
|
||
}
|
||
|
||
Widget _buildConfigSummary() {
|
||
return Column(
|
||
crossAxisAlignment: CrossAxisAlignment.start,
|
||
children: <Widget>[
|
||
Text('Endpoint: ${_activeConfig.endpointBaseUrl}'),
|
||
Text('systemCode: ${_activeConfig.systemCode}'),
|
||
Text('allowInsecureHttp: ${_activeConfig.allowInsecureHttp}'),
|
||
Text('useIsolateStorage: ${_activeConfig.useIsolateStorage}'),
|
||
Text('maxEventAge: ${_activeConfig.maxEventAge.inSeconds}s'),
|
||
],
|
||
);
|
||
}
|
||
|
||
Widget _buildRecentList() {
|
||
if (_recent.isEmpty) {
|
||
return const Text('暂无事件');
|
||
}
|
||
return ListView.separated(
|
||
itemCount: _recent.length,
|
||
separatorBuilder: (_, __) => const Divider(height: 1),
|
||
itemBuilder: (context, index) {
|
||
final item = _recent[index];
|
||
return ListTile(
|
||
dense: true,
|
||
title: Text(item.eventType),
|
||
subtitle: Text(
|
||
'${item.createTime.toIso8601String()} · retry=${item.retryCount}',
|
||
),
|
||
);
|
||
},
|
||
);
|
||
}
|
||
|
||
Widget _buildLogList() {
|
||
if (_logs.isEmpty) {
|
||
return const Text('暂无日志');
|
||
}
|
||
return ListView.builder(
|
||
controller: _logScrollController,
|
||
itemCount: _logs.length,
|
||
itemBuilder: (context, index) => Text(_logs[index]),
|
||
);
|
||
}
|
||
|
||
@override
|
||
Widget build(BuildContext context) {
|
||
return Scaffold(
|
||
appBar: AppBar(title: const Text('YX Tracking Demo')),
|
||
body: ListView(
|
||
padding: const EdgeInsets.all(16),
|
||
children: <Widget>[
|
||
Text(
|
||
'本地缓存事件数:$_cacheCount',
|
||
style: Theme.of(context).textTheme.titleMedium,
|
||
),
|
||
const SizedBox(height: 8),
|
||
_buildSection('配置摘要', <Widget>[_buildConfigSummary()]),
|
||
_buildSection('基础操作', <Widget>[
|
||
Wrap(
|
||
spacing: 12,
|
||
runSpacing: 12,
|
||
children: <Widget>[
|
||
ElevatedButton(
|
||
onPressed: _isRunning('track_demo')
|
||
? null
|
||
: () => _runAction(
|
||
'track_demo',
|
||
'Track Demo Event',
|
||
_trackDemoEvent,
|
||
),
|
||
child: const Text('Track Demo Event'),
|
||
),
|
||
ElevatedButton(
|
||
onPressed: _isRunning('flush')
|
||
? null
|
||
: () => _runAction('flush', 'Flush Now', _flushNow),
|
||
child: Text(
|
||
_isRunning('flush') ? 'Flushing...' : 'Flush Now',
|
||
),
|
||
),
|
||
OutlinedButton(
|
||
onPressed: _isRunning('refresh_config')
|
||
? null
|
||
: () => _runAction(
|
||
'refresh_config',
|
||
'Refresh Config',
|
||
_refreshConfig,
|
||
),
|
||
child: Text(
|
||
_isRunning('refresh_config')
|
||
? 'Refreshing...'
|
||
: 'Refresh Config',
|
||
),
|
||
),
|
||
],
|
||
),
|
||
const SizedBox(height: 8),
|
||
const Text(
|
||
'说明:已对接 SDK-TEST-FLUTTER 系统;若为 HTTP 联调,请保持 allowInsecureHttp=true。',
|
||
),
|
||
]),
|
||
_buildSection('压力测试', <Widget>[
|
||
SwitchListTile(
|
||
contentPadding: EdgeInsets.zero,
|
||
title: const Text('压力测试详细日志'),
|
||
subtitle: const Text('关闭后仅输出结果与异常'),
|
||
value: _verboseStressLogs,
|
||
onChanged: (value) {
|
||
setState(() {
|
||
_verboseStressLogs = value;
|
||
});
|
||
},
|
||
),
|
||
Wrap(
|
||
spacing: 12,
|
||
runSpacing: 12,
|
||
children: <Widget>[
|
||
OutlinedButton(
|
||
onPressed: _isRunning('stress_seq')
|
||
? null
|
||
: () => _runAction(
|
||
'stress_seq',
|
||
'Track 1000 (Sequential)',
|
||
_runStressSequential,
|
||
suppressSdkLogs: !_verboseStressLogs,
|
||
),
|
||
child: Text(
|
||
_isRunning('stress_seq')
|
||
? 'Testing...'
|
||
: 'Track 1000 (Sequential)',
|
||
),
|
||
),
|
||
OutlinedButton(
|
||
onPressed: _isRunning('stress_con')
|
||
? null
|
||
: () => _runAction(
|
||
'stress_con',
|
||
'Track 1000 (Concurrent)',
|
||
_runStressConcurrent,
|
||
suppressSdkLogs: !_verboseStressLogs,
|
||
),
|
||
child: Text(
|
||
_isRunning('stress_con')
|
||
? 'Testing...'
|
||
: 'Track 1000 (Concurrent)',
|
||
),
|
||
),
|
||
OutlinedButton(
|
||
onPressed: _isRunning('continuous')
|
||
? null
|
||
: () => _runAction(
|
||
'continuous',
|
||
'Continuous Track + Flush',
|
||
_runContinuousTrackFlush,
|
||
suppressSdkLogs: !_verboseStressLogs,
|
||
),
|
||
child: Text(
|
||
_isRunning('continuous')
|
||
? 'Running...'
|
||
: 'Continuous Track + Flush (30s)',
|
||
),
|
||
),
|
||
],
|
||
),
|
||
]),
|
||
_buildSection('错误模拟', <Widget>[
|
||
TextField(
|
||
controller: _mockBaseUrlController,
|
||
decoration: const InputDecoration(
|
||
labelText: '5xx/Mock BaseUrl(可选)',
|
||
hintText: 'http://localhost:8080',
|
||
),
|
||
),
|
||
const SizedBox(height: 8),
|
||
Wrap(
|
||
spacing: 12,
|
||
runSpacing: 12,
|
||
children: <Widget>[
|
||
OutlinedButton(
|
||
onPressed: _isRunning('invalid_event')
|
||
? null
|
||
: () => _runAction(
|
||
'invalid_event',
|
||
'Invalid Event (Missing Tag)',
|
||
_trackInvalidEvent,
|
||
),
|
||
child: const Text('Invalid Event (Missing Tag)'),
|
||
),
|
||
OutlinedButton(
|
||
onPressed: _isRunning('timeout')
|
||
? null
|
||
: () => _runAction(
|
||
'timeout',
|
||
'Simulate Network Timeout',
|
||
_simulateTimeout,
|
||
),
|
||
child: const Text('Simulate Network Timeout'),
|
||
),
|
||
OutlinedButton(
|
||
onPressed: _isRunning('server_5xx')
|
||
? null
|
||
: () => _runAction(
|
||
'server_5xx',
|
||
'Simulate Server 5xx',
|
||
_simulateServerError,
|
||
),
|
||
child: const Text('Simulate Server 5xx'),
|
||
),
|
||
],
|
||
),
|
||
]),
|
||
_buildSection('边界用例', <Widget>[
|
||
Wrap(
|
||
spacing: 12,
|
||
runSpacing: 12,
|
||
children: <Widget>[
|
||
OutlinedButton(
|
||
onPressed: _isRunning('empty_params')
|
||
? null
|
||
: () => _runAction(
|
||
'empty_params',
|
||
'Empty Params',
|
||
_trackEmptyParams,
|
||
),
|
||
child: const Text('Empty Params'),
|
||
),
|
||
OutlinedButton(
|
||
onPressed: _isRunning('large_payload')
|
||
? null
|
||
: () => _runAction(
|
||
'large_payload',
|
||
'Large Payload (100KB)',
|
||
_trackLargePayload,
|
||
),
|
||
child: const Text('Large Payload (100KB)'),
|
||
),
|
||
OutlinedButton(
|
||
onPressed: _isRunning('special_chars')
|
||
? null
|
||
: () => _runAction(
|
||
'special_chars',
|
||
'Special Characters',
|
||
_trackSpecialChars,
|
||
),
|
||
child: const Text('Special Characters'),
|
||
),
|
||
OutlinedButton(
|
||
onPressed: _isRunning('rapid_init')
|
||
? null
|
||
: () => _runAction(
|
||
'rapid_init',
|
||
'Rapid Init/Dispose (x5)',
|
||
_rapidInitDispose,
|
||
),
|
||
child: const Text('Rapid Init/Dispose (x5)'),
|
||
),
|
||
OutlinedButton(
|
||
onPressed: _isRunning('expiration')
|
||
? null
|
||
: () => _runAction(
|
||
'expiration',
|
||
'Test Expiration (1s)',
|
||
_testExpiration,
|
||
),
|
||
child: const Text('Test Expiration (1s)'),
|
||
),
|
||
],
|
||
),
|
||
]),
|
||
_buildSection('状态与日志', <Widget>[
|
||
const Text('最近事件(最多 20 条)'),
|
||
const SizedBox(height: 8),
|
||
SizedBox(height: 220, child: _buildRecentList()),
|
||
const SizedBox(height: 12),
|
||
const Text('日志输出'),
|
||
const SizedBox(height: 8),
|
||
SizedBox(height: 160, child: _buildLogList()),
|
||
]),
|
||
],
|
||
),
|
||
);
|
||
}
|
||
}
|