pd_develop_kit 0.2.0
pd_develop_kit: ^0.2.0 copied to clipboard
跨平台 Flutter 开发工具包插件,提供网络抽象、校验工具、深度比较、扩展与 UI 能力。
example/lib/main.dart
import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:pd_develop_kit/pd_develop_kit.dart';
void main() {
runApp(const PDDevelopKitExampleApp());
}
class PDDevelopKitExampleApp extends StatefulWidget {
const PDDevelopKitExampleApp({super.key});
@override
State<PDDevelopKitExampleApp> createState() => _PDDevelopKitExampleAppState();
}
class _PDDevelopKitExampleAppState extends State<PDDevelopKitExampleApp> {
static const String _unknownPlatformVersion = 'Unknown platform version';
String _platformVersion = _unknownPlatformVersion;
final PDDevelopKit _pdDevelopKitPlugin = PDDevelopKit();
late final PDApiClient _demoClient;
late final DemoLogger _logger;
@override
void initState() {
super.initState();
_logger = DemoLogger();
_configureDemoNetwork();
_demoClient = PDNetwork.client;
initPlatformState();
}
Future<void> initPlatformState() async {
String platformVersion;
try {
platformVersion = await _pdDevelopKitPlugin.getPlatformVersion() ?? _unknownPlatformVersion;
} catch (_) {
platformVersion = 'Failed to get platform version.';
}
if (!mounted) {
return;
}
setState(() {
_platformVersion = platformVersion;
});
}
void _configureDemoNetwork() {
PDNetwork.configure(
transport: _DemoTransport(
onSend: ({required request, cancelToken}) async {
await Future<void>.delayed(const Duration(milliseconds: 250));
switch (request.path) {
case '/demo/success':
return PDTransportResponse(
statusCode: 200,
data: <String, dynamic>{
'code': 200,
'success': true,
'msg': '查询成功',
'data': <String, dynamic>{
'userName': 'Pedro',
'role': 'maintainer',
'from': 'standard-envelope',
'baseUrl': request.config.baseUrl,
},
},
);
case '/demo/legacy-success':
return const PDTransportResponse(
statusCode: 200,
data: <String, dynamic>{
'code': 200,
'msg': '旧接口也能正常解析',
'data': <String, dynamic>{'enabled': true, 'from': 'legacy-envelope'},
},
);
case '/demo/business-error':
return const PDTransportResponse(
statusCode: 200,
data: <String, dynamic>{'code': 403, 'success': false, 'msg': '没有访问权限', 'data': null},
);
case '/demo/server-error':
return const PDTransportResponse(statusCode: 500, data: <String, dynamic>{'msg': '服务器异常,请稍后重试'});
case '/demo/timeout':
throw const PDTransportException(
type: PDTransportErrorType.connectionTimeout,
message: 'Connection timeout',
);
case '/demo/mapper-error':
return const PDTransportResponse(
statusCode: 200,
data: <String, dynamic>{
'code': 200,
'success': true,
'msg': '映射前响应正常',
'data': <String, dynamic>{'value': 'unexpected'},
},
);
case '/demo/encrypted-data-field':
return const PDTransportResponse(
statusCode: 200,
data: <String, dynamic>{
'code': 200,
'success': true,
'msg': 'data 字段已加密(模拟)',
'data': 'ENC:{"value":123,"note":"decoded by responseDecoder"}',
},
);
case '/demo/encrypted-request-body':
return PDTransportResponse(
statusCode: 200,
data: <String, dynamic>{
'code': 200,
'success': true,
'msg': '请求体已加密(模拟)',
'data': <String, dynamic>{'receivedBody': request.body},
},
);
case '/demo/custom-encoder':
return PDTransportResponse(
statusCode: 200,
data: <String, dynamic>{
'code': 200,
'success': true,
'msg': '自定义编码器演示',
'data': <String, dynamic>{'receivedBody': request.body, 'receivedQuery': request.queryParameters},
},
);
case '/demo/cancel-demo':
await Future<void>.delayed(const Duration(seconds: 5));
return PDTransportResponse(
statusCode: 200,
data: <String, dynamic>{'code': 200, 'success': true, 'msg': '请求未被取消', 'data': null},
);
default:
return const PDTransportResponse(statusCode: 404, data: <String, dynamic>{'msg': '未找到示例路由'});
}
},
),
config: PDNetworkConfig(
baseUrl: 'https://demo.pd-develop-kit.local',
enableLog: true,
defaultErrorMessage: '请求失败,请稍后重试。',
logger: _logger,
),
);
}
@override
Widget build(BuildContext context) {
return MaterialApp(
theme: ThemeData(colorSchemeSeed: Colors.blue, useMaterial3: true),
home: PDExampleHomePage(platformVersion: _platformVersion, client: _demoClient, logger: _logger),
);
}
}
class PDExampleHomePage extends StatelessWidget {
const PDExampleHomePage({required this.platformVersion, required this.client, required this.logger, super.key});
final String platformVersion;
final PDApiClient client;
final DemoLogger logger;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('PD Develop Kit Example')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
_SectionCard(
title: '平台能力',
child: Text('Running on: $platformVersion', key: const Key('platform-version-text')),
),
const SizedBox(height: 16),
_SectionCard(
title: '示例导航',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
FilledButton.tonal(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => PDNetworkDemoPage(client: client, logger: logger),
),
);
},
child: const Text('网络层(PDNetwork / PDApiClient)'),
),
const SizedBox(height: 12),
FilledButton.tonal(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute<void>(
builder: (_) => PDLongLinkDemoPage(networkConfig: PDNetwork.client.config),
),
);
},
child: const Text('长连接(PDLongLinkClient)'),
),
const SizedBox(height: 12),
FilledButton.tonal(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const PDExtensionsDemoPage()));
},
child: const Text('扩展能力(adapt / color / format 等)'),
),
const SizedBox(height: 12),
FilledButton.tonal(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const PDResponsiveUiDemoPage()));
},
child: const Text('UI 响应式(PDResponsiveBuilder / Grid)'),
),
const SizedBox(height: 12),
FilledButton.tonal(
onPressed: () {
Navigator.of(
context,
).push(MaterialPageRoute<void>(builder: (_) => const PDAdvancedNetworkDemoPage()));
},
child: const Text('网络层进阶(自定义编解码 / 取消请求)'),
),
const SizedBox(height: 12),
FilledButton.tonal(
onPressed: () {
Navigator.of(
context,
).push(MaterialPageRoute<void>(builder: (_) => const PDAdvancedLongLinkDemoPage()));
},
child: const Text('长连接进阶(完整配置 / 消息处理)'),
),
const SizedBox(height: 12),
FilledButton.tonal(
onPressed: () {
Navigator.of(
context,
).push(MaterialPageRoute<void>(builder: (_) => const PDAdvancedExtensionsDemoPage()));
},
child: const Text('扩展能力进阶(便捷缩放 / 自定义断点)'),
),
const SizedBox(height: 12),
FilledButton.tonal(
onPressed: () {
Navigator.of(context).push(MaterialPageRoute<void>(builder: (_) => const PDAdvancedUiDemoPage()));
},
child: const Text('UI 响应式进阶(自定义断点 / 回退策略)'),
),
const SizedBox(height: 12),
FilledButton.tonal(
onPressed: () {
Navigator.of(
context,
).push(MaterialPageRoute<void>(builder: (_) => const PDAdvancedUtilsDemoPage()));
},
child: const Text('工具类进阶(自定义密码规则)'),
),
],
),
),
],
),
);
}
}
class PDNetworkDemoPage extends StatefulWidget {
const PDNetworkDemoPage({required this.client, required this.logger, super.key});
final PDApiClient client;
final DemoLogger logger;
@override
State<PDNetworkDemoPage> createState() => _PDNetworkDemoPageState();
}
class _PDNetworkDemoPageState extends State<PDNetworkDemoPage> {
DemoResult? _lastResult;
String _lastNetworkLog = '';
@override
void initState() {
super.initState();
widget.logger.lastLog.addListener(_onLogChanged);
_lastNetworkLog = widget.logger.lastLog.value;
}
@override
void dispose() {
widget.logger.lastLog.removeListener(_onLogChanged);
super.dispose();
}
void _onLogChanged() {
if (!mounted) {
return;
}
setState(() {
_lastNetworkLog = widget.logger.lastLog.value;
});
}
Future<void> _runScenario(_DemoScenario scenario) async {
final BaseResponse<dynamic> response;
switch (scenario) {
case _DemoScenario.success:
response = await widget.client.get<Map<String, dynamic>>('/demo/success');
case _DemoScenario.legacySuccess:
response = await widget.client.get<Map<String, dynamic>>('/demo/legacy-success');
case _DemoScenario.businessError:
response = await widget.client.get<dynamic>('/demo/business-error');
case _DemoScenario.serverError:
response = await widget.client.get<dynamic>('/demo/server-error');
case _DemoScenario.timeout:
response = await widget.client.get<dynamic>('/demo/timeout');
case _DemoScenario.mapperError:
response = await widget.client.get<int>(
'/demo/mapper-error',
mapper: (_) {
throw const FormatException('demo mapper error');
},
);
case _DemoScenario.perRequestOverride:
response = await widget.client.get<Map<String, dynamic>>(
'/demo/success',
requestConfig: const PDRequestConfig(
baseUrl: 'https://override.example.com',
headers: <String, dynamic>{'X-From': 'requestConfig'},
enableLog: true,
),
);
case _DemoScenario.decryptDataField:
response = await widget.client.get<Map<String, dynamic>>(
'/demo/encrypted-data-field',
requestConfig: PDRequestConfig(
responseDecoder: PDAesResponseDecoder(
decrypt: (cipher) => cipher.replaceFirst('ENC:', ''),
mode: PDDecryptMode.dataFieldOnly,
),
),
);
case _DemoScenario.encryptRequestBody:
response = await widget.client.post<Map<String, dynamic>>(
'/demo/encrypted-request-body',
data: <String, dynamic>{'a': 1, 'b': true},
requestConfig: PDRequestConfig(
requestEncoder: PDAesRequestEncoder(encrypt: (plain) => 'ENC:$plain', bodyCipherField: 'cipherText'),
),
);
}
if (!mounted) {
return;
}
setState(() {
_lastResult = DemoResult(title: scenario.title, description: scenario.description, response: response);
});
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('网络层示例')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
const _SectionCard(
title: '说明',
child: SelectableText(
'这个页面使用 PDNetwork.configure + PDApiClient + 模拟 PDTransport 来演示网络层用法。\n'
'重点验证:无论成功、业务失败、服务器错误还是异常,调用方最终拿到的都是 BaseResponse。',
),
),
const SizedBox(height: 16),
_SectionCard(
title: '初始化示例',
child: SelectableText(
[
"PDNetwork.configure(\n",
" config: const PDNetworkConfig(\n",
" baseUrl: 'https://api.example.com',\n",
" headers: <String, dynamic>{'Accept': 'application/json'},\n",
" enableLog: true,\n",
" ),\n",
");\n\n",
"final client = PDNetwork.client;\n",
"final BaseResponse<Map<String, dynamic>> response =\n",
" await client.get<Map<String, dynamic>>('/user/info');\n\n",
"final overrideResp = await client.get<Map<String, dynamic>>(\n",
" '/user/info',\n",
" requestConfig: const PDRequestConfig(\n",
" headers: <String, dynamic>{'X-Debug': '1'},\n",
" enableLog: false,\n",
" ),\n",
");",
].join(),
key: const Key('network-code-snippet'),
),
),
const SizedBox(height: 16),
_SectionCard(
title: '场景演示',
child: Wrap(
spacing: 12,
runSpacing: 12,
children: _DemoScenario.values.map((scenario) {
return FilledButton.tonal(
key: Key('scenario-${scenario.name}'),
onPressed: () => _runScenario(scenario),
child: Text(scenario.title),
);
}).toList(),
),
),
const SizedBox(height: 16),
_SectionCard(
title: '最近一次响应',
child: _ResultView(result: _lastResult),
),
const SizedBox(height: 16),
_SectionCard(
title: '最近一次网络日志(示例 logger)',
child: SelectableText(
_lastNetworkLog.isEmpty ? '(empty)' : _lastNetworkLog,
key: const Key('network-last-log'),
),
),
],
),
);
}
}
class PDLongLinkDemoPage extends StatefulWidget {
const PDLongLinkDemoPage({required this.networkConfig, super.key});
final PDNetworkConfig networkConfig;
@override
State<PDLongLinkDemoPage> createState() => _PDLongLinkDemoPageState();
}
class _PDLongLinkDemoPageState extends State<PDLongLinkDemoPage> {
final TextEditingController _urlController = TextEditingController(text: 'wss://echo.websocket.events');
final TextEditingController _messageController = TextEditingController(text: '{"type":"chat","text":"hello"}');
PDLongLinkClient? _client;
StreamSubscription<PDLongLinkEvent>? _sub;
final List<String> _logs = <String>[];
PDLongLinkTransportMode _mode = PDLongLinkTransportMode.auto;
@override
void dispose() {
_sub?.cancel();
_client?.dispose();
_urlController.dispose();
_messageController.dispose();
super.dispose();
}
void _appendLog(String line) {
if (!mounted) {
return;
}
setState(() {
_logs.insert(0, '${DateTime.now().toString().substring(11, 19)} $line');
if (_logs.length > 200) {
_logs.removeRange(200, _logs.length);
}
});
}
Future<void> _connect() async {
final String raw = _urlController.text.trim();
final Uri? uri = Uri.tryParse(raw);
if (uri == null || !uri.hasScheme) {
_appendLog('URL 不合法: $raw');
return;
}
await _sub?.cancel();
await _client?.dispose();
final PDLongLinkClient client = PDLongLinkClient(
config: PDLongLinkConfig(
uri: uri,
transportMode: _mode,
reconnectPolicy: const PDReconnectPolicy(
maxAttempts: 3,
baseDelay: Duration(seconds: 1),
maxDelay: Duration(seconds: 8),
jitterRatio: 0.2,
enableInBackground: false,
reconnectOnResume: true,
),
heartbeat: PDHeartbeatConfig(
pingInterval: const Duration(seconds: 15),
pongTimeout: const Duration(seconds: 10),
buildPingText: () => 'ping',
isPongText: (String text) => text == 'ping',
),
networkConfig: widget.networkConfig,
),
);
_client = client;
_sub = client.events.listen((PDLongLinkEvent event) {
switch (event.type) {
case PDLongLinkEventType.open:
_appendLog('open');
case PDLongLinkEventType.message:
_appendLog('message: ${event.text ?? '(null)'}');
case PDLongLinkEventType.close:
_appendLog('close: code=${event.closeCode ?? '-'}, reason=${event.closeReason ?? '-'}');
case PDLongLinkEventType.error:
_appendLog('error: ${event.error}');
case PDLongLinkEventType.state:
_appendLog('state: ${event.state}');
}
});
await client.connect();
_appendLog('connect() called');
}
Future<void> _disconnect() async {
final PDLongLinkClient? client = _client;
if (client == null) {
return;
}
await client.disconnect(closeCode: 1000, closeReason: 'manual');
_appendLog('disconnect() called');
}
Future<void> _send() async {
final PDLongLinkClient? client = _client;
if (client == null) {
_appendLog('client is null');
return;
}
final String text = _messageController.text;
await client.sendText(text);
_appendLog('send: $text');
}
void _clearLogs() {
setState(() {
_logs.clear();
});
}
@override
Widget build(BuildContext context) {
final PDLongLinkState? state = _client?.state;
return Scaffold(
appBar: AppBar(title: const Text('长连接示例')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
_SectionCard(
title: '配置',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextField(
controller: _urlController,
decoration: const InputDecoration(labelText: 'WebSocket URL', border: OutlineInputBorder()),
),
const SizedBox(height: 12),
DropdownButtonFormField<PDLongLinkTransportMode>(
initialValue: _mode,
decoration: const InputDecoration(labelText: 'Transport Mode', border: OutlineInputBorder()),
items: PDLongLinkTransportMode.values
.map(
(PDLongLinkTransportMode mode) =>
DropdownMenuItem<PDLongLinkTransportMode>(value: mode, child: Text(mode.name)),
)
.toList(),
onChanged: (PDLongLinkTransportMode? value) {
if (value == null) {
return;
}
setState(() {
_mode = value;
});
},
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: <Widget>[
FilledButton(onPressed: _connect, child: const Text('Connect')),
FilledButton.tonal(onPressed: _disconnect, child: const Text('Disconnect')),
FilledButton.tonal(onPressed: _clearLogs, child: const Text('Clear Log')),
],
),
const SizedBox(height: 12),
Text('State: ${state?.name ?? '(null)'}'),
],
),
),
const SizedBox(height: 16),
_SectionCard(
title: '发送消息',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextField(
controller: _messageController,
minLines: 1,
maxLines: 6,
decoration: const InputDecoration(labelText: 'Text', border: OutlineInputBorder()),
),
const SizedBox(height: 12),
FilledButton.tonal(onPressed: _send, child: const Text('Send')),
],
),
),
const SizedBox(height: 16),
_SectionCard(title: '事件日志(最新在上)', child: SelectableText(_logs.isEmpty ? '(empty)' : _logs.join('\n'))),
],
),
);
}
}
class PDExtensionsDemoPage extends StatefulWidget {
const PDExtensionsDemoPage({super.key});
@override
State<PDExtensionsDemoPage> createState() => _PDExtensionsDemoPageState();
}
class _PDExtensionsDemoPageState extends State<PDExtensionsDemoPage> {
double _containerWidth = 360;
@override
Widget build(BuildContext context) {
final DateTime now = DateTime.now();
final List<String?> values = <String?>['a', null, 'b', 'c', null];
final List<String> nonNull = values.whereType<String>().toList();
return Scaffold(
appBar: AppBar(title: const Text('扩展能力示例')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
_SectionCard(
title: '容器宽度(影响 num.adapt)',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text('width: ${_containerWidth.toStringAsFixed(0)}'),
Slider(
value: _containerWidth,
min: 280,
max: 1200,
divisions: 46,
onChanged: (double v) => setState(() => _containerWidth = v),
),
],
),
),
const SizedBox(height: 16),
_SectionCard(
title: 'num.adapt / Insets / Radius',
child: Center(
child: SizedBox(
width: _containerWidth,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final double w = constraints.maxWidth;
return Container(
padding: 16.adaptInsetsAll(w),
decoration: BoxDecoration(
color: '#F3F5F8'.color(),
borderRadius: 12.adaptBorderRadius(w),
border: Border.all(color: '#D0D7DE'.color()),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('标题(随容器缩放)', style: TextStyle(fontSize: 18.adapt(w))),
SizedBox(height: 8.adapt(w)),
Text(
'正文:fontSize=${14.adapt(w).toStringAsFixed(1)} padding=${16.adapt(w).toStringAsFixed(1)} radius=${12.adapt(w).toStringAsFixed(1)}',
style: TextStyle(fontSize: 14.adapt(w), color: '#555555'.color()),
),
],
),
);
},
),
),
),
),
const SizedBox(height: 16),
_SectionCard(
title: '字符串转 Color',
child: Wrap(
spacing: 12,
runSpacing: 12,
children: <Widget>[
_ColorChip(label: '#FF5733', color: '#FF5733'.color()),
_ColorChip(label: 'FF5733', color: 'FF5733'.color()),
_ColorChip(label: '#5733', color: '#5733'.color()),
_ColorChip(label: '#1890FF', color: '#1890FF'.color()),
],
),
),
const SizedBox(height: 16),
_SectionCard(
title: 'DateTime 格式化',
child: SelectableText(
[
'now: ${now.toString().substring(0, 19)}',
'now.toIso8601String: ${now.toIso8601String()}',
'now.toString: ${now.toString()}',
].join('\n'),
),
),
const SizedBox(height: 16),
_SectionCard(
title: 'Iterable 扩展(whereNotNull 等)',
child: SelectableText(
[
'source: $values',
'whereNotNull: $nonNull',
'firstOrNull: ${nonNull.firstOrNull}',
'lastOrNull: ${nonNull.lastOrNull}',
'elementAtOrNull(10): ${nonNull.elementAtOrNull(10)}',
].join('\n'),
),
),
const SizedBox(height: 16),
_SectionCard(
title: 'BuildContext 扩展',
child: SelectableText(
[
'screenWidth: ${context.screenWidth.toStringAsFixed(1)}',
'screenHeight: ${context.screenHeight.toStringAsFixed(1)}',
'padding: ${context.padding}',
'theme.primary: ${context.colorScheme.primary}',
].join('\n'),
),
),
],
),
);
}
}
class _ColorChip extends StatelessWidget {
const _ColorChip({required this.label, required this.color});
final String label;
final Color color;
@override
Widget build(BuildContext context) {
final Brightness b = ThemeData.estimateBrightnessForColor(color);
final Color fg = b == Brightness.dark ? Colors.white : Colors.black;
return Container(
padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(999),
border: Border.all(color: Colors.black.withValues(alpha: 0.08)),
),
child: Text(label, style: TextStyle(color: fg)),
);
}
}
class PDResponsiveUiDemoPage extends StatefulWidget {
const PDResponsiveUiDemoPage({super.key});
@override
State<PDResponsiveUiDemoPage> createState() => _PDResponsiveUiDemoPageState();
}
class _PDResponsiveUiDemoPageState extends State<PDResponsiveUiDemoPage> {
double _containerWidth = 360;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('UI 响应式示例')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
_SectionCard(
title: '容器宽度(影响断点判定)',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text('width: ${_containerWidth.toStringAsFixed(0)}'),
Slider(
value: _containerWidth,
min: 320,
max: 1200,
divisions: 44,
onChanged: (double v) => setState(() => _containerWidth = v),
),
],
),
),
const SizedBox(height: 16),
_SectionCard(
title: 'PDResponsiveBuilder(debug paint)',
child: Center(
child: SizedBox(
width: _containerWidth,
child: PDResponsiveBuilder(
debugMode: PDResponsiveDebugMode.paint,
mobileBuilder: (BuildContext context) {
return _ResponsiveLabelCard(label: 'mobile (< 600)', color: '#F6FFED'.color());
},
tabletBuilder: (BuildContext context) {
return _ResponsiveLabelCard(label: 'tablet (600~899)', color: '#FFF7E6'.color());
},
desktopBuilder: (BuildContext context) {
return _ResponsiveLabelCard(label: 'desktop (>= 900)', color: '#E6F7FF'.color());
},
),
),
),
),
const SizedBox(height: 16),
_SectionCard(
title: 'PDResponsiveGridRow(Wrap 网格)',
child: Center(
child: SizedBox(
width: _containerWidth,
child: PDResponsiveGridRow(
spacing: 12,
runSpacing: 12,
debugPaint: true,
children: List<PDResponsiveGridCol>.generate(
12,
(int index) =>
PDResponsiveGridCol(mobile: 4, tablet: 4, desktop: 3, child: _GridItem(index: index)),
),
),
),
),
),
],
),
);
}
}
class _ResponsiveLabelCard extends StatelessWidget {
const _ResponsiveLabelCard({required this.label, required this.color});
final String label;
final Color color;
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
color: color,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.black.withValues(alpha: 0.08)),
),
child: Text(label, style: Theme.of(context).textTheme.titleMedium),
);
}
}
class _GridItem extends StatelessWidget {
const _GridItem({required this.index});
final int index;
@override
Widget build(BuildContext context) {
return Container(
height: 88,
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
border: Border.all(color: Colors.black.withValues(alpha: 0.08)),
),
padding: const EdgeInsets.all(12),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('Item ${index + 1}', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 6),
Text(
'screenType: ${PDResponsiveBuilder.getScreenType(context).name}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
);
}
}
class PDUtilsDemoPage extends StatefulWidget {
const PDUtilsDemoPage({super.key});
@override
State<PDUtilsDemoPage> createState() => _PDUtilsDemoPageState();
}
class _PDUtilsDemoPageState extends State<PDUtilsDemoPage> {
final TextEditingController _phoneController = TextEditingController(text: '13800138000');
final TextEditingController _passwordController = TextEditingController(text: 'Pass123!');
final PDPasswordValidator _passwordValidator = const PDPasswordValidator();
final PDTimerManager _timerManager = PDTimerManager();
int _tick = 0;
PDComparisonResult? _comparison;
@override
void initState() {
super.initState();
_timerManager.schedulePeriodicTimer(
key: 'tick',
period: const Duration(seconds: 1),
callback: () {
if (!mounted) {
return;
}
setState(() {
_tick++;
});
},
);
}
@override
void dispose() {
_timerManager.cancelAllTimers();
_phoneController.dispose();
_passwordController.dispose();
super.dispose();
}
void _runCompare() {
final Map<String, Object?> a = <String, Object?>{
'name': 'Pedro',
'age': 30,
'hobbies': <String>['reading', 'coding'],
'enabled': true,
};
final Map<String, Object?> b = <String, Object?>{
'name': 'Pedro',
'age': 31,
'hobbies': <String>['reading', 'swimming'],
'enabled': true,
};
final PDComparisonResult result = const PDDeepComparator().compare(a, b);
setState(() {
_comparison = result;
});
}
@override
Widget build(BuildContext context) {
final Map<String, Object?> unsafeJson = <String, Object?>{
'id': '123',
'enabled': 'true',
'price': '9.9',
'name': 456,
'tags': <Object?>['a', 1, null, 'b'],
'data': <String, Object?>{'k': 'v'},
};
final int id = PDTypeSafe.ofInt(unsafeJson['id'], defaultValue: 0);
final bool enabled = PDTypeSafe.ofBool(unsafeJson['enabled'], defaultValue: false);
final double price = PDTypeSafe.ofDouble(unsafeJson['price'], defaultValue: 0.0);
final String name = PDTypeSafe.ofString(unsafeJson['name'], defaultValue: '');
final List<String> tags = PDTypeSafe.ofList<String>(unsafeJson['tags']);
final String? phoneError = PDPhoneValidator.validateCNPhone(_phoneController.text);
final PDPasswordValidationResult pwdResult = _passwordValidator.validate(_passwordController.text);
return Scaffold(
appBar: AppBar(title: const Text('工具类示例')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
_SectionCard(
title: 'PDTypeSafe(安全转换)',
child: SelectableText(
[
'json: $unsafeJson',
'id(int): $id',
'enabled(bool): $enabled',
'price(double): $price',
'name(String): $name',
'tags(List<String>): $tags',
].join('\n'),
),
),
const SizedBox(height: 16),
_SectionCard(
title: '表单校验(Phone / Password)',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
TextField(
controller: _phoneController,
keyboardType: TextInputType.phone,
decoration: InputDecoration(
labelText: '手机号(CN)',
border: const OutlineInputBorder(),
errorText: phoneError,
),
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 12),
TextField(
controller: _passwordController,
decoration: InputDecoration(
labelText: '密码',
border: const OutlineInputBorder(),
errorText: pwdResult.isValid ? null : pwdResult.errorMessage,
),
onChanged: (_) => setState(() {}),
),
const SizedBox(height: 8),
Text(
pwdResult.isValid
? '密码校验通过'
: '失败原因: ${pwdResult.validationResults.entries.where((e) => !e.value).map((e) => e.key).join(', ')}',
style: Theme.of(context).textTheme.bodySmall,
),
],
),
),
const SizedBox(height: 16),
_SectionCard(
title: 'PDDeepComparator(深度比较)',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
FilledButton.tonal(onPressed: _runCompare, child: const Text('Compare Demo Data')),
const SizedBox(height: 12),
SelectableText(
_comparison == null
? '(empty)'
: [
'isEqual: ${_comparison!.isEqual}',
'description: ${_comparison!.description}',
'differencePaths: ${_comparison!.differencePaths}',
].join('\n'),
),
],
),
),
const SizedBox(height: 16),
_SectionCard(
title: 'PDTimerManager(周期任务)',
child: Row(
children: <Widget>[
Expanded(child: Text('tick: $_tick')),
FilledButton.tonal(
onPressed: () {
setState(() {
_tick = 0;
});
},
child: const Text('Reset'),
),
],
),
),
],
),
);
}
}
enum _DemoScenario {
success(title: '标准成功', description: '标准 {code, success, msg, data} 响应'),
legacySuccess(title: '旧接口成功', description: '只有 code=200,也会按成功解析'),
businessError(title: '业务失败', description: 'HTTP 200 但业务 code 非 200'),
serverError(title: '服务器错误', description: 'HTTP 500 会转成 BaseResponse.error'),
timeout(title: '网络超时', description: 'Transport 异常不会向外抛出'),
mapperError(title: '映射异常', description: 'mapper 抛错也会收敛为 BaseResponse.error'),
perRequestOverride(title: '单次配置覆盖', description: 'PDRequestConfig 仅影响当前请求,不污染全局配置'),
decryptDataField(title: '解密 data 字段', description: 'responseDecoder 解密 data 字段(模拟)'),
encryptRequestBody(title: '加密请求体', description: 'requestEncoder 加密请求体(模拟)');
const _DemoScenario({required this.title, required this.description});
final String title;
final String description;
}
class DemoResult {
const DemoResult({required this.title, required this.description, required this.response});
final String title;
final String description;
final BaseResponse<dynamic> response;
}
class _SectionCard extends StatelessWidget {
const _SectionCard({required this.title, required this.child});
final String title;
final Widget child;
@override
Widget build(BuildContext context) {
return Card(
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(title, style: Theme.of(context).textTheme.titleMedium),
const SizedBox(height: 12),
child,
],
),
),
);
}
}
class _ResultView extends StatelessWidget {
const _ResultView({required this.result});
final DemoResult? result;
@override
Widget build(BuildContext context) {
if (result == null) {
return const Text('点击上方任意场景按钮后,这里会展示 BaseResponse 的结果内容。', key: Key('result-placeholder'));
}
final BaseResponse<dynamic> response = result!.response;
final String prettyData = _prettyJson(response.data);
final String prettyRawData = _prettyJson(response.rawData);
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text('场景: ${result!.title}', key: const Key('result-title'), style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 8),
Text(result!.description, key: const Key('result-description')),
const SizedBox(height: 12),
Text('isSuccess: ${response.isSuccess}', key: const Key('result-success')),
Text('code: ${response.code}', key: const Key('result-code')),
Text('httpStatusCode: ${response.httpStatusCode ?? '-'}', key: const Key('result-http-code')),
Text('msg: ${response.msg.isEmpty ? '(empty)' : response.msg}', key: const Key('result-message')),
const SizedBox(height: 12),
SelectableText('data:\n$prettyData', key: const Key('result-data')),
const SizedBox(height: 12),
SelectableText('rawData:\n$prettyRawData', key: const Key('result-raw-data')),
],
);
}
String _prettyJson(dynamic value) {
if (value == null) {
return 'null';
}
try {
return const JsonEncoder.withIndent(' ').convert(value);
} catch (_) {
return value.toString();
}
}
}
class _DemoTransport extends PDTransport {
const _DemoTransport({required this.onSend});
final Future<PDTransportResponse> Function({required PDRequest request, PDCancelToken? cancelToken}) onSend;
@override
Future<PDTransportResponse> send({required PDRequest request, PDCancelToken? cancelToken}) {
return onSend(request: request, cancelToken: cancelToken);
}
}
class DemoLogger extends PDNetworkLogger {
DemoLogger();
final ValueNotifier<String> lastLog = ValueNotifier<String>('');
@override
void onRequest(PDRequest request) {
lastLog.value = [
'REQUEST',
'${request.method.name} ${request.config.baseUrl ?? ''}${request.path}',
'headers=${request.headers}',
'query=${request.queryParameters}',
'body=${request.body}',
].join('\n');
}
@override
void onResponse({required PDRequest request, required PDTransportResponse response}) {
lastLog.value = [
'RESPONSE',
'${request.method.name} ${request.config.baseUrl ?? ''}${request.path}',
'status=${response.statusCode}',
'data=${response.data}',
].join('\n');
}
@override
void onError({required PDRequest request, required PDTransportException exception}) {
lastLog.value = [
'ERROR',
'${request.method.name} ${request.config.baseUrl ?? ''}${request.path}',
'type=${exception.type}',
'status=${exception.statusCode ?? '-'}',
'message=${exception.message}',
].join('\n');
}
}
class _DemoCustomEncoder implements PDRequestEncoder {
@override
FutureOr<PDRequest> encode(PDRequest request) {
final Map<String, dynamic> body = request.body ?? <String, dynamic>{};
return request.copyWith(body: <String, dynamic>{'custom_encrypted_body': body});
}
}
class PDAdvancedNetworkDemoPage extends StatefulWidget {
const PDAdvancedNetworkDemoPage({super.key});
@override
State<PDAdvancedNetworkDemoPage> createState() => _PDAdvancedNetworkDemoPageState();
}
class _PDAdvancedNetworkDemoPageState extends State<PDAdvancedNetworkDemoPage> {
DemoResult? _lastResult;
PDCancelToken? _cancelToken;
bool _isCancelling = false;
Future<void> _runCustomEncoderDemo() async {
final BaseResponse<Map<String, dynamic>> response = await PDNetwork.client.post<Map<String, dynamic>>(
'/demo/custom-encoder',
data: <String, dynamic>{'name': 'Pedro', 'age': 30},
requestConfig: PDRequestConfig(
requestEncoder: _DemoCustomEncoder(),
responseDecoder: PDAesResponseDecoder(decrypt: (cipher) => cipher, mode: PDDecryptMode.dataFieldOnly),
),
);
if (!mounted) return;
setState(() {
_lastResult = DemoResult(title: '自定义编码器', description: '请求体经自定义 encoder 加密后发送', response: response);
});
}
Future<void> _runCancelDemo() async {
_cancelToken = PDCancelToken();
setState(() => _isCancelling = true);
final BaseResponse<Map<String, dynamic>> response = await PDNetwork.client.get<Map<String, dynamic>>(
'/demo/cancel-demo',
cancelToken: _cancelToken,
);
if (!mounted) return;
setState(() {
_isCancelling = false;
_lastResult = DemoResult(
title: '取消请求',
description: response.isSuccess ? '请求在取消前完成了' : '请求已被取消: ${response.msg}',
response: response,
);
});
}
void _doCancel() {
_cancelToken?.cancel('用户手动取消');
setState(() => _isCancelling = false);
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('网络层进阶示例')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
_SectionCard(
title: '自定义 PDRequestEncoder / PDResponseDecoder',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
const SelectableText(
'实现 PDRequestEncoder 和 PDResponseDecoder 接口,'
'可以自定义请求体加密和响应体解密逻辑。\n\n'
'class CustomEncoder implements PDRequestEncoder {\n'
' @override\n'
' Map<String, dynamic> encode(Map<String, dynamic> data) {\n'
' return {\'custom_encrypted_body\': data};\n'
' }\n'
'}\n\n'
'class CustomDecoder implements PDResponseDecoder {\n'
' @override\n'
' Map<String, dynamic> decode(Map<String, dynamic> data) {\n'
' return data[\'custom_decrypted\'] as Map<String, dynamic>;\n'
' }\n'
'}',
),
const SizedBox(height: 12),
FilledButton.tonal(onPressed: _runCustomEncoderDemo, child: const Text('运行自定义编解码演示')),
],
),
),
const SizedBox(height: 16),
_SectionCard(
title: 'PDCancelToken 取消请求',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
const SelectableText(
'使用 PDCancelToken 可以在请求进行中取消它。\n'
'取消请求不会抛出异常,而是返回一个失败的 BaseResponse。',
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: <Widget>[
FilledButton.tonal(onPressed: _isCancelling ? null : _runCancelDemo, child: const Text('发起可取消请求')),
FilledButton.tonal(onPressed: _isCancelling ? _doCancel : null, child: const Text('手动取消')),
],
),
],
),
),
const SizedBox(height: 16),
_SectionCard(
title: '最近一次响应',
child: _ResultView(result: _lastResult),
),
],
),
);
}
}
class PDAdvancedLongLinkDemoPage extends StatefulWidget {
const PDAdvancedLongLinkDemoPage({super.key});
@override
State<PDAdvancedLongLinkDemoPage> createState() => _PDAdvancedLongLinkDemoPageState();
}
class _PDAdvancedLongLinkDemoPageState extends State<PDAdvancedLongLinkDemoPage> {
final List<String> _logs = <String>[];
PDLongLinkClient? _client;
StreamSubscription<PDLongLinkEvent>? _sub;
int _reconnectCount = 0;
void _appendLog(String line) {
if (!mounted) return;
setState(() {
_logs.insert(0, '${DateTime.now().toString().substring(11, 19)} $line');
if (_logs.length > 200) _logs.removeRange(200, _logs.length);
});
}
Future<void> _connectWithFullConfig() async {
await _sub?.cancel();
await _client?.dispose();
_reconnectCount = 0;
final PDLongLinkClient client = PDLongLinkClient(
config: PDLongLinkConfig(
uri: Uri.parse('wss://echo.websocket.events'),
headers: const <String, String>{'Authorization': 'Bearer demo-token', 'User-Agent': 'pd_develop_kit/0.1.0'},
connectTimeout: const Duration(seconds: 30),
transportMode: PDLongLinkTransportMode.auto,
reconnectPolicy: const PDReconnectPolicy(
maxAttempts: 10,
baseDelay: Duration(seconds: 1),
maxDelay: Duration(seconds: 30),
jitterRatio: 0.2,
enableInBackground: false,
reconnectOnResume: true,
),
heartbeat: PDHeartbeatConfig(
pingInterval: const Duration(seconds: 30),
pongTimeout: const Duration(seconds: 10),
buildPingText: () => '{"type":"ping"}',
isPongText: (String text) => text.contains('"type":"pong"') || text == 'ping',
),
beforeReconnect: () async {
_reconnectCount++;
_appendLog('beforeReconnect: 第 $_reconnectCount 次重连准备');
return _reconnectCount <= 3;
},
),
);
_client = client;
_sub = client.events.listen((PDLongLinkEvent event) {
switch (event.type) {
case PDLongLinkEventType.open:
_appendLog('open');
case PDLongLinkEventType.message:
_handleMessage(event.text ?? '');
case PDLongLinkEventType.close:
_appendLog('close: code=${event.closeCode ?? '-'}, reason=${event.closeReason ?? '-'}');
case PDLongLinkEventType.error:
_appendLog('error: ${event.error}');
case PDLongLinkEventType.state:
_appendLog('state: ${event.state}');
}
});
await client.connect();
_appendLog('connect() called (完整配置)');
}
void _handleMessage(String text) {
try {
final Map<String, dynamic> data = jsonDecode(text) as Map<String, dynamic>;
final String? type = data['type'] as String?;
switch (type) {
case 'chat':
_appendLog('message(chat): ${data['text']}');
case 'notification':
_appendLog('message(notification): ${data['content']}');
default:
_appendLog('message(json): $text');
}
} catch (_) {
_appendLog('message(text): $text');
}
}
Future<void> _disconnect() async {
await _client?.disconnect(closeCode: 1000, closeReason: 'manual');
_appendLog('disconnect() called');
}
@override
void dispose() {
_sub?.cancel();
_client?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final PDLongLinkState? state = _client?.state;
return Scaffold(
appBar: AppBar(title: const Text('长连接进阶示例')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
_SectionCard(
title: '完整功能配置',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
const SelectableText(
'包含 headers、connectTimeout、reconnectPolicy、heartbeat、beforeReconnect 等完整配置。\n'
'消息处理演示了 JSON 解析 + 按 type 分发的模式。',
),
const SizedBox(height: 12),
Wrap(
spacing: 12,
runSpacing: 12,
children: <Widget>[
FilledButton(onPressed: _connectWithFullConfig, child: const Text('Connect (Full Config)')),
FilledButton.tonal(onPressed: _disconnect, child: const Text('Disconnect')),
],
),
const SizedBox(height: 12),
Text('State: ${state?.name ?? '(null)'}'),
],
),
),
const SizedBox(height: 16),
_SectionCard(title: '事件日志', child: SelectableText(_logs.isEmpty ? '(empty)' : _logs.join('\n'))),
],
),
);
}
}
class PDAdvancedExtensionsDemoPage extends StatefulWidget {
const PDAdvancedExtensionsDemoPage({super.key});
@override
State<PDAdvancedExtensionsDemoPage> createState() => _PDAdvancedExtensionsDemoPageState();
}
class _PDAdvancedExtensionsDemoPageState extends State<PDAdvancedExtensionsDemoPage> {
double _containerWidth = 360;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('扩展能力进阶示例')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
_SectionCard(
title: '容器宽度',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text('width: ${_containerWidth.toStringAsFixed(0)}'),
Slider(
value: _containerWidth,
min: 280,
max: 1200,
divisions: 46,
onChanged: (double v) => setState(() => _containerWidth = v),
),
],
),
),
const SizedBox(height: 16),
_SectionCard(
title: '便捷缩放 (adapt355 / adaptByConstraints)',
child: Center(
child: SizedBox(
width: _containerWidth,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final double w = constraints.maxWidth;
return Container(
padding: 16.adaptInsetsAll(w),
decoration: BoxDecoration(
color: '#F3F5F8'.color(),
borderRadius: 12.adaptBorderRadius(w),
border: Border.all(color: '#D0D7DE'.color()),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'adapt355: fontSize=${14.adapt355(w).toStringAsFixed(1)}',
style: TextStyle(fontSize: 14.adapt355(w)),
),
const SizedBox(height: 8),
Text(
'adaptByConstraints: fontSize=${14.adaptByConstraints(constraints).toStringAsFixed(1)}',
style: TextStyle(fontSize: 14.adaptByConstraints(constraints)),
),
const SizedBox(height: 8),
Text(
'adaptByConstraints(designWidth=390): ${14.adaptByConstraints(constraints, designWidth: 390).toStringAsFixed(1)}',
style: TextStyle(fontSize: 14.adaptByConstraints(constraints, designWidth: 390)),
),
],
),
);
},
),
),
),
),
const SizedBox(height: 16),
_SectionCard(
title: 'InsetsHorizontal / InsetsVertical',
child: Center(
child: SizedBox(
width: _containerWidth,
child: LayoutBuilder(
builder: (BuildContext context, BoxConstraints constraints) {
final double w = constraints.maxWidth;
return Container(
padding: 16.adaptInsetsHorizontal(w),
decoration: BoxDecoration(color: '#FFF7E6'.color(), borderRadius: 8.adaptBorderRadius(w)),
child: Text('水平边距: ${16.adapt(w).toStringAsFixed(1)}'),
);
},
),
),
),
),
],
),
);
}
}
class PDAdvancedUiDemoPage extends StatefulWidget {
const PDAdvancedUiDemoPage({super.key});
@override
State<PDAdvancedUiDemoPage> createState() => _PDAdvancedUiDemoPageState();
}
class _PDAdvancedUiDemoPageState extends State<PDAdvancedUiDemoPage> {
double _containerWidth = 360;
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('UI 响应式进阶示例')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
_SectionCard(
title: '容器宽度',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
Text('width: ${_containerWidth.toStringAsFixed(0)}'),
Slider(
value: _containerWidth,
min: 320,
max: 1200,
divisions: 44,
onChanged: (double v) => setState(() => _containerWidth = v),
),
],
),
),
const SizedBox(height: 16),
_SectionCard(
title: '自定义断点 (tablet=700, desktop=1200)',
child: Center(
child: SizedBox(
width: _containerWidth,
child: PDResponsiveBuilder(
debugMode: PDResponsiveDebugMode.paint,
mobileBuilder: (BuildContext context) =>
_ResponsiveLabelCard(label: 'mobile (< 700)', color: '#F6FFED'.color()),
tabletBuilder: (BuildContext context) =>
_ResponsiveLabelCard(label: 'tablet (700~1199)', color: '#FFF7E6'.color()),
desktopBuilder: (BuildContext context) =>
_ResponsiveLabelCard(label: 'desktop (>= 1200)', color: '#E6F7FF'.color()),
tabletBreakpoint: 700,
desktopBreakpoint: 1200,
),
),
),
),
const SizedBox(height: 16),
_SectionCard(
title: '仅手机和桌面布局(tablet 回退到 mobile)',
child: Center(
child: SizedBox(
width: _containerWidth,
child: PDResponsiveBuilder(
mobileBuilder: (BuildContext context) =>
_ResponsiveLabelCard(label: 'mobile 布局', color: '#F6FFED'.color()),
desktopBuilder: (BuildContext context) =>
_ResponsiveLabelCard(label: 'desktop 布局', color: '#E6F7FF'.color()),
),
),
),
),
const SizedBox(height: 16),
_SectionCard(
title: '网格回退策略(仅配置 mobile,其他回退)',
child: Center(
child: SizedBox(
width: _containerWidth,
child: PDResponsiveGridRow(
spacing: 12,
runSpacing: 12,
debugPaint: true,
children: <PDResponsiveGridCol>[
PDResponsiveGridCol(mobile: 2, child: _GridItem(index: 0)),
PDResponsiveGridCol(mobile: 2, child: _GridItem(index: 1)),
PDResponsiveGridCol(mobile: 4, child: _GridItem(index: 2)),
],
),
),
),
),
],
),
);
}
}
class PDAdvancedUtilsDemoPage extends StatefulWidget {
const PDAdvancedUtilsDemoPage({super.key});
@override
State<PDAdvancedUtilsDemoPage> createState() => _PDAdvancedUtilsDemoPageState();
}
class _PDAdvancedUtilsDemoPageState extends State<PDAdvancedUtilsDemoPage> {
final TextEditingController _passwordController = TextEditingController(text: 'MyPass123!');
PDPasswordValidationResult? _customResult;
PDPasswordValidationResult? _defaultResult;
final PDPasswordValidator _customValidator = PDPasswordValidator(
config: const PDPasswordValidationConfig(
minLength: 8,
maxLength: 20,
requireUppercase: true,
requireLowercase: true,
requireNumbers: true,
requireSpecialChars: true,
),
);
final PDPasswordValidator _defaultValidator = const PDPasswordValidator();
void _validate() {
final String input = _passwordController.text;
setState(() {
_customResult = _customValidator.validate(input);
_defaultResult = _defaultValidator.validate(input);
});
}
@override
void initState() {
super.initState();
_validate();
}
@override
void dispose() {
_passwordController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('工具类进阶示例')),
body: ListView(
padding: const EdgeInsets.all(16),
children: <Widget>[
_SectionCard(
title: '自定义 PDPasswordValidator 规则',
child: Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
children: <Widget>[
const SelectableText(
'final validator = PDPasswordValidator(\n'
' minLength: 8,\n'
' requireUppercase: true,\n'
' requireLowercase: true,\n'
' requireNumber: true,\n'
' requireSpecialChar: true,\n'
' allowedSpecialChars: \'!@#\$%^&*()_+-=\',\n'
');',
),
const SizedBox(height: 12),
TextField(
controller: _passwordController,
decoration: const InputDecoration(labelText: '密码', border: OutlineInputBorder()),
onChanged: (_) => _validate(),
),
const SizedBox(height: 12),
Text('自定义规则结果:', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 4),
Text(
_customResult == null
? '(empty)'
: _customResult!.isValid
? '通过'
: '失败: ${_customResult!.validationResults.entries.where((e) => !e.value).map((e) => e.key).join(', ')}',
),
const SizedBox(height: 12),
Text('默认规则结果:', style: Theme.of(context).textTheme.titleSmall),
const SizedBox(height: 4),
Text(
_defaultResult == null
? '(empty)'
: _defaultResult!.isValid
? '通过'
: '失败: ${_defaultResult!.validationResults.entries.where((e) => !e.value).map((e) => e.key).join(', ')}',
),
],
),
),
],
),
);
}
}