refactor: extract API actions into BoxingApiActions with error handling

Centralize all API call logic (fetchBoxInfo, saveBoxRecord,
updateBoxRecord, deleteBoxRecord) into BoxingApiActions class with
unified error categorization (network, duplicate, missingApiUrl).
Add helper methods to reduce repetitive error handling in boxing_page.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Misaka_Company
2026-05-21 09:54:16 +08:00
parent 9eb1bde645
commit b9f17d5d49
3 changed files with 368 additions and 142 deletions

View File

@@ -0,0 +1,150 @@
import 'package:pad_scanner/services/api_service.dart';
import 'package:pad_scanner/services/app_config_service.dart';
enum BoxingActionErrorKind { missingApiUrl, network, duplicate, backend }
class BoxingActionResult<T> {
final bool success;
final T? data;
final BoxingActionErrorKind? errorKind;
final String? errorMessage;
final int? boxNo;
const BoxingActionResult._({
required this.success,
this.data,
this.errorKind,
this.errorMessage,
this.boxNo,
});
const BoxingActionResult.ok(T data) : this._(success: true, data: data);
const BoxingActionResult.error({
required BoxingActionErrorKind kind,
required String message,
int? boxNo,
}) : this._(
success: false,
errorKind: kind,
errorMessage: message,
boxNo: boxNo,
);
}
typedef ApiUrlLoader = Future<String?> Function();
class BoxingApiActions {
static const networkErrorMessage = '网络异常,请检查网络连接';
static const missingApiUrlMessage = '未配置 API 地址,请前往设置';
final ApiService _apiService;
final ApiUrlLoader _loadApiUrl;
BoxingApiActions({required ApiService apiService, ApiUrlLoader? loadApiUrl})
: _apiService = apiService,
_loadApiUrl =
loadApiUrl ?? (() => AppConfigService().getString('api_url'));
Future<BoxingActionResult<BoxInfoResult>> fetchBoxInfo(String zongpai) async {
final baseUrl = await _requireBaseUrl();
if (baseUrl == null) return _missingApiUrl();
final result = await _apiService.fetchBoxInfo(
baseUrl: baseUrl,
zongpaiNo: zongpai,
);
if (result.success) return BoxingActionResult.ok(result);
return BoxingActionResult.error(
kind: _kindForMessage(result.errorMessage),
message: result.errorMessage ?? '查询失败',
);
}
Future<BoxingActionResult<BoxSaveResult>> saveBoxRecord({
required String zongpaiNo,
required int boxNo,
required int quantity,
}) async {
final baseUrl = await _requireBaseUrl();
if (baseUrl == null) return _missingApiUrl();
final result = await _apiService.saveBoxRecord(
baseUrl: baseUrl,
zongpaiNo: zongpaiNo,
boxNo: boxNo,
quantity: quantity,
);
return _saveResult(result, fallbackMessage: '提交失败');
}
Future<BoxingActionResult<BoxSaveResult>> updateBoxRecord({
required int boxItemId,
required int boxNo,
required int quantity,
}) async {
final baseUrl = await _requireBaseUrl();
if (baseUrl == null) return _missingApiUrl();
final result = await _apiService.updateBoxRecord(
baseUrl: baseUrl,
boxItemId: boxItemId,
boxNo: boxNo,
quantity: quantity,
);
return _saveResult(result, fallbackMessage: '修改失败');
}
Future<BoxingActionResult<BoxDeleteResult>> deleteBoxRecord({
required int boxItemId,
}) async {
final baseUrl = await _requireBaseUrl();
if (baseUrl == null) return _missingApiUrl();
final result = await _apiService.deleteBoxRecord(
baseUrl: baseUrl,
boxItemId: boxItemId,
);
if (result.success) return BoxingActionResult.ok(result);
return BoxingActionResult.error(
kind: _kindForMessage(result.errorMessage),
message: result.errorMessage ?? '删除失败',
);
}
Future<String?> _requireBaseUrl() async {
final baseUrl = await _loadApiUrl() ?? '';
if (baseUrl.isEmpty) return null;
return baseUrl;
}
BoxingActionResult<T> _missingApiUrl<T>() {
return const BoxingActionResult.error(
kind: BoxingActionErrorKind.missingApiUrl,
message: missingApiUrlMessage,
);
}
BoxingActionResult<BoxSaveResult> _saveResult(
BoxSaveResult result, {
required String fallbackMessage,
}) {
if (result.success) return BoxingActionResult.ok(result);
if (result.isDuplicate) {
return BoxingActionResult.error(
kind: BoxingActionErrorKind.duplicate,
message: result.errorMessage ?? fallbackMessage,
boxNo: result.boxNo,
);
}
return BoxingActionResult.error(
kind: _kindForMessage(result.errorMessage),
message: result.errorMessage ?? fallbackMessage,
);
}
BoxingActionErrorKind _kindForMessage(String? message) {
if (message == networkErrorMessage) return BoxingActionErrorKind.network;
return BoxingActionErrorKind.backend;
}
}

View File

@@ -2,12 +2,12 @@ import 'dart:async';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; import 'package:flutter/services.dart';
import 'package:pad_scanner/services/app_config_service.dart';
import 'package:pad_scanner/services/scanner_service.dart'; import 'package:pad_scanner/services/scanner_service.dart';
import 'package:pad_scanner/services/code_parser.dart'; import 'package:pad_scanner/services/code_parser.dart';
import 'package:pad_scanner/services/api_service.dart'; import 'package:pad_scanner/services/api_service.dart';
import 'package:pad_scanner/services/boxing_context.dart'; import 'package:pad_scanner/services/boxing_context.dart';
import 'package:pad_scanner/services/feedback_service.dart'; import 'package:pad_scanner/services/feedback_service.dart';
import 'package:pad_scanner/pages/boxing/boxing_api_actions.dart';
import 'package:pad_scanner/pages/boxing/boxing_box_mutations.dart' import 'package:pad_scanner/pages/boxing/boxing_box_mutations.dart'
as box_mutations; as box_mutations;
import 'package:pad_scanner/pages/boxing/boxing_calculations.dart' import 'package:pad_scanner/pages/boxing/boxing_calculations.dart'
@@ -39,6 +39,7 @@ class BoxingPage extends StatefulWidget {
class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver { class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
final _scannerService = ScannerService(); final _scannerService = ScannerService();
final _apiService = ApiService(); final _apiService = ApiService();
late final BoxingApiActions _apiActions;
final _feedbackService = FeedbackService(); final _feedbackService = FeedbackService();
final _focusNode = FocusNode(); final _focusNode = FocusNode();
StreamSubscription<ScanResult>? _scanSubscription; StreamSubscription<ScanResult>? _scanSubscription;
@@ -100,6 +101,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
@override @override
void initState() { void initState() {
super.initState(); super.initState();
_apiActions = BoxingApiActions(apiService: _apiService);
WidgetsBinding.instance.addObserver(this); WidgetsBinding.instance.addObserver(this);
final arguments = widget.arguments; final arguments = widget.arguments;
if (arguments != null) { if (arguments != null) {
@@ -264,38 +266,25 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
} }
Future<bool> _queryBoxInfo(String zongpai) async { Future<bool> _queryBoxInfo(String zongpai) async {
final configService = AppConfigService(); final action = await _apiActions.fetchBoxInfo(zongpai);
final baseUrl = await configService.getString('api_url') ?? '';
if (baseUrl.isEmpty) {
_feedbackService.trigger(FeedbackEvent.submitFailure);
_showStatusOverride(
'未配置 API 地址,请前往设置',
StatusDotColor.red,
const Duration(seconds: 2),
);
return false;
}
final result = await _apiService.fetchBoxInfo(
baseUrl: baseUrl,
zongpaiNo: zongpai,
);
if (!mounted) return false; if (!mounted) return false;
if (!result.success) { if (!action.success) {
final isNetwork = result.errorMessage == '网络异常,请检查网络连接'; _feedbackService.trigger(switch (action.errorKind) {
_feedbackService.trigger( BoxingActionErrorKind.network => FeedbackEvent.networkError,
isNetwork ? FeedbackEvent.networkError : FeedbackEvent.scanInvalid, BoxingActionErrorKind.missingApiUrl => FeedbackEvent.submitFailure,
); _ => FeedbackEvent.scanInvalid,
});
_showStatusOverride( _showStatusOverride(
result.errorMessage ?? '查询失败', action.errorMessage ?? '查询失败',
isNetwork ? StatusDotColor.yellow : StatusDotColor.red, _dotForActionError(action.errorKind),
const Duration(seconds: 2), const Duration(seconds: 2),
); );
return false; return false;
} }
final result = action.data!;
// 判断该总牌号是否已全部装箱完毕 // 判断该总牌号是否已全部装箱完毕
final packedQuantity = result.currentZongpaiBoxes.fold<int>( final packedQuantity = result.currentZongpaiBoxes.fold<int>(
0, 0,
@@ -599,33 +588,19 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
Future<bool> _submit() async { Future<bool> _submit() async {
if (!_canSubmit) return false; if (!_canSubmit) return false;
final configService = AppConfigService();
final baseUrl = await configService.getString('api_url') ?? '';
if (baseUrl.isEmpty) {
_feedbackService.trigger(FeedbackEvent.submitFailure);
_showStatusOverride(
'未配置 API 地址,请前往设置',
StatusDotColor.red,
const Duration(seconds: 2),
);
return false;
}
final boxNo = int.parse(_boxNoController.text); final boxNo = int.parse(_boxNoController.text);
final quantity = int.parse(_quantityController.text); final quantity = int.parse(_quantityController.text);
setState(() => _isSubmitting = true); setState(() => _isSubmitting = true);
final editing = _editingBox; final editing = _editingBox;
final result = editing == null final action = editing == null
? await _apiService.saveBoxRecord( ? await _apiActions.saveBoxRecord(
baseUrl: baseUrl,
zongpaiNo: _zongpaiNo!, zongpaiNo: _zongpaiNo!,
boxNo: boxNo, boxNo: boxNo,
quantity: quantity, quantity: quantity,
) )
: await _apiService.updateBoxRecord( : await _apiActions.updateBoxRecord(
baseUrl: baseUrl,
boxItemId: editing.boxItemId, boxItemId: editing.boxItemId,
boxNo: boxNo, boxNo: boxNo,
quantity: quantity, quantity: quantity,
@@ -635,31 +610,23 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
setState(() => _isSubmitting = false); setState(() => _isSubmitting = false);
if (result.success) { if (action.success) {
if (editing == null) { if (editing == null) {
_onSubmitSuccess(boxNo, quantity, result.boxItemId); _onSubmitSuccess(boxNo, quantity, action.data!.boxItemId);
} else { } else {
_onUpdateSuccess(editing, boxNo, quantity); _onUpdateSuccess(editing, boxNo, quantity);
} }
return true; return true;
} else if (result.isDuplicate) { } else if (action.errorKind == BoxingActionErrorKind.duplicate) {
_feedbackService.trigger(FeedbackEvent.duplicateBoxNo); _feedbackService.trigger(FeedbackEvent.duplicateBoxNo);
_showStatusOverride( _showStatusOverride(
'该总排号在箱号 ${result.boxNo ?? ""} 已存在,请重新输入', '该总排号在箱号 ${action.boxNo ?? ""} 已存在,请重新输入',
StatusDotColor.amber, StatusDotColor.amber,
const Duration(seconds: 2), const Duration(seconds: 2),
); );
return false; return false;
} else { } else {
final isNetwork = result.errorMessage == '网络异常,请检查网络连接'; _showActionError(action, fallback: '提交失败');
_feedbackService.trigger(
isNetwork ? FeedbackEvent.networkError : FeedbackEvent.submitFailure,
);
_showStatusOverride(
result.errorMessage ?? '提交失败',
isNetwork ? StatusDotColor.yellow : StatusDotColor.red,
const Duration(seconds: 2),
);
return false; return false;
} }
} }
@@ -883,20 +850,8 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
return; return;
} }
final configService = AppConfigService();
final baseUrl = await configService.getString('api_url') ?? '';
if (baseUrl.isEmpty) {
_showStatusOverride(
'未配置 API 地址,请前往设置',
StatusDotColor.red,
const Duration(seconds: 2),
);
return;
}
setState(() => _isSubmitting = true); setState(() => _isSubmitting = true);
final result = await _apiService.updateBoxRecord( final action = await _apiActions.updateBoxRecord(
baseUrl: baseUrl,
boxItemId: item.boxItemId, boxItemId: item.boxItemId,
boxNo: item.boxNo, boxNo: item.boxNo,
quantity: quantity, quantity: quantity,
@@ -904,7 +859,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
if (!mounted) return; if (!mounted) return;
setState(() => _isSubmitting = false); setState(() => _isSubmitting = false);
if (result.success) { if (action.success) {
setState(() { setState(() {
_currentZongpaiBoxes = _currentZongpaiBoxes.map((box) { _currentZongpaiBoxes = _currentZongpaiBoxes.map((box) {
if (box.boxItemId != item.boxItemId) return box; if (box.boxItemId != item.boxItemId) return box;
@@ -925,39 +880,18 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
const Duration(milliseconds: 1500), const Duration(milliseconds: 1500),
); );
} else { } else {
final dot = result.isDuplicate _showActionError(action, fallback: '修改失败');
? StatusDotColor.amber
: StatusDotColor.red;
_showStatusOverride(
result.errorMessage ?? '修改失败',
dot,
const Duration(seconds: 2),
);
} }
} }
Future<void> _deleteAssignedItem(CurrentZongpaiBoxData item) async { Future<void> _deleteAssignedItem(CurrentZongpaiBoxData item) async {
final configService = AppConfigService();
final baseUrl = await configService.getString('api_url') ?? '';
if (baseUrl.isEmpty) {
_showStatusOverride(
'未配置 API 地址,请前往设置',
StatusDotColor.red,
const Duration(seconds: 2),
);
return;
}
setState(() => _isSubmitting = true); setState(() => _isSubmitting = true);
final result = await _apiService.deleteBoxRecord( final action = await _apiActions.deleteBoxRecord(boxItemId: item.boxItemId);
baseUrl: baseUrl,
boxItemId: item.boxItemId,
);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_isSubmitting = false; _isSubmitting = false;
if (result.success) { if (action.success) {
_currentZongpaiBoxes = _currentZongpaiBoxes _currentZongpaiBoxes = _currentZongpaiBoxes
.where((box) => box.boxItemId != item.boxItemId) .where((box) => box.boxItemId != item.boxItemId)
.toList(); .toList();
@@ -971,18 +905,14 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
} }
}); });
if (result.success) { if (action.success) {
_showStatusOverride( _showStatusOverride(
'删除成功', '删除成功',
StatusDotColor.green, StatusDotColor.green,
const Duration(milliseconds: 1500), const Duration(milliseconds: 1500),
); );
} else { } else {
_showStatusOverride( _showActionError(action, fallback: '删除失败');
result.errorMessage ?? '删除失败',
StatusDotColor.red,
const Duration(seconds: 2),
);
} }
} }
@@ -1013,20 +943,8 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
return; return;
} }
final configService = AppConfigService();
final baseUrl = await configService.getString('api_url') ?? '';
if (baseUrl.isEmpty) {
_showStatusOverride(
'未配置 API 地址,请前往设置',
StatusDotColor.red,
const Duration(seconds: 2),
);
return;
}
setState(() => _isSubmitting = true); setState(() => _isSubmitting = true);
final result = await _apiService.updateBoxRecord( final action = await _apiActions.updateBoxRecord(
baseUrl: baseUrl,
boxItemId: item.boxItemId, boxItemId: item.boxItemId,
boxNo: boxNo, boxNo: boxNo,
quantity: quantity, quantity: quantity,
@@ -1034,7 +952,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
if (!mounted) return; if (!mounted) return;
setState(() => _isSubmitting = false); setState(() => _isSubmitting = false);
if (result.success) { if (action.success) {
setState(() { setState(() {
final index = _manyToOnePackedItems.indexWhere( final index = _manyToOnePackedItems.indexWhere(
(packed) => packed.boxItemId == item.boxItemId, (packed) => packed.boxItemId == item.boxItemId,
@@ -1055,14 +973,7 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
const Duration(milliseconds: 1500), const Duration(milliseconds: 1500),
); );
} else { } else {
final dot = result.isDuplicate _showActionError(action, fallback: '修改失败');
? StatusDotColor.amber
: StatusDotColor.red;
_showStatusOverride(
result.errorMessage ?? '修改失败',
dot,
const Duration(seconds: 2),
);
} }
} }
@@ -1082,27 +993,13 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
} }
Future<void> _deletePackedItem(ManyToOnePackedItem item) async { Future<void> _deletePackedItem(ManyToOnePackedItem item) async {
final configService = AppConfigService();
final baseUrl = await configService.getString('api_url') ?? '';
if (baseUrl.isEmpty) {
_showStatusOverride(
'未配置 API 地址,请前往设置',
StatusDotColor.red,
const Duration(seconds: 2),
);
return;
}
setState(() => _isSubmitting = true); setState(() => _isSubmitting = true);
final result = await _apiService.deleteBoxRecord( final action = await _apiActions.deleteBoxRecord(boxItemId: item.boxItemId);
baseUrl: baseUrl,
boxItemId: item.boxItemId,
);
if (!mounted) return; if (!mounted) return;
setState(() { setState(() {
_isSubmitting = false; _isSubmitting = false;
if (result.success) { if (action.success) {
_manyToOnePackedItems.removeWhere( _manyToOnePackedItems.removeWhere(
(packed) => packed.boxItemId == item.boxItemId, (packed) => packed.boxItemId == item.boxItemId,
); );
@@ -1115,18 +1012,14 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
} }
}); });
if (result.success) { if (action.success) {
_showStatusOverride( _showStatusOverride(
'删除成功', '删除成功',
StatusDotColor.green, StatusDotColor.green,
const Duration(milliseconds: 1500), const Duration(milliseconds: 1500),
); );
} else { } else {
_showStatusOverride( _showActionError(action, fallback: '删除失败');
result.errorMessage ?? '删除失败',
StatusDotColor.red,
const Duration(seconds: 2),
);
} }
} }
@@ -1141,6 +1034,30 @@ class _BoxingPageState extends State<BoxingPage> with WidgetsBindingObserver {
// === Status bar management === // === Status bar management ===
StatusDotColor _dotForActionError(BoxingActionErrorKind? kind) {
return switch (kind) {
BoxingActionErrorKind.network => StatusDotColor.yellow,
BoxingActionErrorKind.duplicate => StatusDotColor.amber,
_ => StatusDotColor.red,
};
}
void _showActionError<T>(
BoxingActionResult<T> action, {
required String fallback,
}) {
_feedbackService.trigger(switch (action.errorKind) {
BoxingActionErrorKind.network => FeedbackEvent.networkError,
BoxingActionErrorKind.duplicate => FeedbackEvent.duplicateBoxNo,
_ => FeedbackEvent.submitFailure,
});
_showStatusOverride(
action.errorMessage ?? fallback,
_dotForActionError(action.errorKind),
const Duration(seconds: 2),
);
}
void _showStatusOverride(String text, StatusDotColor dot, Duration duration) { void _showStatusOverride(String text, StatusDotColor dot, Duration duration) {
setState(() { setState(() {
_statusOverrideText = text; _statusOverrideText = text;

View File

@@ -0,0 +1,159 @@
import 'dart:convert';
import 'package:flutter_test/flutter_test.dart';
import 'package:http/http.dart' as http;
import 'package:pad_scanner/pages/boxing/boxing_api_actions.dart';
import 'package:pad_scanner/services/api_service.dart';
void main() {
group('BoxingApiActions', () {
test('returns missingApiUrl when API URL is empty', () async {
final actions = BoxingApiActions(
apiService: ApiService(
client: _MockClient((_) async {
fail('ApiService should not be called without a base URL');
}),
),
loadApiUrl: () async => '',
);
final result = await actions.fetchBoxInfo('26B1');
expect(result.success, isFalse);
expect(result.errorKind, BoxingActionErrorKind.missingApiUrl);
expect(result.errorMessage, BoxingApiActions.missingApiUrlMessage);
});
test('fetchBoxInfo passes through successful data', () async {
final actions = BoxingApiActions(
apiService: ApiService(
client: _MockClient((request) async {
expect(request.url.path, '/CargoTrace/box/info');
expect(request.url.queryParameters['zongpai_no'], '26B1');
return http.Response(
jsonEncode({
'zongpai_no': '26B1',
'paichan_no': 'W00009',
'work_order_no': 'WO001',
'quantity': 8,
'current_zongpai_boxes': [
{'box_item_id': 1, 'box_no': 2, 'quantity': 3},
],
'existing_boxes': [],
'max_box_no': 2,
}),
200,
headers: {'content-type': 'application/json; charset=utf-8'},
);
}),
),
loadApiUrl: () async => 'http://localhost',
);
final result = await actions.fetchBoxInfo('26B1');
expect(result.success, isTrue);
expect(result.data?.paichanNo, 'W00009');
expect(result.data?.currentZongpaiBoxes.single.boxNo, 2);
});
test('saveBoxRecord maps duplicate and preserves box number', () async {
final actions = BoxingApiActions(
apiService: ApiService(
client: _MockClient((_) async {
return http.Response(
jsonEncode({
'error_code': 'DUPLICATE_BOX_ITEM',
'message': '重复装箱',
'box_no': 7,
}),
409,
headers: {'content-type': 'application/json; charset=utf-8'},
);
}),
),
loadApiUrl: () async => 'http://localhost',
);
final result = await actions.saveBoxRecord(
zongpaiNo: '26B1',
boxNo: 7,
quantity: 2,
);
expect(result.success, isFalse);
expect(result.errorKind, BoxingActionErrorKind.duplicate);
expect(result.boxNo, 7);
});
test('updateBoxRecord maps network errors', () async {
final actions = BoxingApiActions(
apiService: ApiService(
client: _MockClient((_) async {
throw Exception('Connection refused');
}),
),
loadApiUrl: () async => 'http://localhost',
);
final result = await actions.updateBoxRecord(
boxItemId: 1,
boxNo: 2,
quantity: 3,
);
expect(result.success, isFalse);
expect(result.errorKind, BoxingActionErrorKind.network);
expect(result.errorMessage, BoxingApiActions.networkErrorMessage);
});
test('deleteBoxRecord passes through success and backend errors', () async {
var calls = 0;
final actions = BoxingApiActions(
apiService: ApiService(
client: _MockClient((_) async {
calls += 1;
if (calls == 1) {
return http.Response(
jsonEncode({'box_item_id': 1, 'deleted': true}),
200,
headers: {'content-type': 'application/json; charset=utf-8'},
);
}
return http.Response(
jsonEncode({'message': '指定装箱明细不存在'}),
404,
headers: {'content-type': 'application/json; charset=utf-8'},
);
}),
),
loadApiUrl: () async => 'http://localhost',
);
final ok = await actions.deleteBoxRecord(boxItemId: 1);
final missing = await actions.deleteBoxRecord(boxItemId: 999);
expect(ok.success, isTrue);
expect(missing.success, isFalse);
expect(missing.errorKind, BoxingActionErrorKind.backend);
expect(missing.errorMessage, '指定装箱明细不存在');
});
});
}
class _MockClient extends http.BaseClient {
final Future<http.Response> Function(http.BaseRequest) _handler;
_MockClient(this._handler);
@override
Future<http.StreamedResponse> send(http.BaseRequest request) async {
final response = await _handler(request);
return http.StreamedResponse(
http.ByteStream.fromBytes(response.bodyBytes),
response.statusCode,
headers: response.headers,
reasonPhrase: response.reasonPhrase,
);
}
}