networking and error handling

This commit is contained in:
amirrezaghabeli
2025-08-12 14:40:49 +03:30
parent 382ed4a2d6
commit 4aeb9229a4
14 changed files with 477 additions and 205 deletions
+1 -4
View File
@@ -1,5 +1,6 @@
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:provider/single_child_widget.dart'; import 'package:provider/single_child_widget.dart';
import 'package:tm_app/core/config/network_manager.dart';
import 'package:tm_app/core/theme/theme_provider.dart'; import 'package:tm_app/core/theme/theme_provider.dart';
import 'package:tm_app/data/repositories/home_repository.dart'; import 'package:tm_app/data/repositories/home_repository.dart';
import 'package:tm_app/data/repositories/tender_detail_repository.dart'; import 'package:tm_app/data/repositories/tender_detail_repository.dart';
@@ -14,7 +15,6 @@ import 'package:tm_app/view_models/your_tenders_view_model.dart';
import '../../data/repositories/auth_repository.dart'; import '../../data/repositories/auth_repository.dart';
import '../../data/services/auth_service.dart'; import '../../data/services/auth_service.dart';
import '../../data/services/network_manager.dart';
import '../../data/services/tenders_service.dart'; import '../../data/services/tenders_service.dart';
import '../../view_models/auth_view_model.dart'; import '../../view_models/auth_view_model.dart';
import '../../view_models/home_view_model.dart'; import '../../view_models/home_view_model.dart';
@@ -87,9 +87,6 @@ List<SingleChildWidget> get viewModels {
(context) => (context) =>
YourTendersViewModel(yourTendersRepository: context.read()), YourTendersViewModel(yourTendersRepository: context.read()),
), ),
ChangeNotifierProvider(
create: (context) => TendersViewModel(tendersRepository: context.read()),
),
]; ];
} }
+320
View File
@@ -0,0 +1,320 @@
import 'dart:io';
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../utils/app_exceptions.dart';
import '../utils/logger.dart';
import '../utils/network_connectivity.dart';
import '../utils/result.dart';
class NetworkManager {
final Dio mainDio;
final NetworkConnectivity _connectivity = NetworkConnectivity();
NetworkManager()
: mainDio = Dio(
BaseOptions(
// baseUrl: 'https://stage.cura-bau.de',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
sendTimeout: const Duration(seconds: 10),
contentType: 'application/json',
headers: {'Accept': 'application/json'},
),
);
Future<void> init() async {
await addInterceptors();
}
Future<void> addInterceptors() async {
final requestInterceptor = InterceptorsWrapper(
onRequest: (options, handler) {
appLogger.info('🌐 Request: ${options.method} ${options.path}');
appLogger.info('🌐 Headers: ${options.headers}');
return handler.next(options);
},
onResponse: (response, handler) {
appLogger.info(
'✅ Response: ${response.statusCode} ${response.requestOptions.path}',
);
appLogger.info('✅ Response data: ${response.data}');
return handler.next(response);
},
onError: (error, handler) {
appLogger.error(
'❌ Error: ${error.message} - ${error.requestOptions.path}',
);
appLogger.error('❌ Error type: ${error.type}');
return handler.next(error);
},
);
mainDio.interceptors.add(requestInterceptor);
}
/// Enhanced makeRequest method with comprehensive error handling
Future<Result<T>> makeRequest<T>(
String endpoint,
T Function(Map<String, dynamic>) fromJson, {
String method = 'GET',
FormData? data,
Map<String, dynamic>? queryParameters,
}) async {
appLogger.info('🚀 network request: $method $endpoint');
try {
// Check network connectivity first
if (!await _connectivity.hasInternetConnection()) {
appLogger.error('❌ No internet connection available');
return Result.error(
NetworkConnectionException('No internet connection available'),
);
}
SharedPreferences prefs = await SharedPreferences.getInstance();
Dio dioInstance = mainDio;
await addInterceptors();
Future<Response> makeApiCall(String token, String url) async {
final headers =
token.isEmpty
? <String, String>{}
: {'Authorization': 'Bearer $token'};
final options = Options(
headers: headers,
validateStatus: (status) => status != null && status < 500,
);
switch (method.toUpperCase()) {
case 'POST':
return await dioInstance.post(
url + endpoint,
data: data,
queryParameters: queryParameters,
options: options,
);
case 'PUT':
return await dioInstance.put(
url + endpoint,
data: data,
queryParameters: queryParameters,
options: options,
);
case 'PATCH':
return await dioInstance.patch(
url + endpoint,
data: data,
queryParameters: queryParameters,
options: options,
);
case 'DELETE':
return await dioInstance.delete(
url + endpoint,
data: data,
queryParameters: queryParameters,
options: options,
);
case 'GET':
return await dioInstance.get(
url + endpoint,
queryParameters: queryParameters,
options: options,
);
default:
return await dioInstance.get(
url + endpoint,
queryParameters: queryParameters,
options: options,
);
}
}
String? bearerToken = prefs.getString('bearer');
String? url = prefs.getString('url');
appLogger.info('🔗 Base URL: ${url ?? 'NOT SET'}');
appLogger.info(
'🔑 Bearer token: ${bearerToken != null ? 'PRESENT' : 'NOT SET'}',
);
if (url == null || url.isEmpty) {
appLogger.error('❌ Base URL not configured');
return Result.error(BadRequestException('Base URL not configured'));
}
Response res = await makeApiCall(bearerToken ?? '', url);
// Handle successful responses
if (res.statusCode == 200 || res.statusCode == 201) {
appLogger.info('✅ Request successful: ${res.statusCode}');
try {
final result = fromJson(res.data);
appLogger.info('✅ Response parsed successfully');
return Result.ok(result);
} on Exception catch (e) {
appLogger.error('❌ Failed to parse response: $e');
return Result.error(
FetchDataException(
'Failed to parse response data',
res.statusCode,
e,
),
);
}
}
// Handle specific HTTP error codes
return _handleHttpError(res);
} on DioException catch (e) {
return _handleDioException(e);
} on Exception catch (e) {
appLogger.error('Unexpected error in makeRequest: $e');
return Result.error(
FetchDataException('Unexpected error occurred', null, e),
);
}
}
/// Handle HTTP error responses
Result<T> _handleHttpError<T>(Response response) {
final statusCode = response.statusCode;
final message = _extractErrorMessage(response.data);
switch (statusCode) {
case 400:
return Result.error(
BadRequestException(message, statusCode, response.data),
);
case 401:
return Result.error(
AuthenticationException(message, statusCode, response.data),
);
case 403:
return Result.error(
ForbiddenException(message, statusCode, response.data),
);
case 404:
return Result.error(
NotFoundException(message, statusCode, response.data),
);
case 423:
return Result.error(
ForbiddenException(
'Resource is locked: $message',
statusCode,
response.data,
),
);
case 429:
return Result.error(
RateLimitException(message, statusCode, response.data),
);
case 500:
case 502:
case 503:
case 504:
return Result.error(
ServerException(message, statusCode, response.data),
);
default:
return Result.error(
FetchDataException(message, statusCode, response.data),
);
}
}
/// Handle Dio exceptions
Result<T> _handleDioException<T>(DioException e) {
switch (e.type) {
case DioExceptionType.connectionTimeout:
case DioExceptionType.sendTimeout:
case DioExceptionType.receiveTimeout:
return Result.error(TimeoutException('Request timed out', e));
case DioExceptionType.connectionError:
return Result.error(
NetworkConnectionException('Connection error occurred', e),
);
case DioExceptionType.badResponse:
if (e.response?.statusCode == 401) {
// Handle token refresh logic here if needed
// For now, return authentication error
return Result.error(
AuthenticationException(
'Authentication failed',
e.response?.statusCode,
e,
),
);
}
return Result.error(
FetchDataException(
'Bad response from server',
e.response?.statusCode,
e,
),
);
case DioExceptionType.cancel:
return Result.error(
FetchDataException('Request was cancelled', null, e),
);
case DioExceptionType.unknown:
default:
if (e.error is SocketException) {
return Result.error(
NetworkConnectionException('No internet connection', e),
);
}
return Result.error(
FetchDataException('Network error occurred', null, e),
);
}
}
/// Extract error message from response data
String _extractErrorMessage(data) {
if (data == null) {
return 'Unknown error';
}
if (data is Map<String, dynamic>) {
return data['message'] ??
data['error'] ??
data['detail'] ??
'Unknown error';
}
if (data is String) {
return data;
}
return 'Unknown error';
}
/// Simple request method for backward compatibility
// Future<T> makeSimpleRequest<T>(
// String endpoint,
// T Function(Map<String, dynamic>) fromJson, {
// String method = 'GET',
// FormData? data,
// }) async {
// final result = await makeRequest(
// endpoint,
// fromJson,
// method: method,
// data: data,
// );
// return switch (result) {
// Ok(value: final value) => value,
// Error(error: final error) => throw error,
// };
// }
}
+1 -1
View File
@@ -6,7 +6,7 @@ import 'package:tm_app/views/login/pages/login_screen.dart';
import '../../views/profile/pages/profile_screen.dart'; import '../../views/profile/pages/profile_screen.dart';
import '../../views/tenders/pages/tenders_screen.dart'; import '../../views/tenders/pages/tenders_screen.dart';
import '../../views/your_tenders/your_tenders_screen.dart'; import '../../views/your_tenders/pages/your_tenders_screen.dart';
import 'app_shell/app_shell_screen.dart'; import 'app_shell/app_shell_screen.dart';
part 'app_routes.g.dart'; part 'app_routes.g.dart';
+92 -5
View File
@@ -1,8 +1,15 @@
class AppException implements Exception { class AppException implements Exception {
final String message; final String message;
final String prefix; final String prefix;
final int? statusCode;
final dynamic originalError;
AppException([this.message = 'An unknown error occurred.', this.prefix = '']); AppException([
this.message = 'An unknown error occurred.',
this.prefix = '',
this.statusCode,
this.originalError,
]);
@override @override
String toString() { String toString() {
@@ -11,11 +18,91 @@ class AppException implements Exception {
} }
class FetchDataException extends AppException { class FetchDataException extends AppException {
FetchDataException([String? message]) FetchDataException([String? message, int? statusCode, originalError])
: super(message ?? 'Error During Communication: ', 'Network Error'); : super(
message ?? 'Error During Communication: ',
'Network Error',
statusCode,
originalError,
);
} }
class AuthenticationException extends AppException { class AuthenticationException extends AppException {
AuthenticationException([String? message]) AuthenticationException([String? message, int? statusCode, originalError])
: super(message ?? 'Authentication Failed.', 'Auth Error'); : super(
message ?? 'Authentication Failed.',
'Auth Error: ',
statusCode,
originalError,
);
}
class NetworkConnectionException extends AppException {
NetworkConnectionException([String? message, originalError])
: super(
message ?? 'No internet connection available.',
'Connection Error: ',
null,
originalError,
);
}
class TimeoutException extends AppException {
TimeoutException([String? message, originalError])
: super(
message ?? 'Request timed out.',
'Timeout Error: ',
null,
originalError,
);
}
class ServerException extends AppException {
ServerException([String? message, int? statusCode, originalError])
: super(
message ?? 'Server error occurred.',
'Server Error: ',
statusCode,
originalError,
);
}
class BadRequestException extends AppException {
BadRequestException([String? message, int? statusCode, originalError])
: super(
message ?? 'Bad request.',
'Bad Request Error: ',
statusCode,
originalError,
);
}
class NotFoundException extends AppException {
NotFoundException([String? message, int? statusCode, originalError])
: super(
message ?? 'Resource not found.',
'Not Found Error: ',
statusCode,
originalError,
);
}
class ForbiddenException extends AppException {
ForbiddenException([String? message, int? statusCode, originalError])
: super(
message ?? 'Access forbidden.',
'Forbidden Error: ',
statusCode,
originalError,
);
}
class RateLimitException extends AppException {
RateLimitException([String? message, int? statusCode, originalError])
: super(
message ?? 'Rate limit exceeded.',
'Rate Limit Error: ',
statusCode,
originalError,
);
} }
+21
View File
@@ -0,0 +1,21 @@
import 'dart:io';
class NetworkConnectivity {
static final NetworkConnectivity _instance = NetworkConnectivity._internal();
factory NetworkConnectivity() => _instance;
NetworkConnectivity._internal();
/// Check if device has internet connectivity
Future<bool> hasInternetConnection() async {
try {
// Try to reach a reliable host to check internet connectivity
final result = await InternetAddress.lookup('google.com');
return result.isNotEmpty && result[0].rawAddress.isNotEmpty;
} on SocketException catch (_) {
return false;
} on Exception catch (_) {
// If connectivity check fails, assume no connection
return false;
}
}
}
+1 -1
View File
@@ -1,4 +1,4 @@
import 'network_manager.dart'; import '../../core/config/network_manager.dart';
class AuthService { class AuthService {
AuthService({required NetworkManager networkManager}) AuthService({required NetworkManager networkManager})
+2 -1
View File
@@ -6,7 +6,8 @@ import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/services/mock_data/home_mock_data.dart'; import 'package:tm_app/data/services/mock_data/home_mock_data.dart';
import 'package:tm_app/data/services/model/home/home_request/home_request_model.dart'; import 'package:tm_app/data/services/model/home/home_request/home_request_model.dart';
import 'package:tm_app/data/services/model/home/home_response/home_response_model.dart'; import 'package:tm_app/data/services/model/home/home_response/home_response_model.dart';
import 'package:tm_app/data/services/network_manager.dart';
import '../../core/config/network_manager.dart';
class HomeService { class HomeService {
HomeService({required NetworkManager networkManager}) HomeService({required NetworkManager networkManager})
-129
View File
@@ -1,129 +0,0 @@
import 'package:dio/dio.dart';
import 'package:shared_preferences/shared_preferences.dart';
class NetworkManager {
final Dio mainDio;
NetworkManager()
: mainDio = Dio(
BaseOptions(
// baseUrl: 'https://stage.cura-bau.de',
connectTimeout: const Duration(seconds: 10),
receiveTimeout: const Duration(seconds: 10),
contentType: 'application/json',
headers: {'Accept': 'application/json'},
),
);
Future<void> init() async {
await addInterceptors();
}
Future<void> addInterceptors() async {
final requestInterceptor = InterceptorsWrapper(
onRequest: (options, handler) {
// TODO: add more
return handler.next(options);
},
);
mainDio.interceptors.add(requestInterceptor);
}
Future<T> makeRequest<T>(
String endpoint,
T Function(Map<String, dynamic>) fromJson, {
String method = 'GET',
FormData? data,
}) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
Dio dioInstance = mainDio;
await addInterceptors();
Future<Response> makeApiCall(String token, String url) async {
final headers =
token.isEmpty
? <String, String>{}
: {'Authorization': 'Bearer $token'};
switch (method.toUpperCase()) {
case 'POST':
return await dioInstance.post(
url + endpoint,
data: data,
options: Options(headers: headers),
);
case 'PUT':
return await dioInstance.put(
url + endpoint,
data: data,
options: Options(headers: headers),
);
case 'PATCH':
return await dioInstance.patch(
url + endpoint,
data: data,
options: Options(headers: headers),
);
case 'DELETE':
return await dioInstance.delete(
url + endpoint,
data: data,
options: Options(headers: headers),
);
case 'GET':
return await dioInstance.get(
url + endpoint,
data: data,
options: Options(headers: headers),
);
default:
return await dioInstance.get(
url + endpoint,
options: Options(headers: headers),
);
}
}
try {
String? bearerToken = prefs.getString('bearer');
String? url = prefs.getString('url');
Response res = await makeApiCall(bearerToken ?? '', url ?? '');
switch (res.statusCode) {
case 200:
case 201:
return fromJson(res.data);
case 401:
break;
case 403:
throw Exception('Forbidden: ${res.data['message']}');
case 404:
throw Exception('Not found: ${res.data['message']}');
case 423:
throw Exception('Locked: ${res.data['message']}');
case 500:
throw Exception('Internal server error: ${res.data['message']}');
default:
throw Exception('Error: ${res.data['message']}');
}
} on DioException catch (e) {
if (e.response?.statusCode == 401) {
// await api.refreshToken();
String? bearerToken = prefs.getString('bearer');
String? url = prefs.getString('url');
Response res = await makeApiCall(bearerToken ?? '', url ?? '');
if (res.statusCode == 200 || res.statusCode == 201) {
return fromJson(res.data);
}
throw Exception(e.message ?? 'Error occurred');
}
} catch (e) {
throw Exception(e.toString());
}
throw Exception('Unhandled error occurred');
}
}
+2 -1
View File
@@ -5,7 +5,8 @@ import 'package:tm_app/core/utils/result.dart';
import 'package:tm_app/data/services/mock_data/tender_detail_mock.dart'; import 'package:tm_app/data/services/mock_data/tender_detail_mock.dart';
import 'package:tm_app/data/services/model/tender_detail_request/tender_detail_request.dart'; import 'package:tm_app/data/services/model/tender_detail_request/tender_detail_request.dart';
import 'package:tm_app/data/services/model/tender_detail_response/tender_detail_response_model.dart'; import 'package:tm_app/data/services/model/tender_detail_response/tender_detail_response_model.dart';
import 'package:tm_app/data/services/network_manager.dart';
import '../../core/config/network_manager.dart';
class TenderDetailService { class TenderDetailService {
TenderDetailService({required NetworkManager networkManager}) TenderDetailService({required NetworkManager networkManager})
+2 -1
View File
@@ -10,7 +10,8 @@ import 'package:tm_app/data/services/model/tenders/tender_reject_response/tender
import 'package:tm_app/data/services/model/tenders/tender_submit_response/tender_submit_response.dart'; import 'package:tm_app/data/services/model/tenders/tender_submit_response/tender_submit_response.dart';
import 'package:tm_app/data/services/model/tenders/tenders_request/tenders_request.dart'; import 'package:tm_app/data/services/model/tenders/tenders_request/tenders_request.dart';
import 'package:tm_app/data/services/model/tenders/tenders_response/tenders_response.dart'; import 'package:tm_app/data/services/model/tenders/tenders_response/tenders_response.dart';
import 'package:tm_app/data/services/network_manager.dart';
import '../../core/config/network_manager.dart';
class TendersService { class TendersService {
TendersService({required NetworkManager networkManager}) TendersService({required NetworkManager networkManager})
+2 -30
View File
@@ -1,7 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/constants/strings.dart'; import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
@@ -9,6 +7,7 @@ import 'package:tm_app/view_models/tender_detail_view_model.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_action.dart'; import 'package:tm_app/views/detail/widgets/tender_detail_action.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_card.dart'; import 'package:tm_app/views/detail/widgets/tender_detail_card.dart';
import 'package:tm_app/views/detail/widgets/tender_detail_header.dart'; import 'package:tm_app/views/detail/widgets/tender_detail_header.dart';
import 'package:tm_app/views/shared/tender_app_bar.dart';
class TenderDetailMobilePage extends StatelessWidget { class TenderDetailMobilePage extends StatelessWidget {
final String tenderId; final String tenderId;
@@ -18,7 +17,7 @@ class TenderDetailMobilePage extends StatelessWidget {
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: AppColors.backgroundColor, backgroundColor: AppColors.backgroundColor,
appBar: _buildAppBar(context), appBar: appBar(context: context, title: AppStrings.tenderDetailTitle),
body: Consumer<TenderDetailViewModel>( body: Consumer<TenderDetailViewModel>(
builder: (context, tenderViewModel, child) { builder: (context, tenderViewModel, child) {
if (tenderViewModel.isLoading) { if (tenderViewModel.isLoading) {
@@ -55,31 +54,4 @@ class TenderDetailMobilePage extends StatelessWidget {
), ),
); );
} }
PreferredSizeWidget _buildAppBar(BuildContext context) {
return PreferredSize(
preferredSize: const Size.fromHeight(60),
child: AppBar(
backgroundColor: AppColors.backgroundColor,
elevation: 0,
titleSpacing: 0,
leading: IconButton(
icon: SvgPicture.asset(
AssetsManager.arrowLeft,
height: 24.0.w(),
width: 24.0.w(),
),
onPressed: () => Navigator.pop(context),
),
title: Text(
AppStrings.tenderDetailTitle,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w600,
fontSize: 20.0.sp(),
),
),
),
);
}
} }
+31
View File
@@ -1,4 +1,5 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:tm_app/core/constants/assets.dart'; import 'package:tm_app/core/constants/assets.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart'; import 'package:tm_app/core/utils/size_config.dart';
@@ -34,3 +35,33 @@ PreferredSizeWidget tenderMobileAppBar({required String title}) {
), ),
); );
} }
PreferredSizeWidget appBar({
required BuildContext context,
required String title,
}) {
return PreferredSize(
preferredSize: const Size.fromHeight(60),
child: AppBar(
backgroundColor: AppColors.backgroundColor,
elevation: 0,
titleSpacing: 0,
leading: IconButton(
icon: SvgPicture.asset(
AssetsManager.arrowLeft,
height: 24.0.w(),
width: 24.0.w(),
),
onPressed: () => Navigator.pop(context),
),
title: Text(
title,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w600,
fontSize: 20.0.sp(),
),
),
),
);
}
@@ -1,16 +1,13 @@
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_svg/svg.dart';
import 'package:provider/provider.dart'; import 'package:provider/provider.dart';
import 'package:tm_app/core/constants/strings.dart'; import 'package:tm_app/core/constants/strings.dart';
import 'package:tm_app/core/theme/colors.dart'; import 'package:tm_app/core/theme/colors.dart';
import 'package:tm_app/core/utils/size_config.dart';
import 'package:tm_app/view_models/your_tenders_view_model.dart'; import 'package:tm_app/view_models/your_tenders_view_model.dart';
import 'package:tm_app/views/shared/main_tab_bar.dart'; import 'package:tm_app/views/shared/main_tab_bar.dart';
import 'package:tm_app/views/shared/tender_app_bar.dart';
import 'package:tm_app/views/your_tenders/widgets/approved_tenders.dart'; import 'package:tm_app/views/your_tenders/widgets/approved_tenders.dart';
import 'package:tm_app/views/your_tenders/widgets/tenders_submitted.dart'; import 'package:tm_app/views/your_tenders/widgets/tenders_submitted.dart';
import '../../../core/constants/assets.dart';
class YourTendersMobilePage extends StatefulWidget { class YourTendersMobilePage extends StatefulWidget {
const YourTendersMobilePage({super.key}); const YourTendersMobilePage({super.key});
@@ -42,7 +39,7 @@ class _YourTendersMobilePageState extends State<YourTendersMobilePage>
Widget build(BuildContext context) { Widget build(BuildContext context) {
return Scaffold( return Scaffold(
backgroundColor: AppColors.backgroundColor, backgroundColor: AppColors.backgroundColor,
appBar: _buildAppBar(context), appBar: appBar(context: context, title: AppStrings.yourTenders),
body: Column( body: Column(
children: [ children: [
MainTabBar( MainTabBar(
@@ -60,30 +57,3 @@ class _YourTendersMobilePageState extends State<YourTendersMobilePage>
); );
} }
} }
PreferredSizeWidget _buildAppBar(BuildContext context) {
return PreferredSize(
preferredSize: const Size.fromHeight(60),
child: AppBar(
backgroundColor: AppColors.backgroundColor,
elevation: 0,
titleSpacing: 0,
leading: IconButton(
icon: SvgPicture.asset(
AssetsManager.arrowLeft,
height: 24.0.w(),
width: 24.0.w(),
),
onPressed: () => Navigator.pop(context),
),
title: Text(
AppStrings.yourTenders,
style: TextStyle(
color: AppColors.grey70,
fontWeight: FontWeight.w600,
fontSize: 20.0.sp(),
),
),
),
);
}