pd_develop_kit 0.1.0
pd_develop_kit: ^0.1.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 MyApp());
}
class MyApp extends StatefulWidget {
const MyApp({super.key});
@override
State<MyApp> createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
static const String _unknownPlatformVersion = 'Unknown platform version';
String _platformVersion = _unknownPlatformVersion;
final _pdDevelopKitPlugin = PdDevelopKit();
late final PDApiClient _demoClient;
late final _DemoLogger _logger;
DemoResult? _lastResult;
String _lastNetworkLog = '';
@override
void initState() {
super.initState();
_logger = _DemoLogger();
_logger.lastLog.addListener(_onLogChanged);
_configureDemoNetwork();
_demoClient = PDNetwork.client;
initPlatformState();
}
@override
void dispose() {
_logger.lastLog.removeListener(_onLogChanged);
super.dispose();
}
void _onLogChanged() {
if (!mounted) {
return;
}
setState(() {
_lastNetworkLog = _logger.lastLog.value;
});
}
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;
});
}
Future<void> _runScenario(_DemoScenario scenario) async {
final BaseResponse<dynamic> response;
switch (scenario) {
case _DemoScenario.success:
response = await _demoClient.get<Map<String, dynamic>>('/demo/success');
case _DemoScenario.legacySuccess:
response = await _demoClient.get<Map<String, dynamic>>(
'/demo/legacy-success',
);
case _DemoScenario.businessError:
response = await _demoClient.get<dynamic>('/demo/business-error');
case _DemoScenario.serverError:
response = await _demoClient.get<dynamic>('/demo/server-error');
case _DemoScenario.timeout:
response = await _demoClient.get<dynamic>('/demo/timeout');
case _DemoScenario.mapperError:
response = await _demoClient.get<int>(
'/demo/mapper-error',
mapper: (json) {
throw const FormatException('demo mapper error');
},
);
case _DemoScenario.perRequestOverride:
response = await _demoClient.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 _demoClient.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 _demoClient.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,
);
});
}
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},
},
);
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(
home: Scaffold(
appBar: AppBar(title: const Text('PD Develop Kit Example')),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
_SectionCard(
title: '平台能力',
child: Text(
'Running on: $_platformVersion',
key: const Key('platform-version-text'),
),
),
const SizedBox(height: 16),
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'),
),
),
],
),
),
),
);
}
}
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');
}
}