From 4aeb9229a434b8b3888c38a4739a374358feb1c2 Mon Sep 17 00:00:00 2001 From: amirrezaghabeli Date: Tue, 12 Aug 2025 14:40:49 +0330 Subject: [PATCH] networking and error handling --- lib/core/config/dependencies.dart | 5 +- lib/core/config/network_manager.dart | 320 ++++++++++++++++++ lib/core/routes/app_routes.dart | 2 +- lib/core/utils/app_exceptions.dart | 97 +++++- lib/core/utils/network_connectivity.dart | 21 ++ lib/data/services/auth_service.dart | 2 +- lib/data/services/home_service.dart | 3 +- lib/data/services/network_manager.dart | 129 ------- lib/data/services/tender_detail_service.dart | 3 +- lib/data/services/tenders_service.dart | 3 +- .../detail/pages/detail_mobile_page.dart | 32 +- lib/views/shared/tender_app_bar.dart | 31 ++ .../pages/your_tenders_mobile_page.dart | 34 +- .../{ => pages}/your_tenders_screen.dart | 0 14 files changed, 477 insertions(+), 205 deletions(-) create mode 100644 lib/core/config/network_manager.dart create mode 100644 lib/core/utils/network_connectivity.dart delete mode 100644 lib/data/services/network_manager.dart rename lib/views/your_tenders/{ => pages}/your_tenders_screen.dart (100%) diff --git a/lib/core/config/dependencies.dart b/lib/core/config/dependencies.dart index 76a8283..8e31569 100644 --- a/lib/core/config/dependencies.dart +++ b/lib/core/config/dependencies.dart @@ -1,5 +1,6 @@ import 'package:provider/provider.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/data/repositories/home_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/services/auth_service.dart'; -import '../../data/services/network_manager.dart'; import '../../data/services/tenders_service.dart'; import '../../view_models/auth_view_model.dart'; import '../../view_models/home_view_model.dart'; @@ -87,9 +87,6 @@ List get viewModels { (context) => YourTendersViewModel(yourTendersRepository: context.read()), ), - ChangeNotifierProvider( - create: (context) => TendersViewModel(tendersRepository: context.read()), - ), ]; } diff --git a/lib/core/config/network_manager.dart b/lib/core/config/network_manager.dart new file mode 100644 index 0000000..e37a4fa --- /dev/null +++ b/lib/core/config/network_manager.dart @@ -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 init() async { + await addInterceptors(); + } + + Future 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> makeRequest( + String endpoint, + T Function(Map) fromJson, { + String method = 'GET', + FormData? data, + Map? 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 makeApiCall(String token, String url) async { + final headers = + token.isEmpty + ? {} + : {'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 _handleHttpError(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 _handleDioException(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) { + 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 makeSimpleRequest( + // String endpoint, + // T Function(Map) 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, + // }; + // } +} diff --git a/lib/core/routes/app_routes.dart b/lib/core/routes/app_routes.dart index a7883a6..a0d6c2f 100644 --- a/lib/core/routes/app_routes.dart +++ b/lib/core/routes/app_routes.dart @@ -6,7 +6,7 @@ import 'package:tm_app/views/login/pages/login_screen.dart'; import '../../views/profile/pages/profile_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'; part 'app_routes.g.dart'; diff --git a/lib/core/utils/app_exceptions.dart b/lib/core/utils/app_exceptions.dart index e17f0c4..1529f1c 100644 --- a/lib/core/utils/app_exceptions.dart +++ b/lib/core/utils/app_exceptions.dart @@ -1,8 +1,15 @@ class AppException implements Exception { final String message; 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 String toString() { @@ -11,11 +18,91 @@ class AppException implements Exception { } class FetchDataException extends AppException { - FetchDataException([String? message]) - : super(message ?? 'Error During Communication: ', 'Network Error'); + FetchDataException([String? message, int? statusCode, originalError]) + : super( + message ?? 'Error During Communication: ', + 'Network Error', + statusCode, + originalError, + ); } class AuthenticationException extends AppException { - AuthenticationException([String? message]) - : super(message ?? 'Authentication Failed.', 'Auth Error'); + AuthenticationException([String? message, int? statusCode, originalError]) + : 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, + ); } diff --git a/lib/core/utils/network_connectivity.dart b/lib/core/utils/network_connectivity.dart new file mode 100644 index 0000000..6dcc9b5 --- /dev/null +++ b/lib/core/utils/network_connectivity.dart @@ -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 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; + } + } +} diff --git a/lib/data/services/auth_service.dart b/lib/data/services/auth_service.dart index 7a03bbf..675f180 100644 --- a/lib/data/services/auth_service.dart +++ b/lib/data/services/auth_service.dart @@ -1,4 +1,4 @@ -import 'network_manager.dart'; +import '../../core/config/network_manager.dart'; class AuthService { AuthService({required NetworkManager networkManager}) diff --git a/lib/data/services/home_service.dart b/lib/data/services/home_service.dart index b579a0c..18076e5 100644 --- a/lib/data/services/home_service.dart +++ b/lib/data/services/home_service.dart @@ -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/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/network_manager.dart'; + +import '../../core/config/network_manager.dart'; class HomeService { HomeService({required NetworkManager networkManager}) diff --git a/lib/data/services/network_manager.dart b/lib/data/services/network_manager.dart deleted file mode 100644 index 9594504..0000000 --- a/lib/data/services/network_manager.dart +++ /dev/null @@ -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 init() async { - await addInterceptors(); - } - - Future addInterceptors() async { - final requestInterceptor = InterceptorsWrapper( - onRequest: (options, handler) { - // TODO: add more - return handler.next(options); - }, - ); - - mainDio.interceptors.add(requestInterceptor); - } - - Future makeRequest( - String endpoint, - T Function(Map) fromJson, { - String method = 'GET', - FormData? data, - }) async { - SharedPreferences prefs = await SharedPreferences.getInstance(); - - Dio dioInstance = mainDio; - - await addInterceptors(); - - Future makeApiCall(String token, String url) async { - final headers = - token.isEmpty - ? {} - : {'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'); - } -} diff --git a/lib/data/services/tender_detail_service.dart b/lib/data/services/tender_detail_service.dart index e6a2b7b..228e355 100644 --- a/lib/data/services/tender_detail_service.dart +++ b/lib/data/services/tender_detail_service.dart @@ -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/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/network_manager.dart'; + +import '../../core/config/network_manager.dart'; class TenderDetailService { TenderDetailService({required NetworkManager networkManager}) diff --git a/lib/data/services/tenders_service.dart b/lib/data/services/tenders_service.dart index e0cc355..c60c3d8 100644 --- a/lib/data/services/tenders_service.dart +++ b/lib/data/services/tenders_service.dart @@ -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/tenders_request/tenders_request.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 { TendersService({required NetworkManager networkManager}) diff --git a/lib/views/detail/pages/detail_mobile_page.dart b/lib/views/detail/pages/detail_mobile_page.dart index d4ef9a4..30c9960 100644 --- a/lib/views/detail/pages/detail_mobile_page.dart +++ b/lib/views/detail/pages/detail_mobile_page.dart @@ -1,7 +1,5 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/flutter_svg.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/theme/colors.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_card.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 { final String tenderId; @@ -18,7 +17,7 @@ class TenderDetailMobilePage extends StatelessWidget { Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.backgroundColor, - appBar: _buildAppBar(context), + appBar: appBar(context: context, title: AppStrings.tenderDetailTitle), body: Consumer( builder: (context, tenderViewModel, child) { 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(), - ), - ), - ), - ); - } } diff --git a/lib/views/shared/tender_app_bar.dart b/lib/views/shared/tender_app_bar.dart index b6adeee..8be706b 100644 --- a/lib/views/shared/tender_app_bar.dart +++ b/lib/views/shared/tender_app_bar.dart @@ -1,4 +1,5 @@ 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/theme/colors.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(), + ), + ), + ), + ); +} diff --git a/lib/views/your_tenders/pages/your_tenders_mobile_page.dart b/lib/views/your_tenders/pages/your_tenders_mobile_page.dart index 82be965..8b7e79e 100644 --- a/lib/views/your_tenders/pages/your_tenders_mobile_page.dart +++ b/lib/views/your_tenders/pages/your_tenders_mobile_page.dart @@ -1,16 +1,13 @@ import 'package:flutter/material.dart'; -import 'package:flutter_svg/svg.dart'; import 'package:provider/provider.dart'; import 'package:tm_app/core/constants/strings.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/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/tenders_submitted.dart'; -import '../../../core/constants/assets.dart'; - class YourTendersMobilePage extends StatefulWidget { const YourTendersMobilePage({super.key}); @@ -42,7 +39,7 @@ class _YourTendersMobilePageState extends State Widget build(BuildContext context) { return Scaffold( backgroundColor: AppColors.backgroundColor, - appBar: _buildAppBar(context), + appBar: appBar(context: context, title: AppStrings.yourTenders), body: Column( children: [ MainTabBar( @@ -60,30 +57,3 @@ class _YourTendersMobilePageState extends State ); } } - -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(), - ), - ), - ), - ); -} diff --git a/lib/views/your_tenders/your_tenders_screen.dart b/lib/views/your_tenders/pages/your_tenders_screen.dart similarity index 100% rename from lib/views/your_tenders/your_tenders_screen.dart rename to lib/views/your_tenders/pages/your_tenders_screen.dart