From cefbb686d926a2c6f4b1d8182e7e412d6939d119 Mon Sep 17 00:00:00 2001 From: Prajapati Chintan Date: Sat, 15 Oct 2022 02:07:50 -0700 Subject: [PATCH 01/15] added: central rest api service basic functionalities checked, works as intended. --- lib/env.dart | 7 +- lib/main.dart | 9 +- .../base/base_stateless.dart | 1 + .../services/rest_api/api.dart | 145 ++++++++++++++++++ .../services/rest_api/api_error.dart | 100 ++++++++++++ .../services/rest_api/demo/demo_api.dart | 25 +++ .../rest_api/demo/demo_controller.dart | 36 +++++ .../services/rest_api/demo/demo_ui.dart | 94 ++++++++++++ .../rest_api/models/api_error_type.dart | 13 ++ .../rest_api/models/api_response.dart | 56 +++++++ .../services/rest_api/models/token.dart | 19 +++ .../services/rest_api/self_signed.dart | 21 +++ pubspec.lock | 133 ++++++++++++++++ pubspec.yaml | 4 + 14 files changed, 658 insertions(+), 5 deletions(-) create mode 100644 lib/vaahextendflutter/services/rest_api/api.dart create mode 100644 lib/vaahextendflutter/services/rest_api/api_error.dart create mode 100644 lib/vaahextendflutter/services/rest_api/demo/demo_api.dart create mode 100644 lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart create mode 100644 lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart create mode 100644 lib/vaahextendflutter/services/rest_api/models/api_error_type.dart create mode 100644 lib/vaahextendflutter/services/rest_api/models/api_response.dart create mode 100644 lib/vaahextendflutter/services/rest_api/models/token.dart create mode 100644 lib/vaahextendflutter/services/rest_api/self_signed.dart diff --git a/lib/env.dart b/lib/env.dart index de594dfd..fba41e39 100644 --- a/lib/env.dart +++ b/lib/env.dart @@ -16,10 +16,11 @@ EnvironmentConfig defaultConfig = const EnvironmentConfig( version: version, build: build, baseUrl: '', - apiBaseUrl: '', + apiBaseUrl: 'https://xrestapi.herokuapp.com', analyticsId: '', enableConsoleLogs: true, enableLocalLogs: true, + enableApiLogs: true, showEnvAndVersionTag: true, envAndVersionTagColor: Colors.red, ); @@ -78,6 +79,7 @@ class EnvironmentConfig { final String analyticsId; final bool enableConsoleLogs; final bool enableLocalLogs; + final bool enableApiLogs; final bool showEnvAndVersionTag; final Color envAndVersionTagColor; @@ -90,6 +92,7 @@ class EnvironmentConfig { required this.analyticsId, required this.enableConsoleLogs, required this.enableLocalLogs, + required this.enableApiLogs, required this.showEnvAndVersionTag, required this.envAndVersionTagColor, }); @@ -103,6 +106,7 @@ class EnvironmentConfig { String? analyticsId, bool? enableConsoleLogs, bool? enableLocalLogs, + bool? enableApiLogs, bool? showEnvAndVersionTag, Color? envAndVersionTagColor, }) { @@ -115,6 +119,7 @@ class EnvironmentConfig { analyticsId: analyticsId ?? this.analyticsId, enableConsoleLogs: enableConsoleLogs ?? this.enableConsoleLogs, enableLocalLogs: enableLocalLogs ?? this.enableLocalLogs, + enableApiLogs: enableApiLogs ?? this.enableApiLogs, showEnvAndVersionTag: showEnvAndVersionTag ?? this.showEnvAndVersionTag, envAndVersionTagColor: envAndVersionTagColor ?? this.envAndVersionTagColor, diff --git a/lib/main.dart b/lib/main.dart index a97d41d6..d74c47de 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -2,8 +2,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'env.dart'; -import 'vaahextendflutter/log/console.dart'; import 'vaahextendflutter/base/base_stateful.dart'; +import 'vaahextendflutter/log/console.dart'; +import 'vaahextendflutter/services/rest_api/demo/demo_ui.dart'; import 'vaahextendflutter/tag/tag.dart'; void main() { @@ -48,9 +49,9 @@ class _TeamHomePageState extends BaseStateful { late EnvController envController; @override - void initState() { + void afterFirstBuild(BuildContext context) { envController = Get.find(); - super.initState(); + super.afterFirstBuild(context); } @override @@ -62,7 +63,7 @@ class _TeamHomePageState extends BaseStateful { alignment: Alignment.topCenter, margin: EdgeInsets.all(10), child: Center( - child: Text('Webreinvent'), + child: DemoUI(), ), ), ); diff --git a/lib/vaahextendflutter/base/base_stateless.dart b/lib/vaahextendflutter/base/base_stateless.dart index fb407dcb..e83c2e1d 100644 --- a/lib/vaahextendflutter/base/base_stateless.dart +++ b/lib/vaahextendflutter/base/base_stateless.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; + import '../services/screen_util.dart'; abstract class BaseStateless extends StatelessWidget with DynamicSize { diff --git a/lib/vaahextendflutter/services/rest_api/api.dart b/lib/vaahextendflutter/services/rest_api/api.dart new file mode 100644 index 00000000..b6f4cae9 --- /dev/null +++ b/lib/vaahextendflutter/services/rest_api/api.dart @@ -0,0 +1,145 @@ +import 'dart:async'; +import 'dart:io'; + +import 'package:cookie_jar/cookie_jar.dart'; +import 'package:dio/dio.dart'; +import 'package:dio_cookie_manager/dio_cookie_manager.dart'; +import 'package:get/get.dart' as getx; +import 'package:path_provider/path_provider.dart'; +import '../../log/console.dart'; + +import '../../../env.dart'; +import 'models/token.dart'; + +class Api { + Api() { + bool envControllerExists = getx.Get.isRegistered(); + if (!envControllerExists) { + throw Exception('envController does not exist in app'); + } + // get env controller and set variable showEnvAndVersionTag + EnvController envController = getx.Get.find(); + apiBaseUrl = envController.config.apiBaseUrl; + if (envController.config.enableApiLogs) { + dio.interceptors.add( + LogInterceptor( + responseBody: true, + requestBody: true, + ), + ); + } + getApplicationDocumentsDirectory().then( + (Directory appDocDir) async { + final String appDocPath = appDocDir.path; + final String cookiePath = '$appDocPath/cookies'; + final Directory dir = Directory(cookiePath); + await dir.create(); + cookieJar = PersistCookieJar( + storage: FileStorage(cookiePath), + ); + dio.interceptors.add( + CookieManager(cookieJar), + ); + dio.interceptors.add( + InterceptorsWrapper( + onResponse: (Response response, handler) async { + final String urlPath = response.requestOptions.path; + final List cookies = + await cookieJar.loadForRequest(Uri.parse(urlPath)); + final String? xsrfToken = cookies + .firstWhereOrNull( + (Cookie c) => c.name == 'XSRF-TOKEN', + ) + ?.value; + // Set dio auth header token once time + if (xsrfToken != null) { + // The XSRF-TOKEN got from cookie requires decoded before add to header + dio.options.headers['X-XSRF-TOKEN'] = + Uri.decodeComponent(xsrfToken); + String cookieStr = ''; + for (int i = 0; i < cookies.length; i++) { + final Cookie c = cookies[i]; + cookieStr += '${c.name}=${c.value}; '; + } + dio.options.headers['Cookie'] = cookieStr; + } + return; + }, + ), + ); + }, + ); + } + + // Credential info + Token? token; + + // Get base url by env + late final String apiBaseUrl; + final Dio dio = Dio(); + late PersistCookieJar cookieJar; + + // Get request header options + Future getOptions( + {String contentType = Headers.jsonContentType}) async { + final Map header = {}; + header.addAll({'Accept': 'application/json'}); + header.addAll({'X-Requested-With': 'XMLHttpRequest'}); + return Options(headers: header, contentType: contentType); + } + + // Get auth header options + Future getAuthOptions({required String contentType}) async { + final Options options = await getOptions(contentType: contentType); + + if (token != null) { + options.headers?.addAll( + {'Authorization': 'Bearer ${token?.bearerToken}'}); + } + + return options; + } + + // Wrap Dio Exception + Future> wrapE(Future> Function() dioApi) async { + try { + return await dioApi(); + } catch (error) { + if (error is DioError && error.type == DioErrorType.response) { + final Response? response = error.response; + try { + // By pass dio header error code to get response content + // Try to return response + if (response != null) { + final Response res = Response( + data: response.data as T, + headers: response.headers, + requestOptions: response.requestOptions, + isRedirect: response.isRedirect, + statusCode: response.statusCode, + statusMessage: response.statusMessage, + redirects: response.redirects, + extra: response.extra, + ); + throw DioError( + requestOptions: error.requestOptions, + response: res, + type: error.type, + error: res.statusMessage, + ); + } + throw DioError( + requestOptions: error.requestOptions, + response: error.response, + type: error.type, + error: response?.statusMessage, + ); + } catch (e) { + rethrow; + // ignore cast error type + } + } + rethrow; + } + } +} diff --git a/lib/vaahextendflutter/services/rest_api/api_error.dart b/lib/vaahextendflutter/services/rest_api/api_error.dart new file mode 100644 index 00000000..6e20cd99 --- /dev/null +++ b/lib/vaahextendflutter/services/rest_api/api_error.dart @@ -0,0 +1,100 @@ +import 'dart:async'; + +import 'package:dio/dio.dart'; + +import '../../log/console.dart'; +import 'models/api_error_type.dart'; + +mixin ApiError { + /// This function was called when trigger safeCallApi + /// and apiError = true as default + Future onApiError(dynamic error); + + /// Call api safety with error handling. + /// Required: + /// - dioApi: call async dio function + /// Optional: + /// - onStart: the function executed before api, can be null + /// - onError: the function executed in case api crashed, can be null + /// - onCompleted: the function executed after api or before crashing, can be null + /// - onFinally: the function executed end of function, can be null + /// - skipOnError: false as default if you want to forward the error to onApiError + Future apiCallSafety( + Future Function() dioApi, { + Future Function()? onStart, + Future Function(dynamic error)? onError, + Future Function(bool status, T? res)? onCompleted, + Future Function()? onFinally, + bool skipOnError = false, + }) async { + try { + /// On start, use for show loading + if (onStart != null) { + await onStart(); + } + + /// Execute api + final T res = await dioApi(); + + /// On completed, use for hide loading + if (onCompleted != null) { + await onCompleted(true, res); + } + + /// Return api response + return res; + } catch (error) { + /// In case error: + /// On completed, use for hide loading + if (onCompleted != null) { + await onCompleted(false, null); + } + + /// On inline error + if (onError != null) { + await onError(error); + } + + /// Call onApiError + if (skipOnError == false) { + onApiError(error); + } + + return null; + } finally { + /// Call finally function + if (onFinally != null) { + await onFinally(); + } + } + } + + /// Parsing error to ErrorType + ApiErrorType parseApiErrorType(dynamic error) { + if (error is DioError && error.type == DioErrorType.response) { + ApiErrorCode errorCode = ApiErrorCode.unknown; + String message = error.message; + if (error.response?.statusCode == 401) { + errorCode = ApiErrorCode.unauthorized; + } + if (error.response?.data != null) { + try { + final Map response = + error.response?.data as Map; + message = response['error'] ?? ''; + } catch (e) { + Console.danger( + e.toString(), + ); + // ignore parsing error + } + } + return ApiErrorType(code: errorCode, message: message); + } else { + Console.danger( + error.toString(), + ); + } + return ApiErrorType(); + } +} diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_api.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_api.dart new file mode 100644 index 00000000..7f302cb9 --- /dev/null +++ b/lib/vaahextendflutter/services/rest_api/demo/demo_api.dart @@ -0,0 +1,25 @@ +import 'package:dio/dio.dart'; + +import '../api.dart'; + +class DemoApi extends Api { + /// How to get X-XSRF-TOKEN + /// GET https://staging.theavenue.live/sanctum/csrf-cookie + /// - Parse header cookie 'X-XSRF-TOKEN' => encoded_token + /// - Use url decode 'encoded_token' to get auth Token + /// - Add X-XSRF-TOKEN to header for any call + /// Note with POST api header: + /// - X-Requested-With=XMLHttpRequest + /// - Content-Type=application/json + Future getXSRFToken() async { + final Options options = await getOptions(); + return wrapE(() => + dio.get('$apiBaseUrl/auth/csrf-cookie', options: options)); + } + + Future getError() async { + final Options options = await getOptions(); + return wrapE( + () => dio.get('$apiBaseUrl/error?code=400', options: options)); + } +} diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart new file mode 100644 index 00000000..fc79587b --- /dev/null +++ b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart @@ -0,0 +1,36 @@ +import 'package:dio/dio.dart'; +import 'package:get/get.dart' as getx; +import '../../../log/console.dart'; + +import '../models/api_response.dart'; +import 'demo_api.dart'; + +class DemoController extends getx.GetxController { + Future getDemoURL() async { + // Call API + DemoApi api = DemoApi(); + final Response result = + await api.getError().timeout(const Duration(seconds: 180)); + Console.info( + result.toString(), + ); + // final DecodedResponse response = DecodedResponse(result.data); + // if (response.data != null) {} + } +} + +class DecodedResponse extends BaseResponse> { + DecodedResponse(Map? fullJson) : super(fullJson); + + @override + List jsonToData(dynamic dataJson) { + final List? dataList = dataJson as List?; + return dataList != null + ? List.from( + dataList.map( + (dynamic x) => x.toString(), + ), + ) + : []; + } +} diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart new file mode 100644 index 00000000..b8cfbb29 --- /dev/null +++ b/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart @@ -0,0 +1,94 @@ +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; +import '../../../base/base_stateful.dart'; +import '../../../log/console.dart'; +import '../api_error.dart'; +import 'demo_controller.dart'; +import '../models/api_error_type.dart'; + +class DemoUI extends StatefulWidget { + const DemoUI({super.key}); + + @override + State createState() => _DemoUIState(); +} + +class _DemoUIState extends BaseStateful with ApiError { + @override + Future onApiError(dynamic error) async { + final ApiErrorType errorType = parseApiErrorType(error); + if (errorType.message.isNotEmpty) { + Console.danger('>>>>> code: ${errorType.code}'); + Console.danger('>>>>> message: ${errorType.message}'); + + await _showMyDialog(); + } + if (errorType.code == ApiErrorCode.unauthorized) { + // TODO: Logout + return 1; + } + return 0; + } + + DemoController? _controller; + @override + void afterFirstBuild(BuildContext context) { + Get.put(DemoController()); + _controller = Get.find(); + load(); + super.afterFirstBuild(context); + } + + Future load({ + bool showLoading = true, + }) async { + await apiCallSafety( + () => _controller!.getDemoURL(), + onStart: () async { + if (showLoading) { + // AppLoadingProvider.show(context); + } + }, + onCompleted: (bool res, void _) async { + if (showLoading) { + // AppLoadingProvider.hide(context); + } + }, + ); + } + + @override + Widget build(BuildContext context) { + return Container(); + } + + Future _showMyDialog() async { + Console.danger('>>>>> dialogue'); + + return showDialog( + context: context, + barrierDismissible: false, // user must tap button! + builder: (BuildContext context) { + return AlertDialog( + title: const Text('AlertDialog Title'), + content: SingleChildScrollView( + child: ListBody( + children: const [ + Text('This is a demo alert dialog.'), + Text('Would you like to approve of this message?'), + ], + ), + ), + actions: [ + TextButton( + child: const Text('Approve'), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/vaahextendflutter/services/rest_api/models/api_error_type.dart b/lib/vaahextendflutter/services/rest_api/models/api_error_type.dart new file mode 100644 index 00000000..ef342dc5 --- /dev/null +++ b/lib/vaahextendflutter/services/rest_api/models/api_error_type.dart @@ -0,0 +1,13 @@ +enum ApiErrorCode { unknown, unauthorized } + +class ApiErrorType { + ApiErrorType({this.code = ApiErrorCode.unknown, this.message = 'Unknown'}); + + final ApiErrorCode code; + final String message; + + @override + String toString() { + return 'ApiErrorType{code: $code, message: $message}'; + } +} diff --git a/lib/vaahextendflutter/services/rest_api/models/api_response.dart b/lib/vaahextendflutter/services/rest_api/models/api_response.dart new file mode 100644 index 00000000..c26426d0 --- /dev/null +++ b/lib/vaahextendflutter/services/rest_api/models/api_response.dart @@ -0,0 +1,56 @@ +import 'dart:core'; + +class BaseResponse { + BaseResponse( + Map? fullJson, { + String? dataKey, + String errorKey = 'error', + }) { + parsing(fullJson, dataKey: dataKey, errorKey: errorKey); + } + + T? data; + late bool success; + late String error; + late String message; + + // Abstract json to data + T? jsonToData(dynamic dataJson) { + return null; + } + + // Abstract data to json + dynamic dataToJson(T? data) { + return null; + } + + // Parsing data to object + // dataKey = null mean parse from root + dynamic parsing( + Map? fullJson, { + String? dataKey, + String errorKey = 'error', + }) { + if (fullJson != null) { + final dynamic dataJson = + dataKey != null ? fullJson[dataKey] : fullJson['data']; + data = dataJson != null ? jsonToData(dataJson) : null; + success = fullJson['success'] as bool; + error = fullJson[errorKey] as String; + message = fullJson['message'] as String; + } + } + + // Data to json + Map toJson() => { + 'data': data != null ? dataToJson(data) : null, + 'success': success, + 'message': message, + 'error': error, + }; + + @override + String toString() { + return 'BaseResponse{data: $data, success:$success, message: $message, error: $error}'; + } +} diff --git a/lib/vaahextendflutter/services/rest_api/models/token.dart b/lib/vaahextendflutter/services/rest_api/models/token.dart new file mode 100644 index 00000000..5b3795e5 --- /dev/null +++ b/lib/vaahextendflutter/services/rest_api/models/token.dart @@ -0,0 +1,19 @@ +class Token { + Token({this.bearerToken}); + + factory Token.fromJson(Map json) => Token( + bearerToken: json['bearerToken'], + ); + + static const String localKey = 'token'; + + String? bearerToken; + + Map toJson() => + {'bearerToken': bearerToken}; + + @override + String toString() { + return 'Token{bearerToken: $bearerToken}'; + } +} diff --git a/lib/vaahextendflutter/services/rest_api/self_signed.dart b/lib/vaahextendflutter/services/rest_api/self_signed.dart new file mode 100644 index 00000000..8d3bb250 --- /dev/null +++ b/lib/vaahextendflutter/services/rest_api/self_signed.dart @@ -0,0 +1,21 @@ +import 'dart:io'; + +/// How to use: override http client global before run app +/// void main() { +/// HttpOverrides.global = SelfSignedHttps(); +/// runApp(MyApp()); +/// } + +class SelfSignedHttps extends HttpOverrides { + @override + HttpClient createHttpClient(SecurityContext? context) { + final HttpClient client = super.createHttpClient(context); + client.badCertificateCallback = ( + X509Certificate cert, + String host, + int port, + ) => + true; + return client; + } +} diff --git a/pubspec.lock b/pubspec.lock index db6d198f..f8fcd6ac 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -43,6 +43,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0" + cookie_jar: + dependency: "direct main" + description: + name: cookie_jar + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" cupertino_icons: dependency: "direct main" description: @@ -50,6 +57,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.5" + dio: + dependency: "direct main" + description: + name: dio + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.6" + dio_cookie_manager: + dependency: "direct main" + description: + name: dio_cookie_manager + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.0" fake_async: dependency: transitive description: @@ -57,6 +78,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" + ffi: + dependency: transitive + description: + name: ffi + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.1" + file: + dependency: transitive + description: + name: file + url: "https://pub.dartlang.org" + source: hosted + version: "6.1.4" flutter: dependency: "direct main" description: flutter @@ -88,6 +123,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.6.5" + http_parser: + dependency: transitive + description: + name: http_parser + url: "https://pub.dartlang.org" + source: hosted + version: "4.0.2" lints: dependency: transitive description: @@ -123,6 +165,76 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.2" + path_provider: + dependency: "direct main" + description: + name: path_provider + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + path_provider_android: + dependency: transitive + description: + name: path_provider_android + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.20" + path_provider_ios: + dependency: transitive + description: + name: path_provider_ios + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.11" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.7" + path_provider_macos: + dependency: transitive + description: + name: path_provider_macos + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.6" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.5" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + platform: + dependency: transitive + description: + name: platform + url: "https://pub.dartlang.org" + source: hosted + version: "3.1.0" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "2.1.3" + process: + dependency: transitive + description: + name: process + url: "https://pub.dartlang.org" + source: hosted + version: "4.2.4" sky_engine: dependency: transitive description: flutter @@ -170,6 +282,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "0.4.12" + typed_data: + dependency: transitive + description: + name: typed_data + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.1" vector_math: dependency: transitive description: @@ -177,6 +296,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" + win32: + dependency: transitive + description: + name: win32 + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.0+2" sdks: dart: ">=2.18.2 <3.0.0" flutter: ">=3.3.4" diff --git a/pubspec.yaml b/pubspec.yaml index 57616f12..005a5a8d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,10 @@ dependencies: get: ^4.6.5 colorize: ^3.0.0 flutter_screenutil: ^5.5.4 + cookie_jar: ^3.0.1 + dio: ^4.0.6 + path_provider: ^2.0.11 + dio_cookie_manager: ^2.0.0 dev_dependencies: flutter_test: From 044079816a71fff074158ec8e28775bc7d414c8a Mon Sep 17 00:00:00 2001 From: Prajapati Chintan Date: Sat, 15 Oct 2022 03:19:15 -0700 Subject: [PATCH 02/15] updated: refactored some code --- .../services/rest_api/api.dart | 31 +++++++------ .../rest_api/demo/demo_controller.dart | 2 +- .../services/rest_api/demo/demo_ui.dart | 45 ++++++++++--------- 3 files changed, 40 insertions(+), 38 deletions(-) diff --git a/lib/vaahextendflutter/services/rest_api/api.dart b/lib/vaahextendflutter/services/rest_api/api.dart index b6f4cae9..a4235067 100644 --- a/lib/vaahextendflutter/services/rest_api/api.dart +++ b/lib/vaahextendflutter/services/rest_api/api.dart @@ -6,7 +6,6 @@ import 'package:dio/dio.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:get/get.dart' as getx; import 'package:path_provider/path_provider.dart'; -import '../../log/console.dart'; import '../../../env.dart'; import 'models/token.dart'; @@ -110,29 +109,29 @@ class Api { try { // By pass dio header error code to get response content // Try to return response - if (response != null) { - final Response res = Response( - data: response.data as T, - headers: response.headers, - requestOptions: response.requestOptions, - isRedirect: response.isRedirect, - statusCode: response.statusCode, - statusMessage: response.statusMessage, - redirects: response.redirects, - extra: response.extra, - ); + if (response == null) { throw DioError( requestOptions: error.requestOptions, - response: res, + response: error.response, type: error.type, - error: res.statusMessage, + error: response?.statusMessage, ); } + final Response res = Response( + data: response.data as T, + headers: response.headers, + requestOptions: response.requestOptions, + isRedirect: response.isRedirect, + statusCode: response.statusCode, + statusMessage: response.statusMessage, + redirects: response.redirects, + extra: response.extra, + ); throw DioError( requestOptions: error.requestOptions, - response: error.response, + response: res, type: error.type, - error: response?.statusMessage, + error: res.statusMessage, ); } catch (e) { rethrow; diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart index fc79587b..54b6187f 100644 --- a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart +++ b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart @@ -1,7 +1,7 @@ import 'package:dio/dio.dart'; import 'package:get/get.dart' as getx; -import '../../../log/console.dart'; +import '../../../log/console.dart'; import '../models/api_response.dart'; import 'demo_api.dart'; diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart index b8cfbb29..9e5ce441 100644 --- a/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart +++ b/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart @@ -1,10 +1,11 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; + import '../../../base/base_stateful.dart'; import '../../../log/console.dart'; import '../api_error.dart'; -import 'demo_controller.dart'; import '../models/api_error_type.dart'; +import 'demo_controller.dart'; class DemoUI extends StatefulWidget { const DemoUI({super.key}); @@ -20,8 +21,10 @@ class _DemoUIState extends BaseStateful with ApiError { if (errorType.message.isNotEmpty) { Console.danger('>>>>> code: ${errorType.code}'); Console.danger('>>>>> message: ${errorType.message}'); - - await _showMyDialog(); + await _showErrorDialog( + title: 'Error', + content: errorType.message, + ); } if (errorType.code == ApiErrorCode.unauthorized) { // TODO: Logout @@ -54,38 +57,38 @@ class _DemoUIState extends BaseStateful with ApiError { // AppLoadingProvider.hide(context); } }, + skipOnError: false, ); } @override Widget build(BuildContext context) { + super.build(context); return Container(); } - Future _showMyDialog() async { - Console.danger('>>>>> dialogue'); - + Future _showErrorDialog({ + required String title, + required String content, + List? actions, + }) async { return showDialog( context: context, barrierDismissible: false, // user must tap button! builder: (BuildContext context) { return AlertDialog( - title: const Text('AlertDialog Title'), - content: SingleChildScrollView( - child: ListBody( - children: const [ - Text('This is a demo alert dialog.'), - Text('Would you like to approve of this message?'), - ], - ), - ), + title: Text(title), + content: Text(content), actions: [ - TextButton( - child: const Text('Approve'), - onPressed: () { - Navigator.of(context).pop(); - }, - ), + if (actions == null || actions.isNotEmpty) + TextButton( + child: const Text('Okay'), + onPressed: () { + Navigator.of(context).pop(); + }, + ) + else + ...actions, ], ); }, From d63779f4c7cbe68144344912cbfd8b181551fca1 Mon Sep 17 00:00:00 2001 From: Prajapati Chintan Date: Sat, 15 Oct 2022 07:49:08 -0700 Subject: [PATCH 03/15] updated: cleaned the code, removed unnecessary files ow no wrapper needed in controllers you pass Api class instance and using that instance you can call any requests --- lib/env.dart | 5 + .../docs/assets/diagrams/ajax.drawio | 1 + .../docs/assets/diagrams/ajax.drawio.png | Bin 0 -> 111782 bytes .../services/rest_api/api.dart | 356 ++++++++++++++---- .../services/rest_api/api_error.dart | 100 ----- .../services/rest_api/demo/demo_api.dart | 25 -- .../rest_api/demo/demo_controller.dart | 14 +- .../services/rest_api/demo/demo_ui.dart | 66 +--- .../services/rest_api/models/token.dart | 19 - 9 files changed, 299 insertions(+), 287 deletions(-) create mode 100644 lib/vaahextendflutter/docs/assets/diagrams/ajax.drawio create mode 100644 lib/vaahextendflutter/docs/assets/diagrams/ajax.drawio.png delete mode 100644 lib/vaahextendflutter/services/rest_api/api_error.dart delete mode 100644 lib/vaahextendflutter/services/rest_api/demo/demo_api.dart delete mode 100644 lib/vaahextendflutter/services/rest_api/models/token.dart diff --git a/lib/env.dart b/lib/env.dart index fba41e39..363fb213 100644 --- a/lib/env.dart +++ b/lib/env.dart @@ -17,6 +17,7 @@ EnvironmentConfig defaultConfig = const EnvironmentConfig( build: build, baseUrl: '', apiBaseUrl: 'https://xrestapi.herokuapp.com', + timeoutLimit: 180, analyticsId: '', enableConsoleLogs: true, enableLocalLogs: true, @@ -76,6 +77,7 @@ class EnvironmentConfig { final String build; final String baseUrl; final String apiBaseUrl; + final int timeoutLimit; // in seconds final String analyticsId; final bool enableConsoleLogs; final bool enableLocalLogs; @@ -89,6 +91,7 @@ class EnvironmentConfig { required this.build, required this.baseUrl, required this.apiBaseUrl, + required this.timeoutLimit, required this.analyticsId, required this.enableConsoleLogs, required this.enableLocalLogs, @@ -103,6 +106,7 @@ class EnvironmentConfig { String? build, String? baseUrl, String? apiBaseUrl, + int? timeoutLimit, String? analyticsId, bool? enableConsoleLogs, bool? enableLocalLogs, @@ -116,6 +120,7 @@ class EnvironmentConfig { build: build ?? this.build, baseUrl: baseUrl ?? this.baseUrl, apiBaseUrl: apiBaseUrl ?? this.apiBaseUrl, + timeoutLimit: timeoutLimit ?? this.timeoutLimit, analyticsId: analyticsId ?? this.analyticsId, enableConsoleLogs: enableConsoleLogs ?? this.enableConsoleLogs, enableLocalLogs: enableLocalLogs ?? this.enableLocalLogs, diff --git a/lib/vaahextendflutter/docs/assets/diagrams/ajax.drawio b/lib/vaahextendflutter/docs/assets/diagrams/ajax.drawio new file mode 100644 index 00000000..06f5f971 --- /dev/null +++ b/lib/vaahextendflutter/docs/assets/diagrams/ajax.drawio @@ -0,0 +1 @@ +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 \ No newline at end of file diff --git a/lib/vaahextendflutter/docs/assets/diagrams/ajax.drawio.png b/lib/vaahextendflutter/docs/assets/diagrams/ajax.drawio.png new file mode 100644 index 0000000000000000000000000000000000000000..a01da55a724f36ec92084e3a15d7520db490e2d4 GIT binary patch literal 111782 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 literal 0 HcmV?d00001 diff --git a/lib/vaahextendflutter/services/rest_api/api.dart b/lib/vaahextendflutter/services/rest_api/api.dart index a4235067..adf7bd2b 100644 --- a/lib/vaahextendflutter/services/rest_api/api.dart +++ b/lib/vaahextendflutter/services/rest_api/api.dart @@ -4,20 +4,300 @@ import 'dart:io'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; +import 'package:flutter/cupertino.dart'; import 'package:get/get.dart' as getx; import 'package:path_provider/path_provider.dart'; +import 'models/api_error_type.dart'; import '../../../env.dart'; -import 'models/token.dart'; + +enum RequestType { get, post, put, patch, delete } class Api { + late EnvController envController; + + // Get base url by env + late final String apiBaseUrl; + final Dio dio = Dio(); + late PersistCookieJar cookieJar; + Api() { + _initApi(); + } + + // Get request header options + Future _getOptions( + {String contentType = Headers.jsonContentType}) async { + final Map header = {}; + header.addAll({'Accept': 'application/json'}); + header.addAll({'X-Requested-With': 'XMLHttpRequest'}); + return Options(headers: header, contentType: contentType); + } + + Future ajax({ + required BuildContext context, + required String url, + Map? data, + Map? queryParameters, + RequestType requestType = RequestType.get, + bool skipOnError = true, // if true then error dialogue won't be + bool showSuccessDialogue = + false, // if false on success nothing will be shown + bool showErrorDialogue = true, // if false on error nothing will be shown + Function? + customSuccessDialogue, // if passed will be showed this instead of default + Function? + customErrorDialogue, // if passed will be showed this instead of default + int? customTimeoutLimit, + Future Function()? onStart, + Future Function(dynamic error)? onError, + Future Function(bool status, Response? res)? onCompleted, + Future Function()? onFinally, + }) async { + try { + // On Start, use for show loading + if (onStart != null) { + await onStart(); + } + Response? response; + final Options options = await _getOptions(); + switch (requestType) { + case RequestType.get: + response = await dio + .get( + '$apiBaseUrl$url', + queryParameters: queryParameters, + options: options, + ) + .timeout( + Duration( + seconds: + customTimeoutLimit ?? envController.config.timeoutLimit, + ), + ); + break; + + case RequestType.post: + if (data == null) 'invalid data'; //TODO: + response = await dio + .post( + '$apiBaseUrl$url', + data: data, + queryParameters: queryParameters, + options: options, + ) + .timeout( + Duration( + seconds: + customTimeoutLimit ?? envController.config.timeoutLimit, + ), + ); + break; + + case RequestType.put: + if (data == null) 'invalid data'; //TODO: + response = await dio + .put( + '$apiBaseUrl$url', + data: data, + queryParameters: queryParameters, + options: options, + ) + .timeout( + Duration( + seconds: + customTimeoutLimit ?? envController.config.timeoutLimit, + ), + ); + break; + + case RequestType.patch: + if (data == null) 'invalid data'; //TODO: + response = await dio + .patch( + '$apiBaseUrl$url', + data: data, + queryParameters: queryParameters, + options: options, + ) + .timeout( + Duration( + seconds: + customTimeoutLimit ?? envController.config.timeoutLimit, + ), + ); + break; + + case RequestType.delete: + if (data == null) 'invalid data'; //TODO: + response = await dio + .delete( + '$apiBaseUrl$url', + data: data, + queryParameters: queryParameters, + options: options, + ) + .timeout( + Duration( + seconds: + customTimeoutLimit ?? envController.config.timeoutLimit, + ), + ); + break; + + default: + // TODO: Invalid request type, try again + break; + } + + // On completed, use for hide loading + if (onCompleted != null) { + await onCompleted(true, response); + } + return response; + // TODO: + } catch (error) { + // In case error: + // On completed, use for hide loading + if (onCompleted != null) { + await onCompleted(false, null); + } + + // On inline error + if (onError != null) { + await onError(error); + } + + if (error is DioError && error.type == DioErrorType.response) { + final Response? response = error.response; + try { + // By pass dio header error code to get response content + // Try to return response + if (response == null) { + throw DioError( + requestOptions: error.requestOptions, + response: error.response, + type: error.type, + error: response?.statusMessage, + ); + } + final Response res = Response( + data: response.data as T, + headers: response.headers, + requestOptions: response.requestOptions, + isRedirect: response.isRedirect, + statusCode: response.statusCode, + statusMessage: response.statusMessage, + redirects: response.redirects, + extra: response.extra, + ); + throw DioError( + requestOptions: error.requestOptions, + response: res, + type: error.type, + error: res.statusMessage, + ); + } catch (e) { + if (error.type == DioErrorType.response) { + ApiErrorCode errorCode = ApiErrorCode.unknown; + String message = error.message; + if (error.response?.statusCode == 401) { + errorCode = ApiErrorCode.unauthorized; + } + if (error.response?.data != null) { + try { + final Map response = + error.response?.data as Map; + message = response['error'] ?? ''; + } catch (e) { + // ignore parsing error + } + } + ApiErrorType apiErrorType = + ApiErrorType(code: errorCode, message: message); + if (apiErrorType.code == ApiErrorCode.unauthorized) { + // TODO: Logout + } + if (showErrorDialogue) { + if (customErrorDialogue != null) { + mainErrorDialogue( + skipOnError: skipOnError, + errorDialogue: () => customErrorDialogue(), + ); + } else { + mainErrorDialogue( + skipOnError: skipOnError, + errorDialogue: () => defaultErrorDialogue( + context: context, + title: 'Error', + content: apiErrorType.message, + ), + ); + } + } + } else { + rethrow; + } + } + } + } finally { + /// Call finally function + if (onFinally != null) { + await onFinally(); + } + } + return null; + } + + mainErrorDialogue( + {required bool skipOnError, required Function() errorDialogue}) async { + if (skipOnError) { + await errorDialogue(); + } else { + await errorDialogue(); + mainErrorDialogue( + skipOnError: skipOnError, + errorDialogue: () => errorDialogue, + ); + } + } + + defaultErrorDialogue({ + required BuildContext context, + required String title, + required String content, + List? actions, + }) { + return showCupertinoDialog( + context: context, + barrierDismissible: false, // user must tap button! + builder: (BuildContext context) { + return CupertinoAlertDialog( + title: Text(title), + content: Text(content), + actions: [ + if (actions == null || actions.isNotEmpty) + CupertinoButton( + child: const Text('Okay'), + onPressed: () { + Navigator.of(context).pop(); + }, + ) + else + ...actions, + ], + ); + }, + ); + } + + Future _initApi() async { bool envControllerExists = getx.Get.isRegistered(); if (!envControllerExists) { throw Exception('envController does not exist in app'); } // get env controller and set variable showEnvAndVersionTag - EnvController envController = getx.Get.find(); + envController = getx.Get.find(); apiBaseUrl = envController.config.apiBaseUrl; if (envController.config.enableApiLogs) { dio.interceptors.add( @@ -69,76 +349,4 @@ class Api { }, ); } - - // Credential info - Token? token; - - // Get base url by env - late final String apiBaseUrl; - final Dio dio = Dio(); - late PersistCookieJar cookieJar; - - // Get request header options - Future getOptions( - {String contentType = Headers.jsonContentType}) async { - final Map header = {}; - header.addAll({'Accept': 'application/json'}); - header.addAll({'X-Requested-With': 'XMLHttpRequest'}); - return Options(headers: header, contentType: contentType); - } - - // Get auth header options - Future getAuthOptions({required String contentType}) async { - final Options options = await getOptions(contentType: contentType); - - if (token != null) { - options.headers?.addAll( - {'Authorization': 'Bearer ${token?.bearerToken}'}); - } - - return options; - } - - // Wrap Dio Exception - Future> wrapE(Future> Function() dioApi) async { - try { - return await dioApi(); - } catch (error) { - if (error is DioError && error.type == DioErrorType.response) { - final Response? response = error.response; - try { - // By pass dio header error code to get response content - // Try to return response - if (response == null) { - throw DioError( - requestOptions: error.requestOptions, - response: error.response, - type: error.type, - error: response?.statusMessage, - ); - } - final Response res = Response( - data: response.data as T, - headers: response.headers, - requestOptions: response.requestOptions, - isRedirect: response.isRedirect, - statusCode: response.statusCode, - statusMessage: response.statusMessage, - redirects: response.redirects, - extra: response.extra, - ); - throw DioError( - requestOptions: error.requestOptions, - response: res, - type: error.type, - error: res.statusMessage, - ); - } catch (e) { - rethrow; - // ignore cast error type - } - } - rethrow; - } - } } diff --git a/lib/vaahextendflutter/services/rest_api/api_error.dart b/lib/vaahextendflutter/services/rest_api/api_error.dart deleted file mode 100644 index 6e20cd99..00000000 --- a/lib/vaahextendflutter/services/rest_api/api_error.dart +++ /dev/null @@ -1,100 +0,0 @@ -import 'dart:async'; - -import 'package:dio/dio.dart'; - -import '../../log/console.dart'; -import 'models/api_error_type.dart'; - -mixin ApiError { - /// This function was called when trigger safeCallApi - /// and apiError = true as default - Future onApiError(dynamic error); - - /// Call api safety with error handling. - /// Required: - /// - dioApi: call async dio function - /// Optional: - /// - onStart: the function executed before api, can be null - /// - onError: the function executed in case api crashed, can be null - /// - onCompleted: the function executed after api or before crashing, can be null - /// - onFinally: the function executed end of function, can be null - /// - skipOnError: false as default if you want to forward the error to onApiError - Future apiCallSafety( - Future Function() dioApi, { - Future Function()? onStart, - Future Function(dynamic error)? onError, - Future Function(bool status, T? res)? onCompleted, - Future Function()? onFinally, - bool skipOnError = false, - }) async { - try { - /// On start, use for show loading - if (onStart != null) { - await onStart(); - } - - /// Execute api - final T res = await dioApi(); - - /// On completed, use for hide loading - if (onCompleted != null) { - await onCompleted(true, res); - } - - /// Return api response - return res; - } catch (error) { - /// In case error: - /// On completed, use for hide loading - if (onCompleted != null) { - await onCompleted(false, null); - } - - /// On inline error - if (onError != null) { - await onError(error); - } - - /// Call onApiError - if (skipOnError == false) { - onApiError(error); - } - - return null; - } finally { - /// Call finally function - if (onFinally != null) { - await onFinally(); - } - } - } - - /// Parsing error to ErrorType - ApiErrorType parseApiErrorType(dynamic error) { - if (error is DioError && error.type == DioErrorType.response) { - ApiErrorCode errorCode = ApiErrorCode.unknown; - String message = error.message; - if (error.response?.statusCode == 401) { - errorCode = ApiErrorCode.unauthorized; - } - if (error.response?.data != null) { - try { - final Map response = - error.response?.data as Map; - message = response['error'] ?? ''; - } catch (e) { - Console.danger( - e.toString(), - ); - // ignore parsing error - } - } - return ApiErrorType(code: errorCode, message: message); - } else { - Console.danger( - error.toString(), - ); - } - return ApiErrorType(); - } -} diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_api.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_api.dart deleted file mode 100644 index 7f302cb9..00000000 --- a/lib/vaahextendflutter/services/rest_api/demo/demo_api.dart +++ /dev/null @@ -1,25 +0,0 @@ -import 'package:dio/dio.dart'; - -import '../api.dart'; - -class DemoApi extends Api { - /// How to get X-XSRF-TOKEN - /// GET https://staging.theavenue.live/sanctum/csrf-cookie - /// - Parse header cookie 'X-XSRF-TOKEN' => encoded_token - /// - Use url decode 'encoded_token' to get auth Token - /// - Add X-XSRF-TOKEN to header for any call - /// Note with POST api header: - /// - X-Requested-With=XMLHttpRequest - /// - Content-Type=application/json - Future getXSRFToken() async { - final Options options = await getOptions(); - return wrapE(() => - dio.get('$apiBaseUrl/auth/csrf-cookie', options: options)); - } - - Future getError() async { - final Options options = await getOptions(); - return wrapE( - () => dio.get('$apiBaseUrl/error?code=400', options: options)); - } -} diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart index 54b6187f..1642e0d8 100644 --- a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart +++ b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart @@ -1,16 +1,20 @@ import 'package:dio/dio.dart'; +import 'package:flutter/cupertino.dart'; import 'package:get/get.dart' as getx; +import '../api.dart'; import '../../../log/console.dart'; import '../models/api_response.dart'; -import 'demo_api.dart'; class DemoController extends getx.GetxController { - Future getDemoURL() async { + Future getDemoURL(BuildContext context) async { // Call API - DemoApi api = DemoApi(); - final Response result = - await api.getError().timeout(const Duration(seconds: 180)); + Api api = Api(); + final Response? result = await api.ajax( + context: context, + url: '/error?code=401', + // skipOnError: false, + ); Console.info( result.toString(), ); diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart index 9e5ce441..7cd41ecf 100644 --- a/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart +++ b/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart @@ -2,9 +2,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../../base/base_stateful.dart'; -import '../../../log/console.dart'; -import '../api_error.dart'; -import '../models/api_error_type.dart'; import 'demo_controller.dart'; class DemoUI extends StatefulWidget { @@ -14,25 +11,7 @@ class DemoUI extends StatefulWidget { State createState() => _DemoUIState(); } -class _DemoUIState extends BaseStateful with ApiError { - @override - Future onApiError(dynamic error) async { - final ApiErrorType errorType = parseApiErrorType(error); - if (errorType.message.isNotEmpty) { - Console.danger('>>>>> code: ${errorType.code}'); - Console.danger('>>>>> message: ${errorType.message}'); - await _showErrorDialog( - title: 'Error', - content: errorType.message, - ); - } - if (errorType.code == ApiErrorCode.unauthorized) { - // TODO: Logout - return 1; - } - return 0; - } - +class _DemoUIState extends BaseStateful { DemoController? _controller; @override void afterFirstBuild(BuildContext context) { @@ -45,20 +24,7 @@ class _DemoUIState extends BaseStateful with ApiError { Future load({ bool showLoading = true, }) async { - await apiCallSafety( - () => _controller!.getDemoURL(), - onStart: () async { - if (showLoading) { - // AppLoadingProvider.show(context); - } - }, - onCompleted: (bool res, void _) async { - if (showLoading) { - // AppLoadingProvider.hide(context); - } - }, - skipOnError: false, - ); + await _controller!.getDemoURL(context); } @override @@ -66,32 +32,4 @@ class _DemoUIState extends BaseStateful with ApiError { super.build(context); return Container(); } - - Future _showErrorDialog({ - required String title, - required String content, - List? actions, - }) async { - return showDialog( - context: context, - barrierDismissible: false, // user must tap button! - builder: (BuildContext context) { - return AlertDialog( - title: Text(title), - content: Text(content), - actions: [ - if (actions == null || actions.isNotEmpty) - TextButton( - child: const Text('Okay'), - onPressed: () { - Navigator.of(context).pop(); - }, - ) - else - ...actions, - ], - ); - }, - ); - } } diff --git a/lib/vaahextendflutter/services/rest_api/models/token.dart b/lib/vaahextendflutter/services/rest_api/models/token.dart deleted file mode 100644 index 5b3795e5..00000000 --- a/lib/vaahextendflutter/services/rest_api/models/token.dart +++ /dev/null @@ -1,19 +0,0 @@ -class Token { - Token({this.bearerToken}); - - factory Token.fromJson(Map json) => Token( - bearerToken: json['bearerToken'], - ); - - static const String localKey = 'token'; - - String? bearerToken; - - Map toJson() => - {'bearerToken': bearerToken}; - - @override - String toString() { - return 'Token{bearerToken: $bearerToken}'; - } -} From c59aa2e266ff11e81115b78a1cdff93f01dacbae Mon Sep 17 00:00:00 2001 From: Prajapati Chintan Date: Mon, 17 Oct 2022 02:14:01 -0700 Subject: [PATCH 04/15] updated: removed buildcontext, refactored code --- lib/main.dart | 2 +- .../services/rest_api/api.dart | 609 ++++++++++++------ .../rest_api/demo/demo_controller.dart | 39 +- .../rest_api/models/api_error_type.dart | 13 +- .../rest_api/models/api_response.dart | 56 -- .../rest_api/models/api_response_type.dart | 18 + 6 files changed, 439 insertions(+), 298 deletions(-) delete mode 100644 lib/vaahextendflutter/services/rest_api/models/api_response.dart create mode 100644 lib/vaahextendflutter/services/rest_api/models/api_response_type.dart diff --git a/lib/main.dart b/lib/main.dart index d74c47de..7f7e3bd4 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -28,7 +28,7 @@ class TeamApp extends StatelessWidget { @override Widget build(BuildContext context) { - return MaterialApp( + return GetMaterialApp( title: 'WebReinvent Team', theme: ThemeData( primarySwatch: Colors.blue, diff --git a/lib/vaahextendflutter/services/rest_api/api.dart b/lib/vaahextendflutter/services/rest_api/api.dart index adf7bd2b..56bd1702 100644 --- a/lib/vaahextendflutter/services/rest_api/api.dart +++ b/lib/vaahextendflutter/services/rest_api/api.dart @@ -7,13 +7,16 @@ import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:flutter/cupertino.dart'; import 'package:get/get.dart' as getx; import 'package:path_provider/path_provider.dart'; -import 'models/api_error_type.dart'; +import '../../log/console.dart'; +import 'models/api_response_type.dart'; +import 'models/api_error_type.dart'; import '../../../env.dart'; enum RequestType { get, post, put, patch, delete } class Api { + // To check env variables logs enabled, apiUrl and timeout limit for requests late EnvController envController; // Get base url by env @@ -34,16 +37,17 @@ class Api { return Options(headers: header, contentType: contentType); } - Future ajax({ - required BuildContext context, + // return type of ajax is ApiResponseType? so if there is error + // then null will be returned otherwise ApiResponseType object + Future ajax({ required String url, + RequestType requestType = RequestType.get, Map? data, Map? queryParameters, - RequestType requestType = RequestType.get, - bool skipOnError = true, // if true then error dialogue won't be bool showSuccessDialogue = false, // if false on success nothing will be shown bool showErrorDialogue = true, // if false on error nothing will be shown + bool skipOnError = true, // if true then error dialogue will be dismissible Function? customSuccessDialogue, // if passed will be showed this instead of default Function? @@ -59,103 +63,26 @@ class Api { if (onStart != null) { await onStart(); } - Response? response; - final Options options = await _getOptions(); - switch (requestType) { - case RequestType.get: - response = await dio - .get( - '$apiBaseUrl$url', - queryParameters: queryParameters, - options: options, - ) - .timeout( - Duration( - seconds: - customTimeoutLimit ?? envController.config.timeoutLimit, - ), - ); - break; - - case RequestType.post: - if (data == null) 'invalid data'; //TODO: - response = await dio - .post( - '$apiBaseUrl$url', - data: data, - queryParameters: queryParameters, - options: options, - ) - .timeout( - Duration( - seconds: - customTimeoutLimit ?? envController.config.timeoutLimit, - ), - ); - break; - - case RequestType.put: - if (data == null) 'invalid data'; //TODO: - response = await dio - .put( - '$apiBaseUrl$url', - data: data, - queryParameters: queryParameters, - options: options, - ) - .timeout( - Duration( - seconds: - customTimeoutLimit ?? envController.config.timeoutLimit, - ), - ); - break; - - case RequestType.patch: - if (data == null) 'invalid data'; //TODO: - response = await dio - .patch( - '$apiBaseUrl$url', - data: data, - queryParameters: queryParameters, - options: options, - ) - .timeout( - Duration( - seconds: - customTimeoutLimit ?? envController.config.timeoutLimit, - ), - ); - break; - - case RequestType.delete: - if (data == null) 'invalid data'; //TODO: - response = await dio - .delete( - '$apiBaseUrl$url', - data: data, - queryParameters: queryParameters, - options: options, - ) - .timeout( - Duration( - seconds: - customTimeoutLimit ?? envController.config.timeoutLimit, - ), - ); - break; - default: - // TODO: Invalid request type, try again - break; - } + Response? response = await handleRequest( + requestType: requestType, + url: url, + data: data, + skipOnError: skipOnError, + queryParameters: queryParameters, + customTimeoutLimit: customTimeoutLimit, + ); // On completed, use for hide loading if (onCompleted != null) { await onCompleted(true, response); } - return response; - // TODO: + + return handleResponse( + response, + showSuccessDialogue, + customSuccessDialogue, + ); } catch (error) { // In case error: // On completed, use for hide loading @@ -168,78 +95,13 @@ class Api { await onError(error); } - if (error is DioError && error.type == DioErrorType.response) { - final Response? response = error.response; - try { - // By pass dio header error code to get response content - // Try to return response - if (response == null) { - throw DioError( - requestOptions: error.requestOptions, - response: error.response, - type: error.type, - error: response?.statusMessage, - ); - } - final Response res = Response( - data: response.data as T, - headers: response.headers, - requestOptions: response.requestOptions, - isRedirect: response.isRedirect, - statusCode: response.statusCode, - statusMessage: response.statusMessage, - redirects: response.redirects, - extra: response.extra, - ); - throw DioError( - requestOptions: error.requestOptions, - response: res, - type: error.type, - error: res.statusMessage, - ); - } catch (e) { - if (error.type == DioErrorType.response) { - ApiErrorCode errorCode = ApiErrorCode.unknown; - String message = error.message; - if (error.response?.statusCode == 401) { - errorCode = ApiErrorCode.unauthorized; - } - if (error.response?.data != null) { - try { - final Map response = - error.response?.data as Map; - message = response['error'] ?? ''; - } catch (e) { - // ignore parsing error - } - } - ApiErrorType apiErrorType = - ApiErrorType(code: errorCode, message: message); - if (apiErrorType.code == ApiErrorCode.unauthorized) { - // TODO: Logout - } - if (showErrorDialogue) { - if (customErrorDialogue != null) { - mainErrorDialogue( - skipOnError: skipOnError, - errorDialogue: () => customErrorDialogue(), - ); - } else { - mainErrorDialogue( - skipOnError: skipOnError, - errorDialogue: () => defaultErrorDialogue( - context: context, - title: 'Error', - content: apiErrorType.message, - ), - ); - } - } - } else { - rethrow; - } - } - } + // Here response error means server sends error response. eg 401: unauthorised + handleResponseError( + error, + showErrorDialogue, + customErrorDialogue, + skipOnError, + ); } finally { /// Call finally function if (onFinally != null) { @@ -249,48 +111,6 @@ class Api { return null; } - mainErrorDialogue( - {required bool skipOnError, required Function() errorDialogue}) async { - if (skipOnError) { - await errorDialogue(); - } else { - await errorDialogue(); - mainErrorDialogue( - skipOnError: skipOnError, - errorDialogue: () => errorDialogue, - ); - } - } - - defaultErrorDialogue({ - required BuildContext context, - required String title, - required String content, - List? actions, - }) { - return showCupertinoDialog( - context: context, - barrierDismissible: false, // user must tap button! - builder: (BuildContext context) { - return CupertinoAlertDialog( - title: Text(title), - content: Text(content), - actions: [ - if (actions == null || actions.isNotEmpty) - CupertinoButton( - child: const Text('Okay'), - onPressed: () { - Navigator.of(context).pop(); - }, - ) - else - ...actions, - ], - ); - }, - ); - } - Future _initApi() async { bool envControllerExists = getx.Get.isRegistered(); if (!envControllerExists) { @@ -349,4 +169,373 @@ class Api { }, ); } + + Future?> handleRequest({ + required RequestType requestType, + required String url, + required Map? data, + required Map? queryParameters, + required int? customTimeoutLimit, + required bool skipOnError, + }) async { + Response? response; + final Options options = await _getOptions(); + switch (requestType) { + case RequestType.get: + response = await dio + .get( + '$apiBaseUrl$url', + queryParameters: queryParameters, + options: options, + ) + .timeout( + Duration( + seconds: + customTimeoutLimit ?? envController.config.timeoutLimit, + ), + ); + break; + + case RequestType.post: + if (data == null) { + mainDialogue( + skip: skipOnError, + dialogue: () => defaultErrorDialogue( + title: 'Error', + content: ['Invalid data!'], + ), + ); + break; + } + response = await dio + .post( + '$apiBaseUrl$url', + data: data, + queryParameters: queryParameters, + options: options, + ) + .timeout( + Duration( + seconds: + customTimeoutLimit ?? envController.config.timeoutLimit, + ), + ); + break; + + case RequestType.put: + if (data == null) { + mainDialogue( + skip: skipOnError, + dialogue: () => defaultErrorDialogue( + title: 'Error', + content: ['Invalid data!'], + ), + ); + break; + } + response = await dio + .put( + '$apiBaseUrl$url', + data: data, + queryParameters: queryParameters, + options: options, + ) + .timeout( + Duration( + seconds: + customTimeoutLimit ?? envController.config.timeoutLimit, + ), + ); + break; + + case RequestType.patch: + if (data == null) { + mainDialogue( + skip: skipOnError, + dialogue: () => defaultErrorDialogue( + title: 'Error', + content: ['Invalid data!'], + ), + ); + break; + } + response = await dio + .patch( + '$apiBaseUrl$url', + data: data, + queryParameters: queryParameters, + options: options, + ) + .timeout( + Duration( + seconds: + customTimeoutLimit ?? envController.config.timeoutLimit, + ), + ); + break; + + case RequestType.delete: + if (data == null) { + mainDialogue( + skip: skipOnError, + dialogue: () => defaultErrorDialogue( + title: 'Error', + content: ['Invalid data!'], + ), + ); + break; + } + response = await dio + .delete( + '$apiBaseUrl$url', + data: data, + queryParameters: queryParameters, + options: options, + ) + .timeout( + Duration( + seconds: + customTimeoutLimit ?? envController.config.timeoutLimit, + ), + ); + break; + + default: + mainDialogue( + skip: skipOnError, + dialogue: () => defaultErrorDialogue( + title: 'Error', + content: ['Invalid request type!'], + ), + ); + break; + } + return response; + } + + ApiResponseType handleResponse( + Response? response, + bool showSuccessDialogue, + Function? customSuccessDialogue, + ) { + if (response != null && response.data != null) { + try { + final Map formatedResponse = + response.data as Map; + dynamic responseSuccess = formatedResponse['success']; + if (responseSuccess == null) { + Console.warning('response doesn\'t contain success key.'); + } + dynamic responseData = formatedResponse['data']; + if (responseData == null) { + Console.warning('response doesn\'t contain data key.'); + } + List? responseMessages = formatedResponse['messages']; + if (responseMessages == null) { + Console.warning('response doesn\'t contain messages key.'); + } + String? responseHint = formatedResponse['hint']; + if (responseHint == null) { + Console.warning('response doesn\'t contain hint key.'); + } + if (showSuccessDialogue) { + if (customSuccessDialogue != null) { + mainDialogue( + skip: true, + dialogue: () => customSuccessDialogue(), + ); + } else { + mainDialogue( + skip: true, + dialogue: () => defaultSuccessDialogue( + title: 'Success', + content: responseMessages, + hint: responseHint, + ), + ); + } + } + return ApiResponseType( + success: responseSuccess, + data: responseData, + messages: responseMessages, + hint: responseHint, + ); + } catch (e) { + throw Exception( + 'Unable to parse response.', + ); + } + } + throw Exception('response from server is null or response.data is null'); + } + + void handleResponseError( + Object error, + bool showErrorDialogue, + Function? customErrorDialogue, + bool skipOnError, + ) { + // Handle response error from server only, otherwise rethrow. + if (error is DioError && error.type == DioErrorType.response) { + final Response? response = error.response; + try { + // By pass dio header error code to get response content + // Try to return response + if (response == null) { + throw DioError( + requestOptions: error.requestOptions, + response: error.response, + type: error.type, + error: response?.statusMessage, + ); + } + final Response res = Response( + data: response.data, + headers: response.headers, + requestOptions: response.requestOptions, + isRedirect: response.isRedirect, + statusCode: response.statusCode, + statusMessage: response.statusMessage, + redirects: response.redirects, + extra: response.extra, + ); + throw DioError( + requestOptions: error.requestOptions, + response: res, + type: error.type, + error: res.statusMessage, + ); + } catch (e) { + if (error.type == DioErrorType.response) { + ApiErrorCode errorCode = ApiErrorCode.unknown; + List errors = [error.message]; + String? debug; + if (error.response?.statusCode == 401) { + errorCode = ApiErrorCode.unauthorized; + } + if (error.response?.data != null) { + try { + final Map response = + error.response?.data as Map; + errors = response['errors'] ?? []; + if (errors.isEmpty) { + Console.warning('response doesn\'t contain errors key.'); + } + debug = response['debug']; + if (debug == null) { + Console.warning('response doesn\'t contain debug key.'); + } + } catch (e) { + throw Exception( + 'Unable to parse error response.', + ); + } + } + ApiErrorType apiErrorType = + ApiErrorType(code: errorCode, errors: errors, debug: debug); + if (apiErrorType.code == ApiErrorCode.unauthorized) { + Console.danger('Error type: unauthorized'); + // TODO: Logout + // Logout user from controller and then send them to login screen + // after that show the error dialogue + } + if (showErrorDialogue) { + if (customErrorDialogue != null) { + mainDialogue( + skip: skipOnError, + dialogue: () => customErrorDialogue(), + ); + return; + } + mainDialogue( + skip: skipOnError, + dialogue: () => defaultErrorDialogue( + title: 'Error', + content: apiErrorType.errors, + ), + ); + } + return; + } + Console.danger(error.toString()); + rethrow; + } + } + } + + void mainDialogue({required bool skip, required Function() dialogue}) async { + if (!skip) { + await dialogue(); + mainDialogue( + skip: skip, + dialogue: () => dialogue, + ); + return; + } + await dialogue(); + return; + } + + defaultSuccessDialogue({ + required String title, + List? content, + String? hint, + List? actions, + }) { + return getx.Get.dialog( + CupertinoAlertDialog( + title: Text(title), + content: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (content != null) Text(content.join(' ')), + // TODO: replace with const margin + const SizedBox(height: 12), + if (hint != null) Text(hint), + ], + ), + ), + actions: [ + if (actions == null || actions.isNotEmpty) + CupertinoButton( + child: const Text('Okay'), + onPressed: () { + getx.Get.back(); + }, + ) + else + ...actions, + ], + ), + barrierDismissible: false, + ); + } + + defaultErrorDialogue({ + required String title, + required List content, + List? actions, + }) { + return getx.Get.dialog( + CupertinoAlertDialog( + title: Text(title), + content: Text(content.join(' ')), + actions: [ + if (actions == null || actions.isNotEmpty) + CupertinoButton( + child: const Text('Okay'), + onPressed: () { + getx.Get.back(); + }, + ) + else + ...actions, + ], + ), + barrierDismissible: false, + ); + } } diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart index 1642e0d8..d15b09aa 100644 --- a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart +++ b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart @@ -1,40 +1,25 @@ -import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:get/get.dart' as getx; -import '../api.dart'; import '../../../log/console.dart'; -import '../models/api_response.dart'; +import '../api.dart'; +import '../models/api_response_type.dart'; class DemoController extends getx.GetxController { Future getDemoURL(BuildContext context) async { // Call API Api api = Api(); - final Response? result = await api.ajax( - context: context, - url: '/error?code=401', + final ApiResponseType? result = await api.ajax( + url: '/error', + queryParameters: { + 'code': 400, + }, // skipOnError: false, + // showSuccessDialogue: true, + customTimeoutLimit: 0, ); - Console.info( - result.toString(), - ); - // final DecodedResponse response = DecodedResponse(result.data); - // if (response.data != null) {} - } -} - -class DecodedResponse extends BaseResponse> { - DecodedResponse(Map? fullJson) : super(fullJson); - - @override - List jsonToData(dynamic dataJson) { - final List? dataList = dataJson as List?; - return dataList != null - ? List.from( - dataList.map( - (dynamic x) => x.toString(), - ), - ) - : []; + if (result != null) { + Console.log(result.toString()); + } } } diff --git a/lib/vaahextendflutter/services/rest_api/models/api_error_type.dart b/lib/vaahextendflutter/services/rest_api/models/api_error_type.dart index ef342dc5..85575dab 100644 --- a/lib/vaahextendflutter/services/rest_api/models/api_error_type.dart +++ b/lib/vaahextendflutter/services/rest_api/models/api_error_type.dart @@ -1,13 +1,18 @@ enum ApiErrorCode { unknown, unauthorized } class ApiErrorType { - ApiErrorType({this.code = ApiErrorCode.unknown, this.message = 'Unknown'}); - final ApiErrorCode code; - final String message; + final List errors; + final String? debug; + + const ApiErrorType({ + this.code = ApiErrorCode.unknown, + this.errors = const ['Unknown'], + this.debug, + }); @override String toString() { - return 'ApiErrorType{code: $code, message: $message}'; + return 'ApiErrorType{code: $code, errors: ${errors.join(' ')}, debug: $debug}'; } } diff --git a/lib/vaahextendflutter/services/rest_api/models/api_response.dart b/lib/vaahextendflutter/services/rest_api/models/api_response.dart deleted file mode 100644 index c26426d0..00000000 --- a/lib/vaahextendflutter/services/rest_api/models/api_response.dart +++ /dev/null @@ -1,56 +0,0 @@ -import 'dart:core'; - -class BaseResponse { - BaseResponse( - Map? fullJson, { - String? dataKey, - String errorKey = 'error', - }) { - parsing(fullJson, dataKey: dataKey, errorKey: errorKey); - } - - T? data; - late bool success; - late String error; - late String message; - - // Abstract json to data - T? jsonToData(dynamic dataJson) { - return null; - } - - // Abstract data to json - dynamic dataToJson(T? data) { - return null; - } - - // Parsing data to object - // dataKey = null mean parse from root - dynamic parsing( - Map? fullJson, { - String? dataKey, - String errorKey = 'error', - }) { - if (fullJson != null) { - final dynamic dataJson = - dataKey != null ? fullJson[dataKey] : fullJson['data']; - data = dataJson != null ? jsonToData(dataJson) : null; - success = fullJson['success'] as bool; - error = fullJson[errorKey] as String; - message = fullJson['message'] as String; - } - } - - // Data to json - Map toJson() => { - 'data': data != null ? dataToJson(data) : null, - 'success': success, - 'message': message, - 'error': error, - }; - - @override - String toString() { - return 'BaseResponse{data: $data, success:$success, message: $message, error: $error}'; - } -} diff --git a/lib/vaahextendflutter/services/rest_api/models/api_response_type.dart b/lib/vaahextendflutter/services/rest_api/models/api_response_type.dart new file mode 100644 index 00000000..1a4f64b8 --- /dev/null +++ b/lib/vaahextendflutter/services/rest_api/models/api_response_type.dart @@ -0,0 +1,18 @@ +class ApiResponseType { + final bool success; + final dynamic data; + final List? messages; + final String? hint; + + const ApiResponseType({ + required this.success, + required this.data, + this.messages, + this.hint, + }); + + @override + String toString() { + return 'ApiResponseType{success: $success, data: $data, messages: ${messages?.join(' ')}, hint: $hint}'; + } +} From 7686e53e7a2f94a95cbe7c137746f8bb39c070d9 Mon Sep 17 00:00:00 2001 From: Prajapati Chintan Date: Mon, 17 Oct 2022 20:24:22 +0530 Subject: [PATCH 05/15] updated: solved response not processed issue. --- lib/env.dart | 6 +- .../services/rest_api/api.dart | 207 +++++++----------- .../rest_api/demo/demo_controller.dart | 24 +- .../services/rest_api/demo/demo_ui.dart | 35 ++- 4 files changed, 121 insertions(+), 151 deletions(-) diff --git a/lib/env.dart b/lib/env.dart index 363fb213..f6e34360 100644 --- a/lib/env.dart +++ b/lib/env.dart @@ -16,8 +16,8 @@ EnvironmentConfig defaultConfig = const EnvironmentConfig( version: version, build: build, baseUrl: '', - apiBaseUrl: 'https://xrestapi.herokuapp.com', - timeoutLimit: 180, + apiBaseUrl: 'https://reqres.in', + timeoutLimit: 60 * 1000, // 60 seconds analyticsId: '', enableConsoleLogs: true, enableLocalLogs: true, @@ -77,7 +77,7 @@ class EnvironmentConfig { final String build; final String baseUrl; final String apiBaseUrl; - final int timeoutLimit; // in seconds + final int timeoutLimit; // in milli seconds final String analyticsId; final bool enableConsoleLogs; final bool enableLocalLogs; diff --git a/lib/vaahextendflutter/services/rest_api/api.dart b/lib/vaahextendflutter/services/rest_api/api.dart index 56bd1702..9e022a5f 100644 --- a/lib/vaahextendflutter/services/rest_api/api.dart +++ b/lib/vaahextendflutter/services/rest_api/api.dart @@ -55,7 +55,7 @@ class Api { int? customTimeoutLimit, Future Function()? onStart, Future Function(dynamic error)? onError, - Future Function(bool status, Response? res)? onCompleted, + Future Function(bool status, ApiResponseType? res)? onCompleted, Future Function()? onFinally, }) async { try { @@ -72,17 +72,20 @@ class Api { queryParameters: queryParameters, customTimeoutLimit: customTimeoutLimit, ); + Console.danger('>>> $response'); - // On completed, use for hide loading - if (onCompleted != null) { - await onCompleted(true, response); - } - - return handleResponse( + ApiResponseType apiResponseType = handleResponse( response, showSuccessDialogue, customSuccessDialogue, ); + + // On completed, use for hide loading + if (onCompleted != null) { + await onCompleted(true, apiResponseType); + } + + return apiResponseType; } catch (error) { // In case error: // On completed, use for hide loading @@ -94,21 +97,33 @@ class Api { if (onError != null) { await onError(error); } - + // All errors other than dio error eg. typeError + if (error is! DioError) { + rethrow; + } + // Timeout Error + else if (error.type == DioErrorType.sendTimeout || + error.type == DioErrorType.receiveTimeout) { + handleTimeoutError(error, skipOnError); + return null; + } // Here response error means server sends error response. eg 401: unauthorised - handleResponseError( - error, - showErrorDialogue, - customErrorDialogue, - skipOnError, - ); + else if (error.type == DioErrorType.response) { + handleResponseError( + error, + showErrorDialogue, + customErrorDialogue, + skipOnError, + ); + return null; + } + rethrow; } finally { - /// Call finally function + // Call finally function if (onFinally != null) { await onFinally(); } } - return null; } Future _initApi() async { @@ -127,47 +142,6 @@ class Api { ), ); } - getApplicationDocumentsDirectory().then( - (Directory appDocDir) async { - final String appDocPath = appDocDir.path; - final String cookiePath = '$appDocPath/cookies'; - final Directory dir = Directory(cookiePath); - await dir.create(); - cookieJar = PersistCookieJar( - storage: FileStorage(cookiePath), - ); - dio.interceptors.add( - CookieManager(cookieJar), - ); - dio.interceptors.add( - InterceptorsWrapper( - onResponse: (Response response, handler) async { - final String urlPath = response.requestOptions.path; - final List cookies = - await cookieJar.loadForRequest(Uri.parse(urlPath)); - final String? xsrfToken = cookies - .firstWhereOrNull( - (Cookie c) => c.name == 'XSRF-TOKEN', - ) - ?.value; - // Set dio auth header token once time - if (xsrfToken != null) { - // The XSRF-TOKEN got from cookie requires decoded before add to header - dio.options.headers['X-XSRF-TOKEN'] = - Uri.decodeComponent(xsrfToken); - String cookieStr = ''; - for (int i = 0; i < cookies.length; i++) { - final Cookie c = cookies[i]; - cookieStr += '${c.name}=${c.value}; '; - } - dio.options.headers['Cookie'] = cookieStr; - } - return; - }, - ), - ); - }, - ); } Future?> handleRequest({ @@ -180,20 +154,17 @@ class Api { }) async { Response? response; final Options options = await _getOptions(); + options.sendTimeout = + customTimeoutLimit ?? envController.config.timeoutLimit; + options.receiveTimeout = + customTimeoutLimit ?? envController.config.timeoutLimit; switch (requestType) { case RequestType.get: - response = await dio - .get( - '$apiBaseUrl$url', - queryParameters: queryParameters, - options: options, - ) - .timeout( - Duration( - seconds: - customTimeoutLimit ?? envController.config.timeoutLimit, - ), - ); + response = await dio.get( + '$apiBaseUrl$url', + queryParameters: queryParameters, + options: options, + ); break; case RequestType.post: @@ -207,19 +178,12 @@ class Api { ); break; } - response = await dio - .post( - '$apiBaseUrl$url', - data: data, - queryParameters: queryParameters, - options: options, - ) - .timeout( - Duration( - seconds: - customTimeoutLimit ?? envController.config.timeoutLimit, - ), - ); + response = await dio.post( + '$apiBaseUrl$url', + data: data, + queryParameters: queryParameters, + options: options, + ); break; case RequestType.put: @@ -233,19 +197,12 @@ class Api { ); break; } - response = await dio - .put( - '$apiBaseUrl$url', - data: data, - queryParameters: queryParameters, - options: options, - ) - .timeout( - Duration( - seconds: - customTimeoutLimit ?? envController.config.timeoutLimit, - ), - ); + response = await dio.put( + '$apiBaseUrl$url', + data: data, + queryParameters: queryParameters, + options: options, + ); break; case RequestType.patch: @@ -259,19 +216,12 @@ class Api { ); break; } - response = await dio - .patch( - '$apiBaseUrl$url', - data: data, - queryParameters: queryParameters, - options: options, - ) - .timeout( - Duration( - seconds: - customTimeoutLimit ?? envController.config.timeoutLimit, - ), - ); + response = await dio.patch( + '$apiBaseUrl$url', + data: data, + queryParameters: queryParameters, + options: options, + ); break; case RequestType.delete: @@ -285,19 +235,12 @@ class Api { ); break; } - response = await dio - .delete( - '$apiBaseUrl$url', - data: data, - queryParameters: queryParameters, - options: options, - ) - .timeout( - Duration( - seconds: - customTimeoutLimit ?? envController.config.timeoutLimit, - ), - ); + response = await dio.delete( + '$apiBaseUrl$url', + data: data, + queryParameters: queryParameters, + options: options, + ); break; default: @@ -322,7 +265,7 @@ class Api { try { final Map formatedResponse = response.data as Map; - dynamic responseSuccess = formatedResponse['success']; + bool? responseSuccess = formatedResponse['success']; if (responseSuccess == null) { Console.warning('response doesn\'t contain success key.'); } @@ -356,27 +299,35 @@ class Api { } } return ApiResponseType( - success: responseSuccess, + success: responseSuccess ?? true, data: responseData, messages: responseMessages, hint: responseHint, ); } catch (e) { - throw Exception( - 'Unable to parse response.', - ); + rethrow; } } throw Exception('response from server is null or response.data is null'); } + void handleTimeoutError(DioError error, bool skipOnError) { + Console.danger(error.toString()); + mainDialogue( + skip: skipOnError, + dialogue: () => defaultErrorDialogue( + title: 'Error', + content: ['Check your internet connection!'], + ), + ); + } + void handleResponseError( Object error, bool showErrorDialogue, Function? customErrorDialogue, bool skipOnError, ) { - // Handle response error from server only, otherwise rethrow. if (error is DioError && error.type == DioErrorType.response) { final Response? response = error.response; try { @@ -493,7 +444,7 @@ class Api { children: [ if (content != null) Text(content.join(' ')), // TODO: replace with const margin - const SizedBox(height: 12), + if (content != null) const SizedBox(height: 12), if (hint != null) Text(hint), ], ), diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart index d15b09aa..4dc144ab 100644 --- a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart +++ b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart @@ -1,4 +1,3 @@ -import 'package:flutter/cupertino.dart'; import 'package:get/get.dart' as getx; import '../../../log/console.dart'; @@ -6,20 +5,15 @@ import '../api.dart'; import '../models/api_response_type.dart'; class DemoController extends getx.GetxController { - Future getDemoURL(BuildContext context) async { - // Call API - Api api = Api(); - final ApiResponseType? result = await api.ajax( - url: '/error', - queryParameters: { - 'code': 400, - }, - // skipOnError: false, - // showSuccessDialogue: true, - customTimeoutLimit: 0, + // Call API + Api api = Api(); + + Future getDemoURL() async { + await api.ajax( + url: '/api/users', + requestType: RequestType.post, ); - if (result != null) { - Console.log(result.toString()); - } } + + Future getDemoURLAfter(suceess, resp) async {} } diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart index 7cd41ecf..e48d484f 100644 --- a/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart +++ b/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart @@ -12,24 +12,49 @@ class DemoUI extends StatefulWidget { } class _DemoUIState extends BaseStateful { + bool _isLoading = false; DemoController? _controller; + @override - void afterFirstBuild(BuildContext context) { + void afterFirstBuild(BuildContext context) async { + super.afterFirstBuild(context); Get.put(DemoController()); _controller = Get.find(); - load(); - super.afterFirstBuild(context); + await load(); } Future load({ bool showLoading = true, }) async { - await _controller!.getDemoURL(context); + setState(() { + _isLoading = true; + }); + await _controller!.getDemoURL(); + setState(() { + _isLoading = false; + }); } @override Widget build(BuildContext context) { super.build(context); - return Container(); + return SizedBox( + height: 40, + width: 150, + child: ElevatedButton( + onPressed: () => load(), + child: _isLoading + ? const Center( + child: SizedBox( + height: 20, + width: 20, + child: CircularProgressIndicator( + color: Colors.white, + ), + ), + ) + : const Text('Request'), + ), + ); } } From 181eb5c9aca16a587f900ba1c9f1250aa5c306fc Mon Sep 17 00:00:00 2001 From: Prajapati Chintan Date: Tue, 18 Oct 2022 20:28:41 +0530 Subject: [PATCH 06/15] fixed: variables, toast and dialogues and methods --- lib/env.dart | 2 +- .../services/rest_api/api.dart | 337 ++++++++++-------- .../rest_api/demo/demo_controller.dart | 16 +- .../rest_api/models/api_response_type.dart | 18 - pubspec.lock | 19 + pubspec.yaml | 1 + 6 files changed, 220 insertions(+), 173 deletions(-) delete mode 100644 lib/vaahextendflutter/services/rest_api/models/api_response_type.dart diff --git a/lib/env.dart b/lib/env.dart index f6e34360..9249f5fa 100644 --- a/lib/env.dart +++ b/lib/env.dart @@ -16,7 +16,7 @@ EnvironmentConfig defaultConfig = const EnvironmentConfig( version: version, build: build, baseUrl: '', - apiBaseUrl: 'https://reqres.in', + apiBaseUrl: 'https://xrestapi.herokuapp.com', timeoutLimit: 60 * 1000, // 60 seconds analyticsId: '', enableConsoleLogs: true, diff --git a/lib/vaahextendflutter/services/rest_api/api.dart b/lib/vaahextendflutter/services/rest_api/api.dart index 9e022a5f..7d821c34 100644 --- a/lib/vaahextendflutter/services/rest_api/api.dart +++ b/lib/vaahextendflutter/services/rest_api/api.dart @@ -5,15 +5,16 @@ import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart' as getx; import 'package:path_provider/path_provider.dart'; import '../../log/console.dart'; -import 'models/api_response_type.dart'; import 'models/api_error_type.dart'; import '../../../env.dart'; -enum RequestType { get, post, put, patch, delete } +// alertType : 'dialog', 'toast', class Api { // To check env variables logs enabled, apiUrl and timeout limit for requests @@ -39,23 +40,20 @@ class Api { // return type of ajax is ApiResponseType? so if there is error // then null will be returned otherwise ApiResponseType object - Future ajax({ + Future ajax({ required String url, - RequestType requestType = RequestType.get, - Map? data, - Map? queryParameters, - bool showSuccessDialogue = - false, // if false on success nothing will be shown - bool showErrorDialogue = true, // if false on error nothing will be shown - bool skipOnError = true, // if true then error dialogue will be dismissible - Function? - customSuccessDialogue, // if passed will be showed this instead of default - Function? - customErrorDialogue, // if passed will be showed this instead of default + Future Function(dynamic data, Response? res)? callback, + String method = 'get', + Map? params, // data passed in post, put, etc. request + Map? query, + List>? headers, + bool showAlert = true, // if false on success/ error, nothing will be shown + String alertType = 'toast', + bool skipOnError = true, // if true then error dialog will be dismissible int? customTimeoutLimit, Future Function()? onStart, + Future Function()? onCompleted, Future Function(dynamic error)? onError, - Future Function(bool status, ApiResponseType? res)? onCompleted, Future Function()? onFinally, }) async { try { @@ -65,57 +63,77 @@ class Api { } Response? response = await handleRequest( - requestType: requestType, url: url, - data: data, + method: method, + query: query, + params: params, + headers: headers, skipOnError: skipOnError, - queryParameters: queryParameters, customTimeoutLimit: customTimeoutLimit, + showAlert: showAlert, + alertType: alertType, ); - Console.danger('>>> $response'); - ApiResponseType apiResponseType = handleResponse( + dynamic responseData = handleResponse( response, - showSuccessDialogue, - customSuccessDialogue, + showAlert, + alertType, ); // On completed, use for hide loading if (onCompleted != null) { - await onCompleted(true, apiResponseType); + await onCompleted(); } - return apiResponseType; + if (callback != null) { + await callback(responseData, response); + } + + return; } catch (error) { - // In case error: // On completed, use for hide loading if (onCompleted != null) { - await onCompleted(false, null); + await onCompleted(); } // On inline error if (onError != null) { await onError(error); } + // All errors other than dio error eg. typeError if (error is! DioError) { + if (callback != null) { + await callback(null, null); + } rethrow; } + // Timeout Error else if (error.type == DioErrorType.sendTimeout || error.type == DioErrorType.receiveTimeout) { - handleTimeoutError(error, skipOnError); - return null; + handleTimeoutError(error, skipOnError, showAlert, alertType); + if (callback != null) { + await callback(null, null); + } + return; } + // Here response error means server sends error response. eg 401: unauthorised else if (error.type == DioErrorType.response) { handleResponseError( error, - showErrorDialogue, - customErrorDialogue, skipOnError, + showAlert, + alertType, ); - return null; + if (callback != null) { + await callback(null, null); + } + return; + } + if (callback != null) { + await callback(null, null); } rethrow; } finally { @@ -145,12 +163,15 @@ class Api { } Future?> handleRequest({ - required RequestType requestType, + required String method, required String url, - required Map? data, - required Map? queryParameters, + required Map? query, + required Map? params, + required List>? headers, required int? customTimeoutLimit, required bool skipOnError, + required bool showAlert, + required String alertType, }) async { Response? response; final Options options = await _getOptions(); @@ -158,117 +179,93 @@ class Api { customTimeoutLimit ?? envController.config.timeoutLimit; options.receiveTimeout = customTimeoutLimit ?? envController.config.timeoutLimit; - switch (requestType) { - case RequestType.get: + if (headers != null && headers.isNotEmpty) { + if (options.headers != null) { + for (Map element in headers) { + options.headers?.addAll(element); + } + } else { + final Map customHeader = {}; + for (Map element in headers) { + customHeader.addAll(element); + } + options.headers = customHeader; + } + } + switch (method) { + case 'get': response = await dio.get( '$apiBaseUrl$url', - queryParameters: queryParameters, + queryParameters: query, options: options, ); break; - case RequestType.post: - if (data == null) { - mainDialogue( - skip: skipOnError, - dialogue: () => defaultErrorDialogue( - title: 'Error', - content: ['Invalid data!'], - ), - ); - break; - } + case 'post': response = await dio.post( '$apiBaseUrl$url', - data: data, - queryParameters: queryParameters, + data: params, + queryParameters: query, options: options, ); break; - case RequestType.put: - if (data == null) { - mainDialogue( - skip: skipOnError, - dialogue: () => defaultErrorDialogue( - title: 'Error', - content: ['Invalid data!'], - ), - ); - break; - } + case 'put': response = await dio.put( '$apiBaseUrl$url', - data: data, - queryParameters: queryParameters, + data: params, + queryParameters: query, options: options, ); break; - case RequestType.patch: - if (data == null) { - mainDialogue( - skip: skipOnError, - dialogue: () => defaultErrorDialogue( - title: 'Error', - content: ['Invalid data!'], - ), - ); - break; - } + case 'patch': response = await dio.patch( '$apiBaseUrl$url', - data: data, - queryParameters: queryParameters, + data: params, + queryParameters: query, options: options, ); break; - case RequestType.delete: - if (data == null) { - mainDialogue( - skip: skipOnError, - dialogue: () => defaultErrorDialogue( - title: 'Error', - content: ['Invalid data!'], - ), - ); - break; - } + case 'delete': response = await dio.delete( '$apiBaseUrl$url', - data: data, - queryParameters: queryParameters, + data: params, + queryParameters: query, options: options, ); break; default: - mainDialogue( - skip: skipOnError, - dialogue: () => defaultErrorDialogue( - title: 'Error', - content: ['Invalid request type!'], - ), - ); - break; + if (showAlert) { + if (alertType == 'dialog') { + showDialog( + skip: skipOnError, + dialog: () => defaultErrorDialog( + title: 'Error', + content: ['Invalid request type!'], + ), + ); + break; + } else { + showToast(content: 'Invalid request type!', toastType: 'failure'); + break; + } + } } return response; } - ApiResponseType handleResponse( + dynamic handleResponse( Response? response, - bool showSuccessDialogue, - Function? customSuccessDialogue, + bool showAlert, + String alertType, ) { if (response != null && response.data != null) { try { final Map formatedResponse = response.data as Map; - bool? responseSuccess = formatedResponse['success']; - if (responseSuccess == null) { - Console.warning('response doesn\'t contain success key.'); - } dynamic responseData = formatedResponse['data']; if (responseData == null) { Console.warning('response doesn\'t contain data key.'); @@ -281,29 +278,24 @@ class Api { if (responseHint == null) { Console.warning('response doesn\'t contain hint key.'); } - if (showSuccessDialogue) { - if (customSuccessDialogue != null) { - mainDialogue( + if (showAlert) { + if (alertType == 'dialog') { + showDialog( skip: true, - dialogue: () => customSuccessDialogue(), - ); - } else { - mainDialogue( - skip: true, - dialogue: () => defaultSuccessDialogue( + dialog: () => defaultSuccessDialog( title: 'Success', content: responseMessages, hint: responseHint, ), ); + } else { + showToast( + content: responseMessages?.join('') ?? 'Successful', + toastType: 'success', + ); } } - return ApiResponseType( - success: responseSuccess ?? true, - data: responseData, - messages: responseMessages, - hint: responseHint, - ); + return responseData; } catch (e) { rethrow; } @@ -311,22 +303,36 @@ class Api { throw Exception('response from server is null or response.data is null'); } - void handleTimeoutError(DioError error, bool skipOnError) { + void handleTimeoutError( + DioError error, + bool skipOnError, + showAlert, + alertType, + ) { Console.danger(error.toString()); - mainDialogue( - skip: skipOnError, - dialogue: () => defaultErrorDialogue( - title: 'Error', - content: ['Check your internet connection!'], - ), - ); + if (showAlert) { + if (alertType == 'dialog') { + showDialog( + skip: skipOnError, + dialog: () => defaultErrorDialog( + title: 'Error', + content: ['Check your internet connection!'], + ), + ); + } else { + showToast( + content: 'Check your internet connection!', + toastType: 'failure', + ); + } + } } void handleResponseError( Object error, - bool showErrorDialogue, - Function? customErrorDialogue, bool skipOnError, + bool showAlert, + String alertType, ) { if (error is DioError && error.type == DioErrorType.response) { final Response? response = error.response; @@ -389,23 +395,25 @@ class Api { Console.danger('Error type: unauthorized'); // TODO: Logout // Logout user from controller and then send them to login screen - // after that show the error dialogue + // after that show the error dialog } - if (showErrorDialogue) { - if (customErrorDialogue != null) { - mainDialogue( + if (showAlert) { + if (alertType == 'dialog') { + showDialog( skip: skipOnError, - dialogue: () => customErrorDialogue(), + dialog: () => defaultErrorDialog( + title: 'Error', + content: apiErrorType.errors, + ), + ); + } else { + showToast( + content: apiErrorType.errors.isEmpty + ? 'Error' + : apiErrorType.errors.join(' '), + toastType: 'failure', ); - return; } - mainDialogue( - skip: skipOnError, - dialogue: () => defaultErrorDialogue( - title: 'Error', - content: apiErrorType.errors, - ), - ); } return; } @@ -415,20 +423,51 @@ class Api { } } - void mainDialogue({required bool skip, required Function() dialogue}) async { + void showToast({ + required String content, + toastType = 'default', + }) async { + switch (toastType) { + case 'success': + defaultToast(content, Colors.green); + break; + case 'failure': + defaultToast(content, Colors.red); + break; + default: + defaultToast(content, Colors.white); + break; + } + } + + void defaultToast(String content, Color color) { + Fluttertoast.showToast( + msg: content, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: color.withOpacity(0.5), + textColor: Colors.black, + fontSize: 16.0, + ); + } + + void showDialog({ + required bool skip, + required Function() dialog, + }) async { if (!skip) { - await dialogue(); - mainDialogue( + await dialog(); + showDialog( skip: skip, - dialogue: () => dialogue, + dialog: () => dialog, ); return; } - await dialogue(); + await dialog(); return; } - defaultSuccessDialogue({ + defaultSuccessDialog({ required String title, List? content, String? hint, @@ -465,7 +504,7 @@ class Api { ); } - defaultErrorDialogue({ + defaultErrorDialog({ required String title, required List content, List? actions, diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart index 4dc144ab..a9590eb2 100644 --- a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart +++ b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart @@ -1,8 +1,7 @@ import 'package:get/get.dart' as getx; +import 'package:team/vaahextendflutter/log/console.dart'; -import '../../../log/console.dart'; import '../api.dart'; -import '../models/api_response_type.dart'; class DemoController extends getx.GetxController { // Call API @@ -10,10 +9,17 @@ class DemoController extends getx.GetxController { Future getDemoURL() async { await api.ajax( - url: '/api/users', - requestType: RequestType.post, + url: '/error', + callback: getDemoURLAfter, + query: {'code': 401}, + showAlert: false, + alertType: 'dialog', ); } - Future getDemoURLAfter(suceess, resp) async {} + Future getDemoURLAfter(dynamic data, dynamic resp) async { + if (data != null) { + Console.info(data.toString()); + } + } } diff --git a/lib/vaahextendflutter/services/rest_api/models/api_response_type.dart b/lib/vaahextendflutter/services/rest_api/models/api_response_type.dart deleted file mode 100644 index 1a4f64b8..00000000 --- a/lib/vaahextendflutter/services/rest_api/models/api_response_type.dart +++ /dev/null @@ -1,18 +0,0 @@ -class ApiResponseType { - final bool success; - final dynamic data; - final List? messages; - final String? hint; - - const ApiResponseType({ - required this.success, - required this.data, - this.messages, - this.hint, - }); - - @override - String toString() { - return 'ApiResponseType{success: $success, data: $data, messages: ${messages?.join(' ')}, hint: $hint}'; - } -} diff --git a/pubspec.lock b/pubspec.lock index f8fcd6ac..3e180250 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -116,6 +116,18 @@ packages: description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + fluttertoast: + dependency: "direct main" + description: + name: fluttertoast + url: "https://pub.dartlang.org" + source: hosted + version: "8.1.1" get: dependency: "direct main" description: @@ -130,6 +142,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.2" + js: + dependency: transitive + description: + name: js + url: "https://pub.dartlang.org" + source: hosted + version: "0.6.4" lints: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 005a5a8d..f2d0d86f 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -20,6 +20,7 @@ dependencies: dio: ^4.0.6 path_provider: ^2.0.11 dio_cookie_manager: ^2.0.0 + fluttertoast: ^8.1.1 dev_dependencies: flutter_test: From 889b73130460a82af6bc3b66a828b553b07e5707 Mon Sep 17 00:00:00 2001 From: Prajapati Chintan Date: Thu, 20 Oct 2022 14:52:19 +0530 Subject: [PATCH 07/15] added: 4 custom alert functions --- README.md | 12 +- .../services/rest_api/api.dart | 189 +++++++++--------- .../rest_api/demo/demo_controller.dart | 11 +- .../rest_api/models/api_error_type.dart | 2 +- 4 files changed, 104 insertions(+), 110 deletions(-) diff --git a/README.md b/README.md index 4cd160ba..daabd485 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ flutter pub get ## Environments -### To run app in `production` mode use command +### To run app in `production` mode use command ``` flutter run --dart-define="environment=production" ``` @@ -47,7 +47,7 @@ flutter build ipa --dart-define="environment=production" ### How to create a new environment: -Go to `lib/env.dart`, find `envConfigs` variable and add configuration for the environment you want to create. example: for `testing` environment add `'testing'` key and value. And in commands pass your environment name +Go to `lib/env.dart`, find `envConfigs` variable and add configuration for the environment you want to create. Example: for `testing` environment add `'testing'` key and value. And in commands pass your environment name ``` // for example if environment name is testing then flutter run --dart-define="environment=testing" @@ -72,12 +72,12 @@ flutter pub outdated - 2 spaces for indentation - test files have `_test.ext` suffix in the file name > example `widget_test.dart` - Libraries, packages, directories, and source files name convention: snake_case(lowercase_with_underscores). -- Classes, enums, typedefs, and extensions naming conevntion: UpperCamelCase. +- Classes, Enums, Typedefs, and extensions naming convention: UpperCamelCase. - Variables, constants, parameters naming convention: lowerCamelCase. -- Method/ functions naming conevntion: lowerCamelCase. +- Method/ functions naming convention: lowerCamelCase. - Use relative path - - ✘ `import 'package:demo/home.dart';` -> This should be avoided. - - ✔ `import './home.dart';` -> Correct way + - ✘ `import 'package:demo/home.dart';` → This should be avoided. + - ✔ `import './home.dart';` → Correct way - to fix imports you can use [dart-import](https://marketplace.visualstudio.com/items?itemName=luanpotter.dart-import) - Avoid using as instead, use is operator - Avoid print()/ debugPrint() calls diff --git a/lib/vaahextendflutter/services/rest_api/api.dart b/lib/vaahextendflutter/services/rest_api/api.dart index 7d821c34..7dc06aba 100644 --- a/lib/vaahextendflutter/services/rest_api/api.dart +++ b/lib/vaahextendflutter/services/rest_api/api.dart @@ -1,14 +1,11 @@ import 'dart:async'; -import 'dart:io'; import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; -import 'package:dio_cookie_manager/dio_cookie_manager.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart' as getx; -import 'package:path_provider/path_provider.dart'; import '../../log/console.dart'; import 'models/api_error_type.dart'; @@ -44,13 +41,19 @@ class Api { required String url, Future Function(dynamic data, Response? res)? callback, String method = 'get', - Map? params, // data passed in post, put, etc. request - Map? query, - List>? headers, - bool showAlert = true, // if false on success/ error, nothing will be shown - String alertType = 'toast', - bool skipOnError = true, // if true then error dialog will be dismissible + Map? + params, // eg: { 'name': 'abc' }. params is data passed in post, put, etc. requests. + Map? query, // eg: { 'name': 'abc' } + List>? + headers, // eg: [{'title': 'content'}, {'key', 'value'}] int? customTimeoutLimit, + bool showAlert = + true, // if set false then on success or error, nothing will be shown + String alertType = 'toast', // 'toast' and 'dialog' are valid values + Future Function()? showSuccessToast, + Future Function()? showErrorToast, + Future Function()? showSuccessDialog, + Future Function()? showErrorDialog, Future Function()? onStart, Future Function()? onCompleted, Future Function(dynamic error)? onError, @@ -68,16 +71,19 @@ class Api { query: query, params: params, headers: headers, - skipOnError: skipOnError, customTimeoutLimit: customTimeoutLimit, showAlert: showAlert, alertType: alertType, + showErrorDialog: showErrorDialog, + showErrorToast: showErrorToast, ); - dynamic responseData = handleResponse( + dynamic responseData = await handleResponse( response, showAlert, alertType, + showSuccessDialog, + showSuccessToast, ); // On completed, use for hide loading @@ -112,7 +118,13 @@ class Api { // Timeout Error else if (error.type == DioErrorType.sendTimeout || error.type == DioErrorType.receiveTimeout) { - handleTimeoutError(error, skipOnError, showAlert, alertType); + await handleTimeoutError( + error, + showAlert, + alertType, + showErrorToast, + showErrorDialog, + ); if (callback != null) { await callback(null, null); } @@ -121,11 +133,12 @@ class Api { // Here response error means server sends error response. eg 401: unauthorised else if (error.type == DioErrorType.response) { - handleResponseError( + await handleResponseError( error, - skipOnError, showAlert, alertType, + showErrorToast, + showErrorDialog, ); if (callback != null) { await callback(null, null); @@ -169,9 +182,10 @@ class Api { required Map? params, required List>? headers, required int? customTimeoutLimit, - required bool skipOnError, required bool showAlert, required String alertType, + required Future Function()? showErrorDialog, + required Future Function()? showErrorToast, }) async { Response? response; final Options options = await _getOptions(); @@ -240,15 +254,20 @@ class Api { default: if (showAlert) { if (alertType == 'dialog') { + if (showErrorDialog != null) { + await showErrorDialog(); + break; + } showDialog( - skip: skipOnError, - dialog: () => defaultErrorDialog( - title: 'Error', - content: ['Invalid request type!'], - ), + title: 'Error', + content: ['Invalid request type!'], ); break; } else { + if (showErrorToast != null) { + await showErrorToast(); + break; + } showToast(content: 'Invalid request type!', toastType: 'failure'); break; } @@ -257,11 +276,13 @@ class Api { return response; } - dynamic handleResponse( + Future handleResponse( Response? response, bool showAlert, String alertType, - ) { + Future Function()? showSuccessDialog, + Future Function()? showSuccessToast, + ) async { if (response != null && response.data != null) { try { final Map formatedResponse = @@ -280,19 +301,23 @@ class Api { } if (showAlert) { if (alertType == 'dialog') { - showDialog( - skip: true, - dialog: () => defaultSuccessDialog( + if (showSuccessDialog != null) { + await showSuccessDialog(); + } else { + showDialog( title: 'Success', content: responseMessages, - hint: responseHint, - ), - ); + ); + } } else { - showToast( - content: responseMessages?.join('') ?? 'Successful', - toastType: 'success', - ); + if (showSuccessToast != null) { + await showSuccessToast(); + } else { + showToast( + content: responseMessages?.join(' ') ?? 'Successful', + toastType: 'success', + ); + } } } return responseData; @@ -303,23 +328,29 @@ class Api { throw Exception('response from server is null or response.data is null'); } - void handleTimeoutError( + Future handleTimeoutError( DioError error, - bool skipOnError, - showAlert, - alertType, - ) { + bool showAlert, + String alertType, + Future Function()? showErrorToast, + Future Function()? showErrorDialog, + ) async { Console.danger(error.toString()); if (showAlert) { if (alertType == 'dialog') { + if (showErrorDialog != null) { + await showErrorDialog(); + return; + } showDialog( - skip: skipOnError, - dialog: () => defaultErrorDialog( - title: 'Error', - content: ['Check your internet connection!'], - ), + title: 'Error', + content: ['Check your internet connection!'], ); } else { + if (showErrorToast != null) { + await showErrorToast(); + return; + } showToast( content: 'Check your internet connection!', toastType: 'failure', @@ -328,12 +359,13 @@ class Api { } } - void handleResponseError( + Future handleResponseError( Object error, - bool skipOnError, bool showAlert, String alertType, - ) { + Future Function()? showErrorToast, + Future Function()? showErrorDialog, + ) async { if (error is DioError && error.type == DioErrorType.response) { final Response? response = error.response; try { @@ -399,14 +431,20 @@ class Api { } if (showAlert) { if (alertType == 'dialog') { + if (showErrorDialog != null) { + await showErrorDialog(); + return; + } + Console.danger(apiErrorType.errors.toString()); showDialog( - skip: skipOnError, - dialog: () => defaultErrorDialog( - title: 'Error', - content: apiErrorType.errors, - ), + title: 'Error', + content: apiErrorType.errors, ); } else { + if (showErrorToast != null) { + await showErrorToast(); + return; + } showToast( content: apiErrorType.errors.isEmpty ? 'Error' @@ -426,7 +464,7 @@ class Api { void showToast({ required String content, toastType = 'default', - }) async { + }) { switch (toastType) { case 'success': defaultToast(content, Colors.green); @@ -451,23 +489,7 @@ class Api { ); } - void showDialog({ - required bool skip, - required Function() dialog, - }) async { - if (!skip) { - await dialog(); - showDialog( - skip: skip, - dialog: () => dialog, - ); - return; - } - await dialog(); - return; - } - - defaultSuccessDialog({ + showDialog({ required String title, List? content, String? hint, @@ -481,10 +503,10 @@ class Api { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (content != null) Text(content.join(' ')), + if (content != null && content.isNotEmpty) Text(content.join(' ')), // TODO: replace with const margin - if (content != null) const SizedBox(height: 12), - if (hint != null) Text(hint), + if (content != null && content.isNotEmpty) const SizedBox(height: 12), + if (hint != null && hint.trim().isNotEmpty) Text(hint), ], ), ), @@ -503,29 +525,4 @@ class Api { barrierDismissible: false, ); } - - defaultErrorDialog({ - required String title, - required List content, - List? actions, - }) { - return getx.Get.dialog( - CupertinoAlertDialog( - title: Text(title), - content: Text(content.join(' ')), - actions: [ - if (actions == null || actions.isNotEmpty) - CupertinoButton( - child: const Text('Okay'), - onPressed: () { - getx.Get.back(); - }, - ) - else - ...actions, - ], - ), - barrierDismissible: false, - ); - } } diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart index a9590eb2..3690cc16 100644 --- a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart +++ b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart @@ -1,3 +1,5 @@ +import 'dart:async'; + import 'package:get/get.dart' as getx; import 'package:team/vaahextendflutter/log/console.dart'; @@ -9,17 +11,12 @@ class DemoController extends getx.GetxController { Future getDemoURL() async { await api.ajax( - url: '/error', + url: '/search', callback: getDemoURLAfter, - query: {'code': 401}, - showAlert: false, - alertType: 'dialog', ); } Future getDemoURLAfter(dynamic data, dynamic resp) async { - if (data != null) { - Console.info(data.toString()); - } + Console.info('>>> callback <<< data: $data'); } } diff --git a/lib/vaahextendflutter/services/rest_api/models/api_error_type.dart b/lib/vaahextendflutter/services/rest_api/models/api_error_type.dart index 85575dab..225a047c 100644 --- a/lib/vaahextendflutter/services/rest_api/models/api_error_type.dart +++ b/lib/vaahextendflutter/services/rest_api/models/api_error_type.dart @@ -7,7 +7,7 @@ class ApiErrorType { const ApiErrorType({ this.code = ApiErrorCode.unknown, - this.errors = const ['Unknown'], + this.errors = const [], this.debug, }); From 2692a1215f3f789f476151c2b1a0ee6c73cb3f21 Mon Sep 17 00:00:00 2001 From: Prajapati Chintan Date: Thu, 20 Oct 2022 17:40:49 +0530 Subject: [PATCH 08/15] Updated: alert methods are moved in helper file --- lib/vaahextendflutter/services/helpers.dart | 59 +++++++++ .../services/rest_api/api.dart | 85 ++++++------- .../rest_api/demo/demo_controller.dart | 4 +- pubspec.lock | 112 ------------------ pubspec.yaml | 3 - 5 files changed, 105 insertions(+), 158 deletions(-) create mode 100644 lib/vaahextendflutter/services/helpers.dart diff --git a/lib/vaahextendflutter/services/helpers.dart b/lib/vaahextendflutter/services/helpers.dart new file mode 100644 index 00000000..39701084 --- /dev/null +++ b/lib/vaahextendflutter/services/helpers.dart @@ -0,0 +1,59 @@ +import 'package:flutter/material.dart'; +import 'package:fluttertoast/fluttertoast.dart'; + +class Helpers { + Helpers() { + showErrorDialog = null; + showErrorToast = null; + showSuccessDialog = null; + showSuccessToast = _showSuccessToast; + } + + Function({ + required String title, + List? content, + List? actions, + })? showErrorDialog; + + Function({required String content})? showErrorToast; + + Function({ + required String title, + List? content, + List? actions, + })? showSuccessDialog; + + Function({required String content})? showSuccessToast; +} + +void _showSuccessToast({required String content}) { + _showToast(content: content, toastType: 'success'); +} + +void _showToast({ + required String content, + String? toastType = 'default', +}) { + switch (toastType) { + case 'success': + _defaultToast(content, Colors.green); + break; + case 'failure': + _defaultToast(content, Colors.red); + break; + default: + _defaultToast(content, Colors.white); + break; + } +} + +void _defaultToast(String content, Color color) { + Fluttertoast.showToast( + msg: content, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: color.withOpacity(0.5), + textColor: Colors.black, + fontSize: 16.0, + ); +} diff --git a/lib/vaahextendflutter/services/rest_api/api.dart b/lib/vaahextendflutter/services/rest_api/api.dart index 7dc06aba..408a0610 100644 --- a/lib/vaahextendflutter/services/rest_api/api.dart +++ b/lib/vaahextendflutter/services/rest_api/api.dart @@ -1,26 +1,26 @@ import 'dart:async'; -import 'package:cookie_jar/cookie_jar.dart'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart' as getx; +import '../../../env.dart'; import '../../log/console.dart'; +import '../helpers.dart'; import 'models/api_error_type.dart'; -import '../../../env.dart'; // alertType : 'dialog', 'toast', class Api { // To check env variables logs enabled, apiUrl and timeout limit for requests late EnvController envController; + Helpers helpers = Helpers(); // Get base url by env late final String apiBaseUrl; final Dio dio = Dio(); - late PersistCookieJar cookieJar; Api() { _initApi(); @@ -50,10 +50,6 @@ class Api { bool showAlert = true, // if set false then on success or error, nothing will be shown String alertType = 'toast', // 'toast' and 'dialog' are valid values - Future Function()? showSuccessToast, - Future Function()? showErrorToast, - Future Function()? showSuccessDialog, - Future Function()? showErrorDialog, Future Function()? onStart, Future Function()? onCompleted, Future Function(dynamic error)? onError, @@ -74,16 +70,12 @@ class Api { customTimeoutLimit: customTimeoutLimit, showAlert: showAlert, alertType: alertType, - showErrorDialog: showErrorDialog, - showErrorToast: showErrorToast, ); dynamic responseData = await handleResponse( response, showAlert, alertType, - showSuccessDialog, - showSuccessToast, ); // On completed, use for hide loading @@ -122,8 +114,6 @@ class Api { error, showAlert, alertType, - showErrorToast, - showErrorDialog, ); if (callback != null) { await callback(null, null); @@ -137,14 +127,13 @@ class Api { error, showAlert, alertType, - showErrorToast, - showErrorDialog, ); if (callback != null) { await callback(null, null); } return; } + if (callback != null) { await callback(null, null); } @@ -184,8 +173,6 @@ class Api { required int? customTimeoutLimit, required bool showAlert, required String alertType, - required Future Function()? showErrorDialog, - required Future Function()? showErrorToast, }) async { Response? response; final Options options = await _getOptions(); @@ -254,8 +241,11 @@ class Api { default: if (showAlert) { if (alertType == 'dialog') { - if (showErrorDialog != null) { - await showErrorDialog(); + if (helpers.showErrorDialog != null) { + await helpers.showErrorDialog!( + title: 'Error', + content: ['Invalid request type!'], + ); break; } showDialog( @@ -264,8 +254,8 @@ class Api { ); break; } else { - if (showErrorToast != null) { - await showErrorToast(); + if (helpers.showErrorToast != null) { + await helpers.showErrorToast!(content: 'Invalid request type!'); break; } showToast(content: 'Invalid request type!', toastType: 'failure'); @@ -280,8 +270,6 @@ class Api { Response? response, bool showAlert, String alertType, - Future Function()? showSuccessDialog, - Future Function()? showSuccessToast, ) async { if (response != null && response.data != null) { try { @@ -301,8 +289,11 @@ class Api { } if (showAlert) { if (alertType == 'dialog') { - if (showSuccessDialog != null) { - await showSuccessDialog(); + if (helpers.showSuccessDialog != null) { + await helpers.showSuccessDialog!( + title: 'Success', + content: responseMessages, + ); } else { showDialog( title: 'Success', @@ -310,8 +301,10 @@ class Api { ); } } else { - if (showSuccessToast != null) { - await showSuccessToast(); + if (helpers.showSuccessToast != null) { + await helpers.showSuccessToast!( + content: responseMessages?.join(' ') ?? 'Successful', + ); } else { showToast( content: responseMessages?.join(' ') ?? 'Successful', @@ -332,14 +325,15 @@ class Api { DioError error, bool showAlert, String alertType, - Future Function()? showErrorToast, - Future Function()? showErrorDialog, ) async { Console.danger(error.toString()); if (showAlert) { if (alertType == 'dialog') { - if (showErrorDialog != null) { - await showErrorDialog(); + if (helpers.showErrorDialog != null) { + await helpers.showErrorDialog!( + title: 'Error', + content: ['Check your internet connection!'], + ); return; } showDialog( @@ -347,8 +341,10 @@ class Api { content: ['Check your internet connection!'], ); } else { - if (showErrorToast != null) { - await showErrorToast(); + if (helpers.showErrorToast != null) { + await helpers.showErrorToast!( + content: 'Check your internet connection!', + ); return; } showToast( @@ -363,8 +359,6 @@ class Api { Object error, bool showAlert, String alertType, - Future Function()? showErrorToast, - Future Function()? showErrorDialog, ) async { if (error is DioError && error.type == DioErrorType.response) { final Response? response = error.response; @@ -431,8 +425,11 @@ class Api { } if (showAlert) { if (alertType == 'dialog') { - if (showErrorDialog != null) { - await showErrorDialog(); + if (helpers.showErrorDialog != null) { + await helpers.showErrorDialog!( + title: 'Error', + content: apiErrorType.errors, + ); return; } Console.danger(apiErrorType.errors.toString()); @@ -441,8 +438,12 @@ class Api { content: apiErrorType.errors, ); } else { - if (showErrorToast != null) { - await showErrorToast(); + if (helpers.showErrorToast != null) { + await helpers.showErrorToast!( + content: apiErrorType.errors.isEmpty + ? 'Error' + : apiErrorType.errors.join(' '), + ); return; } showToast( @@ -503,9 +504,11 @@ class Api { child: Column( mainAxisSize: MainAxisSize.min, children: [ - if (content != null && content.isNotEmpty) Text(content.join(' ')), + if (content != null && content.isNotEmpty) + Text(content.join(' ')), // TODO: replace with const margin - if (content != null && content.isNotEmpty) const SizedBox(height: 12), + if (content != null && content.isNotEmpty) + const SizedBox(height: 12), if (hint != null && hint.trim().isNotEmpty) Text(hint), ], ), diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart index 3690cc16..f97cafc0 100644 --- a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart +++ b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart @@ -1,7 +1,7 @@ import 'dart:async'; import 'package:get/get.dart' as getx; -import 'package:team/vaahextendflutter/log/console.dart'; +import '../../../log/console.dart'; import '../api.dart'; @@ -11,7 +11,7 @@ class DemoController extends getx.GetxController { Future getDemoURL() async { await api.ajax( - url: '/search', + url: '/error', callback: getDemoURLAfter, ); } diff --git a/pubspec.lock b/pubspec.lock index 3e180250..e658acc1 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -43,13 +43,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "3.0.0" - cookie_jar: - dependency: "direct main" - description: - name: cookie_jar - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" cupertino_icons: dependency: "direct main" description: @@ -64,13 +57,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "4.0.6" - dio_cookie_manager: - dependency: "direct main" - description: - name: dio_cookie_manager - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.0" fake_async: dependency: transitive description: @@ -78,20 +64,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.1" - ffi: - dependency: transitive - description: - name: ffi - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.1" - file: - dependency: transitive - description: - name: file - url: "https://pub.dartlang.org" - source: hosted - version: "6.1.4" flutter: dependency: "direct main" description: flutter @@ -184,76 +156,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.2" - path_provider: - dependency: "direct main" - description: - name: path_provider - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.11" - path_provider_android: - dependency: transitive - description: - name: path_provider_android - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.20" - path_provider_ios: - dependency: transitive - description: - name: path_provider_ios - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.11" - path_provider_linux: - dependency: transitive - description: - name: path_provider_linux - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.7" - path_provider_macos: - dependency: transitive - description: - name: path_provider_macos - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.6" - path_provider_platform_interface: - dependency: transitive - description: - name: path_provider_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.0.5" - path_provider_windows: - dependency: transitive - description: - name: path_provider_windows - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.3" - platform: - dependency: transitive - description: - name: platform - url: "https://pub.dartlang.org" - source: hosted - version: "3.1.0" - plugin_platform_interface: - dependency: transitive - description: - name: plugin_platform_interface - url: "https://pub.dartlang.org" - source: hosted - version: "2.1.3" - process: - dependency: transitive - description: - name: process - url: "https://pub.dartlang.org" - source: hosted - version: "4.2.4" sky_engine: dependency: transitive description: flutter @@ -315,20 +217,6 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.2" - win32: - dependency: transitive - description: - name: win32 - url: "https://pub.dartlang.org" - source: hosted - version: "3.0.1" - xdg_directories: - dependency: transitive - description: - name: xdg_directories - url: "https://pub.dartlang.org" - source: hosted - version: "0.2.0+2" sdks: dart: ">=2.18.2 <3.0.0" flutter: ">=3.3.4" diff --git a/pubspec.yaml b/pubspec.yaml index f2d0d86f..814c6eb8 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,10 +16,7 @@ dependencies: get: ^4.6.5 colorize: ^3.0.0 flutter_screenutil: ^5.5.4 - cookie_jar: ^3.0.1 dio: ^4.0.6 - path_provider: ^2.0.11 - dio_cookie_manager: ^2.0.0 fluttertoast: ^8.1.1 dev_dependencies: From 10097b5654b56988ecf77a8c0ab56ed75ffaa5ce Mon Sep 17 00:00:00 2001 From: Prajapati Chintan Date: Thu, 20 Oct 2022 20:07:47 +0530 Subject: [PATCH 09/15] updated: converted ajax request to static method, removed unwanted files --- lib/main.dart | 4 +- lib/routes/routes.dart | 0 lib/vaahextendflutter/services/helpers.dart | 121 ++++++++---- .../services/rest_api/api.dart | 183 ++++++++---------- .../rest_api/demo/demo_controller.dart | 8 +- .../rest_api/models/api_error_type.dart | 18 -- 6 files changed, 168 insertions(+), 166 deletions(-) create mode 100644 lib/routes/routes.dart delete mode 100644 lib/vaahextendflutter/services/rest_api/models/api_error_type.dart diff --git a/lib/main.dart b/lib/main.dart index 7f7e3bd4..27771f9f 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'package:team/vaahextendflutter/services/rest_api/api.dart'; import 'env.dart'; import 'vaahextendflutter/base/base_stateful.dart'; @@ -7,7 +8,7 @@ import 'vaahextendflutter/log/console.dart'; import 'vaahextendflutter/services/rest_api/demo/demo_ui.dart'; import 'vaahextendflutter/tag/tag.dart'; -void main() { +void main() async { WidgetsFlutterBinding.ensureInitialized(); String environment = const String.fromEnvironment('environment', defaultValue: 'default'); @@ -20,6 +21,7 @@ void main() { Console.info( '>>>>> ${envController.config.version}+${envController.config.build}', ); + await Api.initApi(); runApp(const TeamApp()); } diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/vaahextendflutter/services/helpers.dart b/lib/vaahextendflutter/services/helpers.dart index 39701084..18c88307 100644 --- a/lib/vaahextendflutter/services/helpers.dart +++ b/lib/vaahextendflutter/services/helpers.dart @@ -1,59 +1,94 @@ +import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; +import 'package:get/get.dart'; class Helpers { - Helpers() { - showErrorDialog = null; - showErrorToast = null; - showSuccessDialog = null; - showSuccessToast = _showSuccessToast; + + static logout() { + // Navigate using getx } - Function({ - required String title, - List? content, - List? actions, - })? showErrorDialog; - Function({required String content})? showErrorToast; + // ignore: unused_element + static _toast({required String content, Color color = Colors.white}) { + Fluttertoast.showToast( + msg: content, + toastLength: Toast.LENGTH_SHORT, + gravity: ToastGravity.BOTTOM, + backgroundColor: color.withOpacity(0.5), + textColor: color, + fontSize: 16.0, + ); + } - Function({ + // ignore: unused_element + static _dialog({ required String title, List? content, - List? actions, - })? showSuccessDialog; + String? hint, + List? actions, + }) { + return Get.dialog( + CupertinoAlertDialog( + title: Text(title), + content: SingleChildScrollView( + physics: const BouncingScrollPhysics(), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + if (content != null && content.isNotEmpty) + Text(content.join('\n')), + // TODO: replace with const margin + if (content != null && content.isNotEmpty) + const SizedBox(height: 8), + if (hint != null && hint.trim().isNotEmpty) Text(hint), + ], + ), + ), + actions: [ + if (actions == null || actions.isNotEmpty) + CupertinoButton( + child: const Text('Okay'), + onPressed: () { + Get.back(); + }, + ) + else + ...actions, + ], + ), + barrierDismissible: false, + ); + } - Function({required String content})? showSuccessToast; -} + static showErrorToast({required String content}) { + _toast(content: content, color: Colors.red); + } -void _showSuccessToast({required String content}) { - _showToast(content: content, toastType: 'success'); -} + // static const showErrorToast = null; -void _showToast({ - required String content, - String? toastType = 'default', -}) { - switch (toastType) { - case 'success': - _defaultToast(content, Colors.green); - break; - case 'failure': - _defaultToast(content, Colors.red); - break; - default: - _defaultToast(content, Colors.white); - break; + static showSuccessToast({required String content}) { + _toast(content: content, color: Colors.green); } -} -void _defaultToast(String content, Color color) { - Fluttertoast.showToast( - msg: content, - toastLength: Toast.LENGTH_SHORT, - gravity: ToastGravity.BOTTOM, - backgroundColor: color.withOpacity(0.5), - textColor: Colors.black, - fontSize: 16.0, - ); + // static const showSuccessToast = null; + + // static showErrorDialog({ + // required String title, + // List? content, + // String? hint, + // List? actions, + // }) {} + + static const showErrorDialog = null; + + // static showSuccessDialog({ + // required String title, + // List? content, + // String? hint, + // List? actions, + // }) {} + + static const showSuccessDialog = null; } diff --git a/lib/vaahextendflutter/services/rest_api/api.dart b/lib/vaahextendflutter/services/rest_api/api.dart index 408a0610..4cf3b4a6 100644 --- a/lib/vaahextendflutter/services/rest_api/api.dart +++ b/lib/vaahextendflutter/services/rest_api/api.dart @@ -8,26 +8,21 @@ import 'package:get/get.dart' as getx; import '../../../env.dart'; import '../../log/console.dart'; -import '../helpers.dart'; -import 'models/api_error_type.dart'; +import '../Helpers.dart'; // alertType : 'dialog', 'toast', + class Api { // To check env variables logs enabled, apiUrl and timeout limit for requests - late EnvController envController; - Helpers helpers = Helpers(); + static late final EnvController _envController; // Get base url by env - late final String apiBaseUrl; - final Dio dio = Dio(); - - Api() { - _initApi(); - } + static late final String _apiBaseUrl; + static final Dio _dio = Dio(); // Get request header options - Future _getOptions( + static Future _getOptions( {String contentType = Headers.jsonContentType}) async { final Map header = {}; header.addAll({'Accept': 'application/json'}); @@ -37,7 +32,7 @@ class Api { // return type of ajax is ApiResponseType? so if there is error // then null will be returned otherwise ApiResponseType object - Future ajax({ + static Future ajax({ required String url, Future Function(dynamic data, Response? res)? callback, String method = 'get', @@ -61,7 +56,7 @@ class Api { await onStart(); } - Response? response = await handleRequest( + Response? response = await _handleRequest( url: url, method: method, query: query, @@ -72,7 +67,7 @@ class Api { alertType: alertType, ); - dynamic responseData = await handleResponse( + dynamic responseData = await _handleResponse( response, showAlert, alertType, @@ -110,7 +105,7 @@ class Api { // Timeout Error else if (error.type == DioErrorType.sendTimeout || error.type == DioErrorType.receiveTimeout) { - await handleTimeoutError( + await _handleTimeoutError( error, showAlert, alertType, @@ -123,7 +118,7 @@ class Api { // Here response error means server sends error response. eg 401: unauthorised else if (error.type == DioErrorType.response) { - await handleResponseError( + await _handleResponseError( error, showAlert, alertType, @@ -146,16 +141,16 @@ class Api { } } - Future _initApi() async { + static Future initApi() async { bool envControllerExists = getx.Get.isRegistered(); if (!envControllerExists) { throw Exception('envController does not exist in app'); } // get env controller and set variable showEnvAndVersionTag - envController = getx.Get.find(); - apiBaseUrl = envController.config.apiBaseUrl; - if (envController.config.enableApiLogs) { - dio.interceptors.add( + _envController = getx.Get.find(); + _apiBaseUrl = _envController.config.apiBaseUrl; + if (_envController.config.enableApiLogs) { + _dio.interceptors.add( LogInterceptor( responseBody: true, requestBody: true, @@ -164,7 +159,7 @@ class Api { } } - Future?> handleRequest({ + static Future?> _handleRequest({ required String method, required String url, required Map? query, @@ -177,9 +172,9 @@ class Api { Response? response; final Options options = await _getOptions(); options.sendTimeout = - customTimeoutLimit ?? envController.config.timeoutLimit; + customTimeoutLimit ?? _envController.config.timeoutLimit; options.receiveTimeout = - customTimeoutLimit ?? envController.config.timeoutLimit; + customTimeoutLimit ?? _envController.config.timeoutLimit; if (headers != null && headers.isNotEmpty) { if (options.headers != null) { for (Map element in headers) { @@ -195,16 +190,16 @@ class Api { } switch (method) { case 'get': - response = await dio.get( - '$apiBaseUrl$url', + response = await _dio.get( + '$_apiBaseUrl$url', queryParameters: query, options: options, ); break; case 'post': - response = await dio.post( - '$apiBaseUrl$url', + response = await _dio.post( + '$_apiBaseUrl$url', data: params, queryParameters: query, options: options, @@ -212,8 +207,8 @@ class Api { break; case 'put': - response = await dio.put( - '$apiBaseUrl$url', + response = await _dio.put( + '$_apiBaseUrl$url', data: params, queryParameters: query, options: options, @@ -221,8 +216,8 @@ class Api { break; case 'patch': - response = await dio.patch( - '$apiBaseUrl$url', + response = await _dio.patch( + '$_apiBaseUrl$url', data: params, queryParameters: query, options: options, @@ -230,8 +225,8 @@ class Api { break; case 'delete': - response = await dio.delete( - '$apiBaseUrl$url', + response = await _dio.delete( + '$_apiBaseUrl$url', data: params, queryParameters: query, options: options, @@ -241,24 +236,26 @@ class Api { default: if (showAlert) { if (alertType == 'dialog') { - if (helpers.showErrorDialog != null) { - await helpers.showErrorDialog!( + // ignore: unnecessary_null_comparison + if (Helpers.showErrorDialog != null) { + await Helpers.showErrorDialog( title: 'Error', content: ['Invalid request type!'], ); break; } - showDialog( + _showDialog( title: 'Error', content: ['Invalid request type!'], ); break; } else { - if (helpers.showErrorToast != null) { - await helpers.showErrorToast!(content: 'Invalid request type!'); + // ignore: unnecessary_null_comparison + if (Helpers.showErrorToast != null) { + await Helpers.showErrorToast(content: 'Invalid request type!'); break; } - showToast(content: 'Invalid request type!', toastType: 'failure'); + _showToast(content: 'Invalid request type!', color: Colors.red); break; } } @@ -266,7 +263,7 @@ class Api { return response; } - Future handleResponse( + static Future _handleResponse( Response? response, bool showAlert, String alertType, @@ -289,26 +286,28 @@ class Api { } if (showAlert) { if (alertType == 'dialog') { - if (helpers.showSuccessDialog != null) { - await helpers.showSuccessDialog!( + // ignore: unnecessary_null_comparison + if (Helpers.showSuccessDialog != null) { + await Helpers.showSuccessDialog( title: 'Success', content: responseMessages, ); } else { - showDialog( + _showDialog( title: 'Success', content: responseMessages, ); } } else { - if (helpers.showSuccessToast != null) { - await helpers.showSuccessToast!( - content: responseMessages?.join(' ') ?? 'Successful', + // ignore: unnecessary_null_comparison + if (Helpers.showSuccessToast != null) { + await Helpers.showSuccessToast( + content: responseMessages?.join('\n') ?? 'Successful', ); } else { - showToast( - content: responseMessages?.join(' ') ?? 'Successful', - toastType: 'success', + _showToast( + content: responseMessages?.join('\n') ?? 'Successful', + color: Colors.green, ); } } @@ -321,7 +320,7 @@ class Api { throw Exception('response from server is null or response.data is null'); } - Future handleTimeoutError( + static Future _handleTimeoutError( DioError error, bool showAlert, String alertType, @@ -329,33 +328,35 @@ class Api { Console.danger(error.toString()); if (showAlert) { if (alertType == 'dialog') { - if (helpers.showErrorDialog != null) { - await helpers.showErrorDialog!( + // ignore: unnecessary_null_comparison + if (Helpers.showErrorDialog != null) { + await Helpers.showErrorDialog( title: 'Error', content: ['Check your internet connection!'], ); return; } - showDialog( + _showDialog( title: 'Error', content: ['Check your internet connection!'], ); } else { - if (helpers.showErrorToast != null) { - await helpers.showErrorToast!( + // ignore: unnecessary_null_comparison + if (Helpers.showErrorToast != null) { + await Helpers.showErrorToast( content: 'Check your internet connection!', ); return; } - showToast( + _showToast( content: 'Check your internet connection!', - toastType: 'failure', + color: Colors.green, ); } } } - Future handleResponseError( + static Future _handleResponseError( Object error, bool showAlert, String alertType, @@ -391,11 +392,11 @@ class Api { ); } catch (e) { if (error.type == DioErrorType.response) { - ApiErrorCode errorCode = ApiErrorCode.unknown; + String errorCode = 'unknown'; List errors = [error.message]; String? debug; if (error.response?.statusCode == 401) { - errorCode = ApiErrorCode.unauthorized; + errorCode = 'unauthorized'; } if (error.response?.data != null) { try { @@ -415,42 +416,40 @@ class Api { ); } } - ApiErrorType apiErrorType = - ApiErrorType(code: errorCode, errors: errors, debug: debug); - if (apiErrorType.code == ApiErrorCode.unauthorized) { + if (errorCode == 'unauthorized') { Console.danger('Error type: unauthorized'); - // TODO: Logout - // Logout user from controller and then send them to login screen - // after that show the error dialog + Helpers.logout(); } if (showAlert) { if (alertType == 'dialog') { - if (helpers.showErrorDialog != null) { - await helpers.showErrorDialog!( + // ignore: unnecessary_null_comparison + if (Helpers.showErrorDialog != null) { + await Helpers.showErrorDialog( title: 'Error', - content: apiErrorType.errors, + content: errors, ); return; } - Console.danger(apiErrorType.errors.toString()); - showDialog( + Console.danger(errors.toString()); + _showDialog( title: 'Error', - content: apiErrorType.errors, + content: errors, ); } else { - if (helpers.showErrorToast != null) { - await helpers.showErrorToast!( - content: apiErrorType.errors.isEmpty + // ignore: unnecessary_null_comparison + if (Helpers.showErrorToast != null) { + await Helpers.showErrorToast( + content: errors.isEmpty ? 'Error' - : apiErrorType.errors.join(' '), + : errors.join('\n'), ); return; } - showToast( - content: apiErrorType.errors.isEmpty + _showToast( + content: errors.isEmpty ? 'Error' - : apiErrorType.errors.join(' '), - toastType: 'failure', + : errors.join('\n'), + color: Colors.green, ); } } @@ -462,24 +461,10 @@ class Api { } } - void showToast({ + static void _showToast({ required String content, - toastType = 'default', + Color color = Colors.white, }) { - switch (toastType) { - case 'success': - defaultToast(content, Colors.green); - break; - case 'failure': - defaultToast(content, Colors.red); - break; - default: - defaultToast(content, Colors.white); - break; - } - } - - void defaultToast(String content, Color color) { Fluttertoast.showToast( msg: content, toastLength: Toast.LENGTH_SHORT, @@ -490,7 +475,7 @@ class Api { ); } - showDialog({ + static _showDialog({ required String title, List? content, String? hint, @@ -505,7 +490,7 @@ class Api { mainAxisSize: MainAxisSize.min, children: [ if (content != null && content.isNotEmpty) - Text(content.join(' ')), + Text(content.join('\n')), // TODO: replace with const margin if (content != null && content.isNotEmpty) const SizedBox(height: 12), diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart index f97cafc0..6a746f38 100644 --- a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart +++ b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart @@ -6,12 +6,10 @@ import '../../../log/console.dart'; import '../api.dart'; class DemoController extends getx.GetxController { - // Call API - Api api = Api(); - Future getDemoURL() async { - await api.ajax( - url: '/error', + await Api.ajax( + url: '/search', + query: {'code': 401}, callback: getDemoURLAfter, ); } diff --git a/lib/vaahextendflutter/services/rest_api/models/api_error_type.dart b/lib/vaahextendflutter/services/rest_api/models/api_error_type.dart deleted file mode 100644 index 225a047c..00000000 --- a/lib/vaahextendflutter/services/rest_api/models/api_error_type.dart +++ /dev/null @@ -1,18 +0,0 @@ -enum ApiErrorCode { unknown, unauthorized } - -class ApiErrorType { - final ApiErrorCode code; - final List errors; - final String? debug; - - const ApiErrorType({ - this.code = ApiErrorCode.unknown, - this.errors = const [], - this.debug, - }); - - @override - String toString() { - return 'ApiErrorType{code: $code, errors: ${errors.join(' ')}, debug: $debug}'; - } -} From af7eca8a5d89f4ab91cff6c3d3125584bde91800 Mon Sep 17 00:00:00 2001 From: Prajapati Chintan Date: Fri, 21 Oct 2022 14:55:15 +0530 Subject: [PATCH 10/15] added: base controller, updated: helpers Alert UI, fixxed: apitypeErrors --- lib/controllers/base_controller.dart | 22 ++++ lib/env.dart | 118 +++++++++++++++++- lib/main.dart | 20 +-- lib/vaahextendflutter/services/helpers.dart | 113 +++++++++++------ .../services/rest_api/api.dart | 55 ++++---- .../rest_api/demo/demo_controller.dart | 3 +- .../services/rest_api/demo/demo_ui.dart | 3 +- lib/vaahextendflutter/tag/tag.dart | 4 +- 8 files changed, 259 insertions(+), 79 deletions(-) create mode 100644 lib/controllers/base_controller.dart diff --git a/lib/controllers/base_controller.dart b/lib/controllers/base_controller.dart new file mode 100644 index 00000000..8cedd432 --- /dev/null +++ b/lib/controllers/base_controller.dart @@ -0,0 +1,22 @@ +import 'package:get/get.dart'; + +import '../env.dart'; +import '../vaahextendflutter/log/console.dart'; +import '../vaahextendflutter/services/rest_api/api.dart'; + +class BaseController extends GetxController { + Future init() async { + String environment = + const String.fromEnvironment('environment', defaultValue: 'default'); + final EnvController envController = Get.put( + EnvController( + environment, + ), + ); + Console.info('>>>>> ${envController.config.envType}'); + Console.info( + '>>>>> ${envController.config.version}+${envController.config.build}', + ); + await Api.initApi(); + } +} diff --git a/lib/env.dart b/lib/env.dart index 9249f5fa..fb3c11a8 100644 --- a/lib/env.dart +++ b/lib/env.dart @@ -16,14 +16,14 @@ EnvironmentConfig defaultConfig = const EnvironmentConfig( version: version, build: build, baseUrl: '', - apiBaseUrl: 'https://xrestapi.herokuapp.com', + apiBaseUrl: 'http://192.168.1.12:5000', timeoutLimit: 60 * 1000, // 60 seconds analyticsId: '', enableConsoleLogs: true, enableLocalLogs: true, enableApiLogs: true, showEnvAndVersionTag: true, - envAndVersionTagColor: Colors.red, + envAndVersionTagColor: kDangerColor, ); // To add new configuration add new key, value pair in envConfigs @@ -131,3 +131,117 @@ class EnvironmentConfig { ); } } + +const MaterialColor kPrimaryColor = MaterialColor( + 0xFF3366FF, + { + 50: Color(0xFFD6E4FF), + 100: Color(0xFFD6E4FF), + 200: Color(0xFFADC8FF), + 300: Color(0xFF84A9FF), + 400: Color(0xFF6690FF), + 500: Color(0xFF3366FF), + 600: Color(0xFF254EDB), + 700: Color(0xFF1939B7), + 800: Color(0xFF102693), + 900: Color(0xFF091A7A), + }, +); + +const MaterialColor kSuccessColor = MaterialColor( + 0xFF4FB52D, + { + 50: Color(0xFFE9FBD5), + 100: Color(0xFFE9FBD5), + 200: Color(0xFFCFF7AD), + 300: Color(0xFFA8E87F), + 400: Color(0xFF81D25B), + 500: Color(0xFF4FB52D), + 600: Color(0xFF369B20), + 700: Color(0xFF228216), + 800: Color(0xFF11680E), + 900: Color(0xFF08560B), + }, +); + +const MaterialColor kInfoColor = MaterialColor( + 0xFF4CA8FF, + { + 50: Color(0xFFDBF4FF), + 100: Color(0xFFDBF4FF), + 200: Color(0xFFB7E7FF), + 300: Color(0xFF93D5FF), + 400: Color(0xFF78C4FF), + 500: Color(0xFF4CA8FF), + 600: Color(0xFF3783DB), + 700: Color(0xFF2662B7), + 800: Color(0xFF184493), + 900: Color(0xFF0E2F7A), + }, +); + +const MaterialColor kWarningColor = MaterialColor( + 0xFFFFBF00, + { + 50: Color(0xFFFFF7CC), + 100: Color(0xFFFFF7CC), + 200: Color(0xFFFFED99), + 300: Color(0xFFFFE066), + 400: Color(0xFFFFD33F), + 500: Color(0xFFFFBF00), + 600: Color(0xFFDB9E00), + 700: Color(0xFFB77F00), + 800: Color(0xFF936300), + 900: Color(0xFF7A4E00), + }, +); + +const MaterialColor kDangerColor = MaterialColor( + 0xFFFF382D, + { + 50: Color(0xFFFFE5D5), + 100: Color(0xFFFFE5D5), + 200: Color(0xFFFFC4AB), + 300: Color(0xFFFF9C81), + 400: Color(0xFFFF7661), + 500: Color(0xFFFF382D), + 600: Color(0xFFDB2026), + 700: Color(0xFFB71629), + 800: Color(0xFF930E28), + 900: Color(0xFF7A0828), + }, +); + + + +const MaterialColor kWhiteColor = MaterialColor( + 0xFFFFFFFF, + { + 50: Color(0xFFFFFFFF), + 100: Color(0xFFFFFFFF), + 200: Color(0xFFFFFFFF), + 300: Color(0xFFFFFFFF), + 400: Color(0xFFFFFFFF), + 500: Color(0xFFFFFFFF), + 600: Color(0xFFFFFFFF), + 700: Color(0xFFFFFFFF), + 800: Color(0xFFFFFFFF), + 900: Color(0xFFFFFFFF), + }, +); + +const MaterialColor kBlackColor = MaterialColor( + 0xFF000000, + { + 50: Color(0xFFF2F2F2), + 100: Color(0xFFF2F2F2), + 200: Color(0xFFE5E5E5), + 300: Color(0xFFB2B2B2), + 400: Color(0xFF666666), + 500: Color(0xFF000000), + 600: Color(0xFF000000), + 700: Color(0xFF000000), + 800: Color(0xFF000000), + 900: Color(0xFF000000), + }, +); diff --git a/lib/main.dart b/lib/main.dart index 27771f9f..a28d5e9b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,27 +1,16 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'package:team/vaahextendflutter/services/rest_api/api.dart'; +import 'controllers/base_controller.dart'; import 'env.dart'; import 'vaahextendflutter/base/base_stateful.dart'; -import 'vaahextendflutter/log/console.dart'; import 'vaahextendflutter/services/rest_api/demo/demo_ui.dart'; import 'vaahextendflutter/tag/tag.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); - String environment = - const String.fromEnvironment('environment', defaultValue: 'default'); - final EnvController envController = Get.put( - EnvController( - environment, - ), - ); - Console.info('>>>>> ${envController.config.envType}'); - Console.info( - '>>>>> ${envController.config.version}+${envController.config.build}', - ); - await Api.initApi(); + BaseController baseController = Get.put(BaseController()); + await baseController.init(); runApp(const TeamApp()); } @@ -33,7 +22,7 @@ class TeamApp extends StatelessWidget { return GetMaterialApp( title: 'WebReinvent Team', theme: ThemeData( - primarySwatch: Colors.blue, + primarySwatch: kPrimaryColor, ), home: const TeamHomePage(), ); @@ -60,6 +49,7 @@ class _TeamHomePageState extends BaseStateful { Widget build(BuildContext context) { super.build(context); return Scaffold( + backgroundColor: kWarningColor, appBar: AppBar(), body: const TagWrapper( alignment: Alignment.topCenter, diff --git a/lib/vaahextendflutter/services/helpers.dart b/lib/vaahextendflutter/services/helpers.dart index 18c88307..e0fb8ec4 100644 --- a/lib/vaahextendflutter/services/helpers.dart +++ b/lib/vaahextendflutter/services/helpers.dart @@ -1,17 +1,16 @@ -import 'package:flutter/cupertino.dart'; import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart'; +import '../../env.dart'; + class Helpers { - static logout() { // Navigate using getx } - // ignore: unused_element - static _toast({required String content, Color color = Colors.white}) { + static _toast({required String content, Color color = kWhiteColor}) { Fluttertoast.showToast( msg: content, toastLength: Toast.LENGTH_SHORT, @@ -28,31 +27,65 @@ class Helpers { List? content, String? hint, List? actions, + Color color = kWhiteColor, }) { return Get.dialog( - CupertinoAlertDialog( - title: Text(title), + AlertDialog( + shape: const RoundedRectangleBorder( + borderRadius: BorderRadius.all( + // TODO: define const + Radius.circular(16.0), + ), + ), + contentPadding: EdgeInsets.zero, + title: Center(child: Text(title)), content: SingleChildScrollView( physics: const BouncingScrollPhysics(), child: Column( mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, children: [ - if (content != null && content.isNotEmpty) - Text(content.join('\n')), // TODO: replace with const margin if (content != null && content.isNotEmpty) + const SizedBox(height: 12), + if (content != null && content.isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + content.join('\n'), + textAlign: TextAlign.justify, + ), + ), + if ((content != null && content.isNotEmpty) || (hint != null && hint.trim().isNotEmpty)) + const SizedBox(height: 8), + if (hint != null && hint.trim().isNotEmpty) + Padding( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + hint, + textAlign: TextAlign.justify, + ), + ), + if (hint != null && hint.trim().isNotEmpty) const SizedBox(height: 8), - if (hint != null && hint.trim().isNotEmpty) Text(hint), ], ), ), actions: [ if (actions == null || actions.isNotEmpty) - CupertinoButton( - child: const Text('Okay'), - onPressed: () { - Get.back(); - }, + Center( + child: ElevatedButton( + style: ElevatedButton.styleFrom(backgroundColor: color), + child: Text( + 'Ok', + style: TextStyle( + color: color == kWhiteColor ? kBlackColor : kWhiteColor, + ), + ), + onPressed: () { + Get.back(); + }, + ), ) else ...actions, @@ -63,32 +96,40 @@ class Helpers { } static showErrorToast({required String content}) { - _toast(content: content, color: Colors.red); + _toast(content: content, color: kDangerColor); } - // static const showErrorToast = null; - static showSuccessToast({required String content}) { - _toast(content: content, color: Colors.green); + _toast(content: content, color: kSuccessColor); } - // static const showSuccessToast = null; - - // static showErrorDialog({ - // required String title, - // List? content, - // String? hint, - // List? actions, - // }) {} - - static const showErrorDialog = null; - - // static showSuccessDialog({ - // required String title, - // List? content, - // String? hint, - // List? actions, - // }) {} + static showErrorDialog({ + required String title, + List? content, + String? hint, + List? actions, + }) { + _dialog( + title: title, + content: content, + hint: hint, + actions: actions, + color: kDangerColor, + ); + } - static const showSuccessDialog = null; + static showSuccessDialog({ + required String title, + List? content, + String? hint, + List? actions, + }) { + _dialog( + title: title, + content: content, + hint: hint, + actions: actions, + color: kSuccessColor, + ); + } } diff --git a/lib/vaahextendflutter/services/rest_api/api.dart b/lib/vaahextendflutter/services/rest_api/api.dart index 4cf3b4a6..e9120fb9 100644 --- a/lib/vaahextendflutter/services/rest_api/api.dart +++ b/lib/vaahextendflutter/services/rest_api/api.dart @@ -2,7 +2,6 @@ import 'dart:async'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; -import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart' as getx; @@ -12,7 +11,6 @@ import '../Helpers.dart'; // alertType : 'dialog', 'toast', - class Api { // To check env variables logs enabled, apiUrl and timeout limit for requests static late final EnvController _envController; @@ -241,12 +239,15 @@ class Api { await Helpers.showErrorDialog( title: 'Error', content: ['Invalid request type!'], + hint: + "get, post, put, patch, delete request types are allowed.", ); break; } _showDialog( title: 'Error', content: ['Invalid request type!'], + hint: "get, post, put, patch, delete request types are allowed.", ); break; } else { @@ -255,7 +256,7 @@ class Api { await Helpers.showErrorToast(content: 'Invalid request type!'); break; } - _showToast(content: 'Invalid request type!', color: Colors.red); + _showToast(content: 'Invalid request type!', color: kDangerColor); break; } } @@ -263,7 +264,7 @@ class Api { return response; } - static Future _handleResponse( + static Future _handleResponse( Response? response, bool showAlert, String alertType, @@ -276,11 +277,15 @@ class Api { if (responseData == null) { Console.warning('response doesn\'t contain data key.'); } - List? responseMessages = formatedResponse['messages']; - if (responseMessages == null) { + List? responseMessages; + if (formatedResponse['messages'] == null) { Console.warning('response doesn\'t contain messages key.'); + } else { + responseMessages = (formatedResponse['messages'] as List) + .map((e) => e.toString()) + .toList(); } - String? responseHint = formatedResponse['hint']; + String? responseHint = formatedResponse['hint'] as String?; if (responseHint == null) { Console.warning('response doesn\'t contain hint key.'); } @@ -291,11 +296,13 @@ class Api { await Helpers.showSuccessDialog( title: 'Success', content: responseMessages, + hint: responseHint, ); } else { _showDialog( title: 'Success', content: responseMessages, + hint: responseHint, ); } } else { @@ -307,7 +314,7 @@ class Api { } else { _showToast( content: responseMessages?.join('\n') ?? 'Successful', - color: Colors.green, + color: kSuccessColor, ); } } @@ -320,7 +327,7 @@ class Api { throw Exception('response from server is null or response.data is null'); } - static Future _handleTimeoutError( + static Future _handleTimeoutError( DioError error, bool showAlert, String alertType, @@ -350,7 +357,7 @@ class Api { } _showToast( content: 'Check your internet connection!', - color: Colors.green, + color: kSuccessColor, ); } } @@ -400,13 +407,19 @@ class Api { } if (error.response?.data != null) { try { + Console.danger('${error.response}'); final Map response = error.response?.data as Map; - errors = response['errors'] ?? []; + Console.danger('$response'); + if (response['errors'] != null) { + errors = (response['errors'] as List) + .map((e) => e.toString()) + .toList(); + } if (errors.isEmpty) { Console.warning('response doesn\'t contain errors key.'); } - debug = response['debug']; + debug = response['debug'] as String?; if (debug == null) { Console.warning('response doesn\'t contain debug key.'); } @@ -427,6 +440,7 @@ class Api { await Helpers.showErrorDialog( title: 'Error', content: errors, + hint: debug, ); return; } @@ -434,22 +448,19 @@ class Api { _showDialog( title: 'Error', content: errors, + hint: debug, ); } else { // ignore: unnecessary_null_comparison if (Helpers.showErrorToast != null) { await Helpers.showErrorToast( - content: errors.isEmpty - ? 'Error' - : errors.join('\n'), + content: errors.isEmpty ? 'Error' : errors.join('\n'), ); return; } _showToast( - content: errors.isEmpty - ? 'Error' - : errors.join('\n'), - color: Colors.green, + content: errors.isEmpty ? 'Error' : errors.join('\n'), + color: kSuccessColor, ); } } @@ -463,14 +474,14 @@ class Api { static void _showToast({ required String content, - Color color = Colors.white, + Color color = kWhiteColor, }) { Fluttertoast.showToast( msg: content, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM, backgroundColor: color.withOpacity(0.5), - textColor: Colors.black, + textColor: color == kWhiteColor ? kBlackColor : kWhiteColor, fontSize: 16.0, ); } @@ -501,7 +512,7 @@ class Api { actions: [ if (actions == null || actions.isNotEmpty) CupertinoButton( - child: const Text('Okay'), + child: const Text('Ok'), onPressed: () { getx.Get.back(); }, diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart index 6a746f38..0de015d7 100644 --- a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart +++ b/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart @@ -1,8 +1,8 @@ import 'dart:async'; import 'package:get/get.dart' as getx; -import '../../../log/console.dart'; +import '../../../log/console.dart'; import '../api.dart'; class DemoController extends getx.GetxController { @@ -11,6 +11,7 @@ class DemoController extends getx.GetxController { url: '/search', query: {'code': 401}, callback: getDemoURLAfter, + alertType: 'dialog' ); } diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart b/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart index e48d484f..a1ba46bb 100644 --- a/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart +++ b/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart @@ -1,6 +1,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import '../../../../env.dart'; import '../../../base/base_stateful.dart'; import 'demo_controller.dart'; @@ -49,7 +50,7 @@ class _DemoUIState extends BaseStateful { height: 20, width: 20, child: CircularProgressIndicator( - color: Colors.white, + color: kWhiteColor, ), ), ) diff --git a/lib/vaahextendflutter/tag/tag.dart b/lib/vaahextendflutter/tag/tag.dart index 901fdf10..44495b78 100644 --- a/lib/vaahextendflutter/tag/tag.dart +++ b/lib/vaahextendflutter/tag/tag.dart @@ -34,7 +34,7 @@ class TagWrapper extends StatelessWidget { child: Container( margin: margin, child: tagWidget( - color: Colors.red, + color: kDangerColor, envType: envController!.config.envType, version: envController.config.version, build: envController.config.build, @@ -57,7 +57,7 @@ Widget tagWidget({ padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), decoration: BoxDecoration( border: Border.all( - color: Colors.red, + color: kDangerColor, width: 2, ), borderRadius: BorderRadius.circular(20), From dc7360ca5a8f68dec7ec36ec80bf787b7eb7fb14 Mon Sep 17 00:00:00 2001 From: Prajapati Chintan Date: Fri, 21 Oct 2022 17:10:49 +0530 Subject: [PATCH 11/15] Updated: Refactored directory structure --- lib/controllers/base_controller.dart | 4 ++-- lib/env.dart | 20 +++++++++---------- lib/main.dart | 6 +++--- lib/vaahextendflutter/base/base_stateful.dart | 2 +- .../base/base_stateless.dart | 2 +- lib/vaahextendflutter/base/base_theme.dart | 0 .../{log => helpers}/console.dart | 2 +- .../date_time_helpers.dart} | 0 .../{services => helpers}/helpers.dart | 20 +++++++++---------- .../{log => helpers}/local.dart | 0 .../screen_helpers.dart} | 0 .../services/{rest_api => }/api.dart | 16 +++++++-------- .../self_signed.dart => api_self_signed.dart} | 0 lib/vaahextendflutter/tag/tag.dart | 6 +++--- .../widgets}/demo/demo_controller.dart | 9 ++++----- .../widgets}/demo/demo_ui.dart | 6 +++--- 16 files changed, 46 insertions(+), 47 deletions(-) create mode 100644 lib/vaahextendflutter/base/base_theme.dart rename lib/vaahextendflutter/{log => helpers}/console.dart (98%) rename lib/vaahextendflutter/{services/date_time_extension.dart => helpers/date_time_helpers.dart} (100%) rename lib/vaahextendflutter/{services => helpers}/helpers.dart (86%) rename lib/vaahextendflutter/{log => helpers}/local.dart (100%) rename lib/vaahextendflutter/{services/screen_util.dart => helpers/screen_helpers.dart} (100%) rename lib/vaahextendflutter/services/{rest_api => }/api.dart (98%) rename lib/vaahextendflutter/services/{rest_api/self_signed.dart => api_self_signed.dart} (100%) rename lib/{vaahextendflutter/services/rest_api => view/widgets}/demo/demo_controller.dart (66%) rename lib/{vaahextendflutter/services/rest_api => view/widgets}/demo/demo_ui.dart (90%) diff --git a/lib/controllers/base_controller.dart b/lib/controllers/base_controller.dart index 8cedd432..2d55fee0 100644 --- a/lib/controllers/base_controller.dart +++ b/lib/controllers/base_controller.dart @@ -1,8 +1,8 @@ import 'package:get/get.dart'; import '../env.dart'; -import '../vaahextendflutter/log/console.dart'; -import '../vaahextendflutter/services/rest_api/api.dart'; +import '../vaahextendflutter/helpers/console.dart'; +import '../vaahextendflutter/services/api.dart'; class BaseController extends GetxController { Future init() async { diff --git a/lib/env.dart b/lib/env.dart index fb3c11a8..3174ebb8 100644 --- a/lib/env.dart +++ b/lib/env.dart @@ -3,7 +3,7 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'vaahextendflutter/log/console.dart'; +import 'vaahextendflutter/helpers/console.dart'; // After changing any const you will need to restart the app (Hot-reload won't work). @@ -16,14 +16,14 @@ EnvironmentConfig defaultConfig = const EnvironmentConfig( version: version, build: build, baseUrl: '', - apiBaseUrl: 'http://192.168.1.12:5000', + apiBaseUrl: 'https://apivoid.herokuapp.com', timeoutLimit: 60 * 1000, // 60 seconds analyticsId: '', enableConsoleLogs: true, enableLocalLogs: true, enableApiLogs: true, showEnvAndVersionTag: true, - envAndVersionTagColor: kDangerColor, + envAndVersionTagColor: dangerColor, ); // To add new configuration add new key, value pair in envConfigs @@ -132,7 +132,7 @@ class EnvironmentConfig { } } -const MaterialColor kPrimaryColor = MaterialColor( +const MaterialColor primaryColor = MaterialColor( 0xFF3366FF, { 50: Color(0xFFD6E4FF), @@ -148,7 +148,7 @@ const MaterialColor kPrimaryColor = MaterialColor( }, ); -const MaterialColor kSuccessColor = MaterialColor( +const MaterialColor successColor = MaterialColor( 0xFF4FB52D, { 50: Color(0xFFE9FBD5), @@ -164,7 +164,7 @@ const MaterialColor kSuccessColor = MaterialColor( }, ); -const MaterialColor kInfoColor = MaterialColor( +const MaterialColor infoColor = MaterialColor( 0xFF4CA8FF, { 50: Color(0xFFDBF4FF), @@ -180,7 +180,7 @@ const MaterialColor kInfoColor = MaterialColor( }, ); -const MaterialColor kWarningColor = MaterialColor( +const MaterialColor warningColor = MaterialColor( 0xFFFFBF00, { 50: Color(0xFFFFF7CC), @@ -196,7 +196,7 @@ const MaterialColor kWarningColor = MaterialColor( }, ); -const MaterialColor kDangerColor = MaterialColor( +const MaterialColor dangerColor = MaterialColor( 0xFFFF382D, { 50: Color(0xFFFFE5D5), @@ -214,7 +214,7 @@ const MaterialColor kDangerColor = MaterialColor( -const MaterialColor kWhiteColor = MaterialColor( +const MaterialColor whiteColor = MaterialColor( 0xFFFFFFFF, { 50: Color(0xFFFFFFFF), @@ -230,7 +230,7 @@ const MaterialColor kWhiteColor = MaterialColor( }, ); -const MaterialColor kBlackColor = MaterialColor( +const MaterialColor blackColor = MaterialColor( 0xFF000000, { 50: Color(0xFFF2F2F2), diff --git a/lib/main.dart b/lib/main.dart index a28d5e9b..df87229b 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -4,8 +4,8 @@ import 'package:get/get.dart'; import 'controllers/base_controller.dart'; import 'env.dart'; import 'vaahextendflutter/base/base_stateful.dart'; -import 'vaahextendflutter/services/rest_api/demo/demo_ui.dart'; import 'vaahextendflutter/tag/tag.dart'; +import 'view/widgets/demo/demo_ui.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -22,7 +22,7 @@ class TeamApp extends StatelessWidget { return GetMaterialApp( title: 'WebReinvent Team', theme: ThemeData( - primarySwatch: kPrimaryColor, + primarySwatch: primaryColor, ), home: const TeamHomePage(), ); @@ -49,7 +49,7 @@ class _TeamHomePageState extends BaseStateful { Widget build(BuildContext context) { super.build(context); return Scaffold( - backgroundColor: kWarningColor, + backgroundColor: dangerColor, appBar: AppBar(), body: const TagWrapper( alignment: Alignment.topCenter, diff --git a/lib/vaahextendflutter/base/base_stateful.dart b/lib/vaahextendflutter/base/base_stateful.dart index b8bdb087..d50b8d74 100644 --- a/lib/vaahextendflutter/base/base_stateful.dart +++ b/lib/vaahextendflutter/base/base_stateful.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../services/screen_util.dart'; +import '../helpers/screen_helpers.dart'; abstract class BaseStateful extends State with DynamicSize { diff --git a/lib/vaahextendflutter/base/base_stateless.dart b/lib/vaahextendflutter/base/base_stateless.dart index e83c2e1d..7f56eb2a 100644 --- a/lib/vaahextendflutter/base/base_stateless.dart +++ b/lib/vaahextendflutter/base/base_stateless.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../services/screen_util.dart'; +import '../helpers/screen_helpers.dart'; abstract class BaseStateless extends StatelessWidget with DynamicSize { const BaseStateless({super.key}); diff --git a/lib/vaahextendflutter/base/base_theme.dart b/lib/vaahextendflutter/base/base_theme.dart new file mode 100644 index 00000000..e69de29b diff --git a/lib/vaahextendflutter/log/console.dart b/lib/vaahextendflutter/helpers/console.dart similarity index 98% rename from lib/vaahextendflutter/log/console.dart rename to lib/vaahextendflutter/helpers/console.dart index bb153aa4..e8cf634e 100644 --- a/lib/vaahextendflutter/log/console.dart +++ b/lib/vaahextendflutter/helpers/console.dart @@ -2,7 +2,7 @@ import 'package:colorize/colorize.dart'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import '../../env.dart'; +import '../../../env.dart'; class Console { static void printChunks(Colorize text) { diff --git a/lib/vaahextendflutter/services/date_time_extension.dart b/lib/vaahextendflutter/helpers/date_time_helpers.dart similarity index 100% rename from lib/vaahextendflutter/services/date_time_extension.dart rename to lib/vaahextendflutter/helpers/date_time_helpers.dart diff --git a/lib/vaahextendflutter/services/helpers.dart b/lib/vaahextendflutter/helpers/helpers.dart similarity index 86% rename from lib/vaahextendflutter/services/helpers.dart rename to lib/vaahextendflutter/helpers/helpers.dart index e0fb8ec4..fda6c268 100644 --- a/lib/vaahextendflutter/services/helpers.dart +++ b/lib/vaahextendflutter/helpers/helpers.dart @@ -10,12 +10,12 @@ class Helpers { } // ignore: unused_element - static _toast({required String content, Color color = kWhiteColor}) { + static _toast({required String content, Color color = whiteColor}) { Fluttertoast.showToast( msg: content, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM, - backgroundColor: color.withOpacity(0.5), + backgroundColor: color.withOpacity(0.4), textColor: color, fontSize: 16.0, ); @@ -27,7 +27,7 @@ class Helpers { List? content, String? hint, List? actions, - Color color = kWhiteColor, + Color color = whiteColor, }) { return Get.dialog( AlertDialog( @@ -53,7 +53,7 @@ class Helpers { padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( content.join('\n'), - textAlign: TextAlign.justify, + textAlign: TextAlign.center, ), ), if ((content != null && content.isNotEmpty) || (hint != null && hint.trim().isNotEmpty)) @@ -63,7 +63,7 @@ class Helpers { padding: const EdgeInsets.symmetric(horizontal: 8.0), child: Text( hint, - textAlign: TextAlign.justify, + textAlign: TextAlign.center, ), ), if (hint != null && hint.trim().isNotEmpty) @@ -79,7 +79,7 @@ class Helpers { child: Text( 'Ok', style: TextStyle( - color: color == kWhiteColor ? kBlackColor : kWhiteColor, + color: color == whiteColor ? blackColor : whiteColor, ), ), onPressed: () { @@ -96,11 +96,11 @@ class Helpers { } static showErrorToast({required String content}) { - _toast(content: content, color: kDangerColor); + _toast(content: content, color: dangerColor); } static showSuccessToast({required String content}) { - _toast(content: content, color: kSuccessColor); + _toast(content: content, color: successColor); } static showErrorDialog({ @@ -114,7 +114,7 @@ class Helpers { content: content, hint: hint, actions: actions, - color: kDangerColor, + color: dangerColor, ); } @@ -129,7 +129,7 @@ class Helpers { content: content, hint: hint, actions: actions, - color: kSuccessColor, + color: successColor, ); } } diff --git a/lib/vaahextendflutter/log/local.dart b/lib/vaahextendflutter/helpers/local.dart similarity index 100% rename from lib/vaahextendflutter/log/local.dart rename to lib/vaahextendflutter/helpers/local.dart diff --git a/lib/vaahextendflutter/services/screen_util.dart b/lib/vaahextendflutter/helpers/screen_helpers.dart similarity index 100% rename from lib/vaahextendflutter/services/screen_util.dart rename to lib/vaahextendflutter/helpers/screen_helpers.dart diff --git a/lib/vaahextendflutter/services/rest_api/api.dart b/lib/vaahextendflutter/services/api.dart similarity index 98% rename from lib/vaahextendflutter/services/rest_api/api.dart rename to lib/vaahextendflutter/services/api.dart index e9120fb9..204d5e5e 100644 --- a/lib/vaahextendflutter/services/rest_api/api.dart +++ b/lib/vaahextendflutter/services/api.dart @@ -6,8 +6,8 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart' as getx; import '../../../env.dart'; -import '../../log/console.dart'; -import '../Helpers.dart'; +import '../helpers/console.dart'; +import '../helpers/helpers.dart'; // alertType : 'dialog', 'toast', @@ -256,7 +256,7 @@ class Api { await Helpers.showErrorToast(content: 'Invalid request type!'); break; } - _showToast(content: 'Invalid request type!', color: kDangerColor); + _showToast(content: 'Invalid request type!', color: dangerColor); break; } } @@ -314,7 +314,7 @@ class Api { } else { _showToast( content: responseMessages?.join('\n') ?? 'Successful', - color: kSuccessColor, + color: successColor, ); } } @@ -357,7 +357,7 @@ class Api { } _showToast( content: 'Check your internet connection!', - color: kSuccessColor, + color: successColor, ); } } @@ -460,7 +460,7 @@ class Api { } _showToast( content: errors.isEmpty ? 'Error' : errors.join('\n'), - color: kSuccessColor, + color: successColor, ); } } @@ -474,14 +474,14 @@ class Api { static void _showToast({ required String content, - Color color = kWhiteColor, + Color color = whiteColor, }) { Fluttertoast.showToast( msg: content, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM, backgroundColor: color.withOpacity(0.5), - textColor: color == kWhiteColor ? kBlackColor : kWhiteColor, + textColor: color == whiteColor ? blackColor : whiteColor, fontSize: 16.0, ); } diff --git a/lib/vaahextendflutter/services/rest_api/self_signed.dart b/lib/vaahextendflutter/services/api_self_signed.dart similarity index 100% rename from lib/vaahextendflutter/services/rest_api/self_signed.dart rename to lib/vaahextendflutter/services/api_self_signed.dart diff --git a/lib/vaahextendflutter/tag/tag.dart b/lib/vaahextendflutter/tag/tag.dart index 44495b78..2af0ebfa 100644 --- a/lib/vaahextendflutter/tag/tag.dart +++ b/lib/vaahextendflutter/tag/tag.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../env.dart'; -import '../services/screen_util.dart'; +import '../helpers/screen_helpers.dart'; class TagWrapper extends StatelessWidget { final Widget child; @@ -34,7 +34,7 @@ class TagWrapper extends StatelessWidget { child: Container( margin: margin, child: tagWidget( - color: kDangerColor, + color: dangerColor, envType: envController!.config.envType, version: envController.config.version, build: envController.config.build, @@ -57,7 +57,7 @@ Widget tagWidget({ padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), decoration: BoxDecoration( border: Border.all( - color: kDangerColor, + color: dangerColor, width: 2, ), borderRadius: BorderRadius.circular(20), diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart b/lib/view/widgets/demo/demo_controller.dart similarity index 66% rename from lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart rename to lib/view/widgets/demo/demo_controller.dart index 0de015d7..b066ebaf 100644 --- a/lib/vaahextendflutter/services/rest_api/demo/demo_controller.dart +++ b/lib/view/widgets/demo/demo_controller.dart @@ -2,16 +2,15 @@ import 'dart:async'; import 'package:get/get.dart' as getx; -import '../../../log/console.dart'; -import '../api.dart'; +import '../../../vaahextendflutter/helpers/console.dart'; +import '../../../vaahextendflutter/services/api.dart'; class DemoController extends getx.GetxController { Future getDemoURL() async { await Api.ajax( - url: '/search', - query: {'code': 401}, + url: '/api/data/', callback: getDemoURLAfter, - alertType: 'dialog' + // alertType: 'dialog' ); } diff --git a/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart b/lib/view/widgets/demo/demo_ui.dart similarity index 90% rename from lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart rename to lib/view/widgets/demo/demo_ui.dart index a1ba46bb..10ec4034 100644 --- a/lib/vaahextendflutter/services/rest_api/demo/demo_ui.dart +++ b/lib/view/widgets/demo/demo_ui.dart @@ -1,8 +1,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import '../../../../env.dart'; -import '../../../base/base_stateful.dart'; +import '../../../env.dart'; +import '../../../vaahextendflutter/base/base_stateful.dart'; import 'demo_controller.dart'; class DemoUI extends StatefulWidget { @@ -50,7 +50,7 @@ class _DemoUIState extends BaseStateful { height: 20, width: 20, child: CircularProgressIndicator( - color: kWhiteColor, + color: whiteColor, ), ), ) From 4e0ea5efa6463a68c6e5742b3b9bd3505d13bf6d Mon Sep 17 00:00:00 2001 From: Prajapati Chintan Date: Fri, 21 Oct 2022 21:56:14 +0530 Subject: [PATCH 12/15] updated: toast and toast error message, theme and helpers --- .gitignore | 3 + README.md | 15 +- lib/app/app.dart | 34 ++++ lib/controllers/base_controller.dart | 6 +- lib/env.dart | 157 +++--------------- lib/main.dart | 82 +-------- lib/routes/routes.dart | 10 ++ lib/theme.dart | 46 +++++ lib/vaahextendflutter/base/base_stateful.dart | 2 +- .../base/base_stateless.dart | 2 +- lib/vaahextendflutter/base/base_theme.dart | 130 +++++++++++++++ .../{console.dart => console_log_helper.dart} | 0 .../{constants.dart => constant_helpers.dart} | 7 +- ...ime_helpers.dart => date_time_helper.dart} | 0 lib/vaahextendflutter/helpers/helpers.dart | 38 ++--- .../{local.dart => local_log_helper.dart} | 0 ...screen_helpers.dart => screen_helper.dart} | 11 +- .../{styles.dart => styles_helper.dart} | 3 +- lib/vaahextendflutter/helpers/theme.dart | 1 - lib/vaahextendflutter/services/api.dart | 29 ++-- lib/vaahextendflutter/tag/tag.dart | 84 ---------- lib/vaahextendflutter/tag/tag_panel.dart | 12 +- lib/view/pages/home/home.dart | 37 +++++ lib/view/widgets/demo/demo_controller.dart | 20 --- lib/view/widgets/demo/demo_ui.dart | 61 ------- test/widget_test.dart | 2 +- 26 files changed, 345 insertions(+), 447 deletions(-) create mode 100644 lib/app/app.dart create mode 100644 lib/theme.dart rename lib/vaahextendflutter/helpers/{console.dart => console_log_helper.dart} (100%) rename lib/vaahextendflutter/helpers/{constants.dart => constant_helpers.dart} (96%) rename lib/vaahextendflutter/helpers/{date_time_helpers.dart => date_time_helper.dart} (100%) rename lib/vaahextendflutter/helpers/{local.dart => local_log_helper.dart} (100%) rename lib/vaahextendflutter/helpers/{screen_helpers.dart => screen_helper.dart} (85%) rename lib/vaahextendflutter/helpers/{styles.dart => styles_helper.dart} (99%) delete mode 100644 lib/vaahextendflutter/helpers/theme.dart delete mode 100644 lib/vaahextendflutter/tag/tag.dart create mode 100644 lib/view/pages/home/home.dart delete mode 100644 lib/view/widgets/demo/demo_controller.dart delete mode 100644 lib/view/widgets/demo/demo_ui.dart diff --git a/.gitignore b/.gitignore index 24476c5d..855935bd 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,9 @@ migrate_working_dir/ *.iws .idea/ +# VS code +.vscode/ + # The .vscode folder contains launch configuration and tasks you configure in # VS Code which you may wish to be included in version control, so this line # is commented out by default. diff --git a/README.md b/README.md index 9307015a..4c9461d1 100644 --- a/README.md +++ b/README.md @@ -69,6 +69,9 @@ flutter pub outdated
    ## Project structure and coding conventions: + +### [Official dart conventions](https://dart.dev/guides/language/effective-dart/style) + - 2 spaces for indentation - test files have `_test.ext` suffix in the file name > example `widget_test.dart` - Libraries, packages, directories, and source files name convention: snake_case(lowercase_with_underscores). @@ -88,7 +91,7 @@ Android Production iOS Production - universal package: `com.webreinvent.team` -## VaahExtendedFlutter +## VaahExtendFlutter ### → Central log library: @@ -108,7 +111,7 @@ NOTE: `Remember showEnvAndVersionTag for production should always be false in En #### NOTE: You have to write below code in MaterialApp, and that will show tag panel on each screen. You don't have to wrap any other screen/ widget, or you don't have to extend any screen/ any widget with TagPanelHost. -In file cotaining material app paste this code after imports +In file containing material app paste this code after imports ```dart final _navigatorKey = GlobalKey(); ``` @@ -121,9 +124,9 @@ builder: (BuildContext context, Widget? child) { ); }, ``` -This panel uses EnvController, thus dependens on env.dart file. +This panel uses EnvController, thus depends on env.dart file. -### → Dynamic fontsize, dynamic width, dynamic height depending on device size +### → Dynamic font size, dynamic width, dynamic height depending on device size To use it directly by importing `screen_util.dart` check Usage: comment in `screen_util.dart` file. @@ -156,9 +159,9 @@ SizedBox( ### → Base widgets `vaahextendflutter/base` folder contains all the base classes/ widgets. -BaseStateless and BaseStateful are used when dev wants to init/ add dependencies in many screens and don't want to write same logic in every file, so they write the logic in base files only. eg. internet connectivity checker, dynamic size dependency, etc. +BaseStateless and BaseStateful are used when dev wants to init/ add dependencies in many screens and don't want to write same logic in every file, so they write the logic in base files only. e.g. internet connectivity checker, dynamic size dependency, etc. -so base class implements those logics and other classes can extend the base classes. +So base class implements those logics and other classes can extend the base classes. ### → Helpers Most common constants and styles used in whole app. diff --git a/lib/app/app.dart b/lib/app/app.dart new file mode 100644 index 00000000..c4e1d4d2 --- /dev/null +++ b/lib/app/app.dart @@ -0,0 +1,34 @@ + +import 'package:flutter/material.dart'; +import 'package:get/get.dart'; + +import '../routes/routes.dart'; +import '../vaahextendflutter/tag/tag_panel.dart'; +import '../view/pages/home/home.dart'; +import '../theme.dart'; + +final _navigatorKey = GlobalKey(); + +class TeamApp extends StatelessWidget { + const TeamApp({super.key}); + + @override + Widget build(BuildContext context) { + return GetMaterialApp( + title: 'WebReinvent Team', + theme: ThemeData( + primarySwatch: AppTheme.primaryColor, + ), + onGenerateInitialRoutes: (String initialRoute) { + return [TeamHomePage.route()]; + }, + onGenerateRoute: onGenerateRoute, + builder: (BuildContext context, Widget? child) { + return TagPanelHost( + navigatorKey: _navigatorKey, + child: child!, + ); + }, + ); + } +} diff --git a/lib/controllers/base_controller.dart b/lib/controllers/base_controller.dart index 2d55fee0..04904f64 100644 --- a/lib/controllers/base_controller.dart +++ b/lib/controllers/base_controller.dart @@ -1,7 +1,7 @@ import 'package:get/get.dart'; import '../env.dart'; -import '../vaahextendflutter/helpers/console.dart'; +import '../vaahextendflutter/helpers/console_log_helper.dart'; import '../vaahextendflutter/services/api.dart'; class BaseController extends GetxController { @@ -13,9 +13,9 @@ class BaseController extends GetxController { environment, ), ); - Console.info('>>>>> ${envController.config.envType}'); + Console.info('Env Type: ${envController.config.envType}'); Console.info( - '>>>>> ${envController.config.version}+${envController.config.build}', + 'Version: ${envController.config.version}+${envController.config.build}', ); await Api.initApi(); } diff --git a/lib/env.dart b/lib/env.dart index 50e4cf3c..448a1a5d 100644 --- a/lib/env.dart +++ b/lib/env.dart @@ -3,7 +3,8 @@ import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'vaahextendflutter/helpers/console.dart'; +import 'theme.dart'; +import 'vaahextendflutter/helpers/console_log_helper.dart'; // After changing any const you will need to restart the app (Hot-reload won't work). @@ -11,19 +12,19 @@ import 'vaahextendflutter/helpers/console.dart'; const String version = '1.0.0'; // version format 1.0.0 (major.minor.patch) const String build = '2022100901'; // build no format 'YYYYMMDDNUMBER' -EnvironmentConfig defaultConfig = const EnvironmentConfig( +EnvironmentConfig defaultConfig = EnvironmentConfig( envType: 'default', version: version, build: build, - baseUrl: '', - apiBaseUrl: 'https://apivoid.herokuapp.com', + backendUrl: '', + apiUrl: 'https://apivoid.herokuapp.com', timeoutLimit: 60 * 1000, // 60 seconds - analyticsId: '', + firebaseId: '', enableConsoleLogs: true, enableLocalLogs: true, enableApiLogs: true, showEnvAndVersionTag: true, - envAndVersionTagColor: dangerColor, + envAndVersionTagColor: AppTheme.blackColor.withOpacity(0.7), ); // To add new configuration add new key, value pair in envConfigs @@ -75,9 +76,10 @@ class EnvironmentConfig { final String envType; final String version; final String build; - final String baseUrl; - final String apiBaseUrl; - final String analyticsId; + final String backendUrl; + final String apiUrl; + final String firebaseId; + final int timeoutLimit; final bool enableConsoleLogs; final bool enableLocalLogs; final bool enableApiLogs; @@ -88,9 +90,10 @@ class EnvironmentConfig { required this.envType, required this.version, required this.build, - required this.baseUrl, - required this.apiBaseUrl, - required this.analyticsId, + required this.backendUrl, + required this.apiUrl, + required this.firebaseId, + required this.timeoutLimit, required this.enableConsoleLogs, required this.enableLocalLogs, required this.enableApiLogs, @@ -102,9 +105,10 @@ class EnvironmentConfig { String? envType, String? version, String? build, - String? baseUrl, - String? apiBaseUrl, - String? analyticsId, + String? backendUrl, + String? apiUrl, + String? firebaseId, + int? timeoutLimit, bool? enableConsoleLogs, bool? enableLocalLogs, bool? enableApiLogs, @@ -115,9 +119,10 @@ class EnvironmentConfig { envType: envType ?? this.envType, version: version ?? this.version, build: build ?? this.build, - baseUrl: baseUrl ?? this.baseUrl, - apiBaseUrl: apiBaseUrl ?? this.apiBaseUrl, - analyticsId: analyticsId ?? this.analyticsId, + backendUrl: backendUrl ?? this.backendUrl, + apiUrl: apiUrl ?? this.apiUrl, + firebaseId: firebaseId ?? this.firebaseId, + timeoutLimit: timeoutLimit ?? this.timeoutLimit, enableConsoleLogs: enableConsoleLogs ?? this.enableConsoleLogs, enableLocalLogs: enableLocalLogs ?? this.enableLocalLogs, enableApiLogs: enableApiLogs ?? this.enableApiLogs, @@ -126,118 +131,4 @@ class EnvironmentConfig { envAndVersionTagColor ?? this.envAndVersionTagColor, ); } -} - -const MaterialColor primaryColor = MaterialColor( - 0xFF3366FF, - { - 50: Color(0xFFD6E4FF), - 100: Color(0xFFD6E4FF), - 200: Color(0xFFADC8FF), - 300: Color(0xFF84A9FF), - 400: Color(0xFF6690FF), - 500: Color(0xFF3366FF), - 600: Color(0xFF254EDB), - 700: Color(0xFF1939B7), - 800: Color(0xFF102693), - 900: Color(0xFF091A7A), - }, -); - -const MaterialColor successColor = MaterialColor( - 0xFF4FB52D, - { - 50: Color(0xFFE9FBD5), - 100: Color(0xFFE9FBD5), - 200: Color(0xFFCFF7AD), - 300: Color(0xFFA8E87F), - 400: Color(0xFF81D25B), - 500: Color(0xFF4FB52D), - 600: Color(0xFF369B20), - 700: Color(0xFF228216), - 800: Color(0xFF11680E), - 900: Color(0xFF08560B), - }, -); - -const MaterialColor infoColor = MaterialColor( - 0xFF4CA8FF, - { - 50: Color(0xFFDBF4FF), - 100: Color(0xFFDBF4FF), - 200: Color(0xFFB7E7FF), - 300: Color(0xFF93D5FF), - 400: Color(0xFF78C4FF), - 500: Color(0xFF4CA8FF), - 600: Color(0xFF3783DB), - 700: Color(0xFF2662B7), - 800: Color(0xFF184493), - 900: Color(0xFF0E2F7A), - }, -); - -const MaterialColor warningColor = MaterialColor( - 0xFFFFBF00, - { - 50: Color(0xFFFFF7CC), - 100: Color(0xFFFFF7CC), - 200: Color(0xFFFFED99), - 300: Color(0xFFFFE066), - 400: Color(0xFFFFD33F), - 500: Color(0xFFFFBF00), - 600: Color(0xFFDB9E00), - 700: Color(0xFFB77F00), - 800: Color(0xFF936300), - 900: Color(0xFF7A4E00), - }, -); - -const MaterialColor dangerColor = MaterialColor( - 0xFFFF382D, - { - 50: Color(0xFFFFE5D5), - 100: Color(0xFFFFE5D5), - 200: Color(0xFFFFC4AB), - 300: Color(0xFFFF9C81), - 400: Color(0xFFFF7661), - 500: Color(0xFFFF382D), - 600: Color(0xFFDB2026), - 700: Color(0xFFB71629), - 800: Color(0xFF930E28), - 900: Color(0xFF7A0828), - }, -); - - - -const MaterialColor whiteColor = MaterialColor( - 0xFFFFFFFF, - { - 50: Color(0xFFFFFFFF), - 100: Color(0xFFFFFFFF), - 200: Color(0xFFFFFFFF), - 300: Color(0xFFFFFFFF), - 400: Color(0xFFFFFFFF), - 500: Color(0xFFFFFFFF), - 600: Color(0xFFFFFFFF), - 700: Color(0xFFFFFFFF), - 800: Color(0xFFFFFFFF), - 900: Color(0xFFFFFFFF), - }, -); - -const MaterialColor blackColor = MaterialColor( - 0xFF000000, - { - 50: Color(0xFFF2F2F2), - 100: Color(0xFFF2F2F2), - 200: Color(0xFFE5E5E5), - 300: Color(0xFFB2B2B2), - 400: Color(0xFF666666), - 500: Color(0xFF000000), - 600: Color(0xFF000000), - 700: Color(0xFF000000), - 800: Color(0xFF000000), - 900: Color(0xFF000000), - }, -); +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index 3b7d7897..5722719c 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,15 +1,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'app/app.dart'; import 'controllers/base_controller.dart'; -import 'env.dart'; -import 'vaahextendflutter/base/base_stateful.dart'; -import 'vaahextendflutter/tag/tag.dart'; -import 'view/widgets/demo/demo_ui.dart'; -import 'vaahextendflutter/base/base_stateful.dart'; -import 'vaahextendflutter/helpers/constants.dart'; -import 'vaahextendflutter/log/console.dart'; -import 'vaahextendflutter/tag/tag_panel.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); @@ -17,76 +10,3 @@ void main() async { await baseController.init(); runApp(const TeamApp()); } - -final _navigatorKey = GlobalKey(); - -class TeamApp extends StatelessWidget { - const TeamApp({super.key}); - - @override - Widget build(BuildContext context) { - return GetMaterialApp( - title: 'WebReinvent Team', - theme: ThemeData( - primarySwatch: Colors.blue, - ), - onGenerateInitialRoutes: (String initialRoute) { - return [TeamHomePage.route()]; - }, - onGenerateRoute: onGenerateRoute, - builder: (BuildContext context, Widget? child) { - return TagPanelHost( - navigatorKey: _navigatorKey, - child: child!, - ); - }, - ); - } -} - -class TeamHomePage extends StatefulWidget { - static Route route() { - return MaterialPageRoute( - settings: const RouteSettings(name: '/'), - builder: (_) => const TeamHomePage(), - ); - } - - const TeamHomePage({super.key}); - - @override - State createState() => _TeamHomePageState(); -} - -class _TeamHomePageState extends BaseStateful { - late EnvController envController; - - @override - void afterFirstBuild(BuildContext context) { - envController = Get.find(); - super.afterFirstBuild(context); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return Scaffold( - backgroundColor: dangerColor, - appBar: AppBar(), - body: const TagWrapper( - alignment: Alignment.topCenter, - margin: EdgeInsets.all(10), - child: Center( - child: Text('Webreinvent'), - ), - ), - ); - } -} - -Route? onGenerateRoute(RouteSettings settings) { - if (settings.name == '/') { - return TeamHomePage.route(); - } - return null; -} diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart index e69de29b..a7d3d114 100644 --- a/lib/routes/routes.dart +++ b/lib/routes/routes.dart @@ -0,0 +1,10 @@ +import 'package:flutter/material.dart'; + +import '../view/pages/home/home.dart'; + +Route? onGenerateRoute(RouteSettings settings) { + if (settings.name == '/') { + return TeamHomePage.route(); + } + return null; +} diff --git a/lib/theme.dart b/lib/theme.dart new file mode 100644 index 00000000..befccb43 --- /dev/null +++ b/lib/theme.dart @@ -0,0 +1,46 @@ +import 'package:flutter/material.dart'; + +import 'vaahextendflutter/base/base_theme.dart'; + +class AppTheme { + static const MaterialColor primaryColor = BaseTheme.primaryColor; + static const MaterialColor infoColor = BaseTheme.infoColor; + static const MaterialColor successColor = BaseTheme.successColor; + static const MaterialColor warningColor = BaseTheme.warningColor; + static const MaterialColor dangerColor = BaseTheme.dangerColor; + static const MaterialColor whiteColor = BaseTheme.whiteColor; + static const MaterialColor blackColor = BaseTheme.blackColor; +} + +// extends, implements, with + +// class AppThemeNew { +// static Map? colors; + +// static init() { +// Map tempColors = BaseThemeNew.colors; +// if (tempColors.containsKey('newPrimaryColor')) { +// tempColors.update('newPrimaryColor', (value) => newPrimaryColor); +// } else { +// Map color = {'newPrimaryColor': newPrimaryColor}; +// tempColors.addAll(color); +// } +// colors = tempColors; +// } +// } + +// const MaterialColor newPrimaryColor = MaterialColor( +// 0xFF4FB52D, +// { +// 50: Color(0xFFE9FBD5), +// 100: Color(0xFFE9FBD5), +// 200: Color(0xFFCFF7AD), +// 300: Color(0xFFA8E87F), +// 400: Color(0xFF81D25B), +// 500: Color(0xFF4FB52D), +// 600: Color(0xFF369B20), +// 700: Color(0xFF228216), +// 800: Color(0xFF11680E), +// 900: Color(0xFF08560B), +// }, +// ); diff --git a/lib/vaahextendflutter/base/base_stateful.dart b/lib/vaahextendflutter/base/base_stateful.dart index d50b8d74..7a5e219f 100644 --- a/lib/vaahextendflutter/base/base_stateful.dart +++ b/lib/vaahextendflutter/base/base_stateful.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../helpers/screen_helpers.dart'; +import '../helpers/screen_helper.dart'; abstract class BaseStateful extends State with DynamicSize { diff --git a/lib/vaahextendflutter/base/base_stateless.dart b/lib/vaahextendflutter/base/base_stateless.dart index 7f56eb2a..02df13c9 100644 --- a/lib/vaahextendflutter/base/base_stateless.dart +++ b/lib/vaahextendflutter/base/base_stateless.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../helpers/screen_helpers.dart'; +import '../helpers/screen_helper.dart'; abstract class BaseStateless extends StatelessWidget with DynamicSize { const BaseStateless({super.key}); diff --git a/lib/vaahextendflutter/base/base_theme.dart b/lib/vaahextendflutter/base/base_theme.dart index e69de29b..3a854f25 100644 --- a/lib/vaahextendflutter/base/base_theme.dart +++ b/lib/vaahextendflutter/base/base_theme.dart @@ -0,0 +1,130 @@ +import 'package:flutter/material.dart'; + +class BaseThemeNew { + static const Map colors = { + 'primaryColor': _primaryColor, + 'infoColor': _infoColor, + }; +} + +class BaseTheme { + static const MaterialColor primaryColor = _primaryColor; + static const MaterialColor infoColor = _infoColor; + static const MaterialColor successColor = _successColor; + static const MaterialColor warningColor = _warningColor; + static const MaterialColor dangerColor = _dangerColor; + static const MaterialColor whiteColor = _whiteColor; + static const MaterialColor blackColor = _blackColor; +} + +const MaterialColor _primaryColor = MaterialColor( + 0xFF3366FF, + { + 50: Color(0xFFD6E4FF), + 100: Color(0xFFD6E4FF), + 200: Color(0xFFADC8FF), + 300: Color(0xFF84A9FF), + 400: Color(0xFF6690FF), + 500: Color(0xFF3366FF), + 600: Color(0xFF254EDB), + 700: Color(0xFF1939B7), + 800: Color(0xFF102693), + 900: Color(0xFF091A7A), + }, +); + +const MaterialColor _successColor = MaterialColor( + 0xFF4FB52D, + { + 50: Color(0xFFE9FBD5), + 100: Color(0xFFE9FBD5), + 200: Color(0xFFCFF7AD), + 300: Color(0xFFA8E87F), + 400: Color(0xFF81D25B), + 500: Color(0xFF4FB52D), + 600: Color(0xFF369B20), + 700: Color(0xFF228216), + 800: Color(0xFF11680E), + 900: Color(0xFF08560B), + }, +); + +const MaterialColor _infoColor = MaterialColor( + 0xFF4CA8FF, + { + 50: Color(0xFFDBF4FF), + 100: Color(0xFFDBF4FF), + 200: Color(0xFFB7E7FF), + 300: Color(0xFF93D5FF), + 400: Color(0xFF78C4FF), + 500: Color(0xFF4CA8FF), + 600: Color(0xFF3783DB), + 700: Color(0xFF2662B7), + 800: Color(0xFF184493), + 900: Color(0xFF0E2F7A), + }, +); + +const MaterialColor _warningColor = MaterialColor( + 0xFFFFBF00, + { + 50: Color(0xFFFFF7CC), + 100: Color(0xFFFFF7CC), + 200: Color(0xFFFFED99), + 300: Color(0xFFFFE066), + 400: Color(0xFFFFD33F), + 500: Color(0xFFFFBF00), + 600: Color(0xFFDB9E00), + 700: Color(0xFFB77F00), + 800: Color(0xFF936300), + 900: Color(0xFF7A4E00), + }, +); + +const MaterialColor _dangerColor = MaterialColor( + 0xFFFF382D, + { + 50: Color(0xFFFFE5D5), + 100: Color(0xFFFFE5D5), + 200: Color(0xFFFFC4AB), + 300: Color(0xFFFF9C81), + 400: Color(0xFFFF7661), + 500: Color(0xFFFF382D), + 600: Color(0xFFDB2026), + 700: Color(0xFFB71629), + 800: Color(0xFF930E28), + 900: Color(0xFF7A0828), + }, +); + +const MaterialColor _whiteColor = MaterialColor( + 0xFFFFFFFF, + { + 50: Color(0xFFFFFFFF), + 100: Color(0xFFFFFFFF), + 200: Color(0xFFFFFFFF), + 300: Color(0xFFFFFFFF), + 400: Color(0xFFFFFFFF), + 500: Color(0xFFFFFFFF), + 600: Color(0xFFFFFFFF), + 700: Color(0xFFFFFFFF), + 800: Color(0xFFFFFFFF), + 900: Color(0xFFFFFFFF), + }, +); + +const MaterialColor _blackColor = MaterialColor( + 0xFF000000, + { + 50: Color(0xFFF2F2F2), + 100: Color(0xFFF2F2F2), + 200: Color(0xFFE5E5E5), + 300: Color(0xFFB2B2B2), + 400: Color(0xFF666666), + 500: Color(0xFF000000), + 600: Color(0xFF000000), + 700: Color(0xFF000000), + 800: Color(0xFF000000), + 900: Color(0xFF000000), + }, +); diff --git a/lib/vaahextendflutter/helpers/console.dart b/lib/vaahextendflutter/helpers/console_log_helper.dart similarity index 100% rename from lib/vaahextendflutter/helpers/console.dart rename to lib/vaahextendflutter/helpers/console_log_helper.dart diff --git a/lib/vaahextendflutter/helpers/constants.dart b/lib/vaahextendflutter/helpers/constant_helpers.dart similarity index 96% rename from lib/vaahextendflutter/helpers/constants.dart rename to lib/vaahextendflutter/helpers/constant_helpers.dart index 50edaea9..c785f268 100644 --- a/lib/vaahextendflutter/helpers/constants.dart +++ b/lib/vaahextendflutter/helpers/constant_helpers.dart @@ -1,10 +1,7 @@ import 'package:flutter/material.dart'; -const double kDeafaultPadding = 16.0; -const double kDeafaultMargin = 16.0; - -const MaterialColor kPrimaryColor = Colors.amber; -const MaterialColor kSecondaryColor = Colors.amber; +const double deafaultPadding = 16.0; +const double deafaultMargin = 16.0; // Common Widgets const emptyWidget = SizedBox(); diff --git a/lib/vaahextendflutter/helpers/date_time_helpers.dart b/lib/vaahextendflutter/helpers/date_time_helper.dart similarity index 100% rename from lib/vaahextendflutter/helpers/date_time_helpers.dart rename to lib/vaahextendflutter/helpers/date_time_helper.dart diff --git a/lib/vaahextendflutter/helpers/helpers.dart b/lib/vaahextendflutter/helpers/helpers.dart index fda6c268..7e84ce99 100644 --- a/lib/vaahextendflutter/helpers/helpers.dart +++ b/lib/vaahextendflutter/helpers/helpers.dart @@ -2,7 +2,8 @@ import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart'; -import '../../env.dart'; +import '../../theme.dart'; +import 'constant_helpers.dart'; class Helpers { static logout() { @@ -10,13 +11,13 @@ class Helpers { } // ignore: unused_element - static _toast({required String content, Color color = whiteColor}) { + static _toast({required String content}) { Fluttertoast.showToast( msg: content, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM, - backgroundColor: color.withOpacity(0.4), - textColor: color, + backgroundColor: AppTheme.whiteColor, + textColor: AppTheme.blackColor, fontSize: 16.0, ); } @@ -27,14 +28,13 @@ class Helpers { List? content, String? hint, List? actions, - Color color = whiteColor, + Color color = AppTheme.whiteColor, }) { return Get.dialog( AlertDialog( shape: const RoundedRectangleBorder( borderRadius: BorderRadius.all( - // TODO: define const - Radius.circular(16.0), + Radius.circular(deafaultPadding), ), ), contentPadding: EdgeInsets.zero, @@ -45,29 +45,29 @@ class Helpers { mainAxisSize: MainAxisSize.min, crossAxisAlignment: CrossAxisAlignment.center, children: [ - // TODO: replace with const margin if (content != null && content.isNotEmpty) - const SizedBox(height: 12), + verticalMargin12, if (content != null && content.isNotEmpty) Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: horizontalPadding8, child: Text( content.join('\n'), textAlign: TextAlign.center, ), ), - if ((content != null && content.isNotEmpty) || (hint != null && hint.trim().isNotEmpty)) - const SizedBox(height: 8), + if ((content != null && content.isNotEmpty) || + (hint != null && hint.trim().isNotEmpty)) + verticalMargin8, if (hint != null && hint.trim().isNotEmpty) Padding( - padding: const EdgeInsets.symmetric(horizontal: 8.0), + padding: horizontalPadding8, child: Text( hint, textAlign: TextAlign.center, ), ), if (hint != null && hint.trim().isNotEmpty) - const SizedBox(height: 8), + verticalMargin8 ], ), ), @@ -79,7 +79,7 @@ class Helpers { child: Text( 'Ok', style: TextStyle( - color: color == whiteColor ? blackColor : whiteColor, + color: color == AppTheme.whiteColor ? AppTheme.blackColor : AppTheme.whiteColor, ), ), onPressed: () { @@ -96,11 +96,11 @@ class Helpers { } static showErrorToast({required String content}) { - _toast(content: content, color: dangerColor); + _toast(content: content); } static showSuccessToast({required String content}) { - _toast(content: content, color: successColor); + _toast(content: content); } static showErrorDialog({ @@ -114,7 +114,7 @@ class Helpers { content: content, hint: hint, actions: actions, - color: dangerColor, + color: AppTheme.dangerColor, ); } @@ -129,7 +129,7 @@ class Helpers { content: content, hint: hint, actions: actions, - color: successColor, + color: AppTheme.successColor, ); } } diff --git a/lib/vaahextendflutter/helpers/local.dart b/lib/vaahextendflutter/helpers/local_log_helper.dart similarity index 100% rename from lib/vaahextendflutter/helpers/local.dart rename to lib/vaahextendflutter/helpers/local_log_helper.dart diff --git a/lib/vaahextendflutter/helpers/screen_helpers.dart b/lib/vaahextendflutter/helpers/screen_helper.dart similarity index 85% rename from lib/vaahextendflutter/helpers/screen_helpers.dart rename to lib/vaahextendflutter/helpers/screen_helper.dart index f30a2811..359adf80 100644 --- a/lib/vaahextendflutter/helpers/screen_helpers.dart +++ b/lib/vaahextendflutter/helpers/screen_helper.dart @@ -6,16 +6,7 @@ import 'package:flutter_screenutil/flutter_screenutil.dart'; // @override // Widget build(BuildContext context) { // initDynamicSize(context); -// return SizedBox( -// width: 300.wExt, // or swExt -// height: 200.hExt, // or shExt -// child: Text( -// 'demo', -// style: TextStyle( -// fontSize: 20.spExt, -// ), -// ), -// ); +// return SizedBox(); // } // } diff --git a/lib/vaahextendflutter/helpers/styles.dart b/lib/vaahextendflutter/helpers/styles_helper.dart similarity index 99% rename from lib/vaahextendflutter/helpers/styles.dart rename to lib/vaahextendflutter/helpers/styles_helper.dart index 87bced55..b6889f31 100644 --- a/lib/vaahextendflutter/helpers/styles.dart +++ b/lib/vaahextendflutter/helpers/styles_helper.dart @@ -1,5 +1,6 @@ import 'package:flutter/material.dart'; -import '../services/screen_util.dart'; + +import 'screen_helper.dart'; class TextStyles { const TextStyles._(); diff --git a/lib/vaahextendflutter/helpers/theme.dart b/lib/vaahextendflutter/helpers/theme.dart deleted file mode 100644 index 8b137891..00000000 --- a/lib/vaahextendflutter/helpers/theme.dart +++ /dev/null @@ -1 +0,0 @@ - diff --git a/lib/vaahextendflutter/services/api.dart b/lib/vaahextendflutter/services/api.dart index 204d5e5e..99b60bce 100644 --- a/lib/vaahextendflutter/services/api.dart +++ b/lib/vaahextendflutter/services/api.dart @@ -6,7 +6,9 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart' as getx; import '../../../env.dart'; -import '../helpers/console.dart'; +import '../../theme.dart'; +import '../helpers/console_log_helper.dart'; +import '../helpers/constant_helpers.dart'; import '../helpers/helpers.dart'; // alertType : 'dialog', 'toast', @@ -146,7 +148,7 @@ class Api { } // get env controller and set variable showEnvAndVersionTag _envController = getx.Get.find(); - _apiBaseUrl = _envController.config.apiBaseUrl; + _apiBaseUrl = _envController.config.apiUrl; if (_envController.config.enableApiLogs) { _dio.interceptors.add( LogInterceptor( @@ -256,7 +258,7 @@ class Api { await Helpers.showErrorToast(content: 'Invalid request type!'); break; } - _showToast(content: 'Invalid request type!', color: dangerColor); + _showToast(content: 'ERR: Invalid request type!', color: AppTheme.dangerColor); break; } } @@ -314,7 +316,7 @@ class Api { } else { _showToast( content: responseMessages?.join('\n') ?? 'Successful', - color: successColor, + color: AppTheme.successColor, ); } } @@ -351,13 +353,13 @@ class Api { // ignore: unnecessary_null_comparison if (Helpers.showErrorToast != null) { await Helpers.showErrorToast( - content: 'Check your internet connection!', + content: 'ERR: Check your internet connection!', ); return; } _showToast( - content: 'Check your internet connection!', - color: successColor, + content: 'ERR: Check your internet connection!', + color: AppTheme.successColor, ); } } @@ -454,13 +456,13 @@ class Api { // ignore: unnecessary_null_comparison if (Helpers.showErrorToast != null) { await Helpers.showErrorToast( - content: errors.isEmpty ? 'Error' : errors.join('\n'), + content: errors.isEmpty ? 'Error' : 'ERR: ${errors.join('\n')}', ); return; } _showToast( - content: errors.isEmpty ? 'Error' : errors.join('\n'), - color: successColor, + content: errors.isEmpty ? 'Error' : 'ERR: ${errors.join('\n')}', + color: AppTheme.successColor, ); } } @@ -474,14 +476,14 @@ class Api { static void _showToast({ required String content, - Color color = whiteColor, + Color color = AppTheme.whiteColor, }) { Fluttertoast.showToast( msg: content, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM, backgroundColor: color.withOpacity(0.5), - textColor: color == whiteColor ? blackColor : whiteColor, + textColor: color == AppTheme.whiteColor ? AppTheme.blackColor : AppTheme.whiteColor, fontSize: 16.0, ); } @@ -502,9 +504,8 @@ class Api { children: [ if (content != null && content.isNotEmpty) Text(content.join('\n')), - // TODO: replace with const margin if (content != null && content.isNotEmpty) - const SizedBox(height: 12), + verticalMargin12, if (hint != null && hint.trim().isNotEmpty) Text(hint), ], ), diff --git a/lib/vaahextendflutter/tag/tag.dart b/lib/vaahextendflutter/tag/tag.dart deleted file mode 100644 index 2af0ebfa..00000000 --- a/lib/vaahextendflutter/tag/tag.dart +++ /dev/null @@ -1,84 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -import '../../env.dart'; -import '../helpers/screen_helpers.dart'; - -class TagWrapper extends StatelessWidget { - final Widget child; - final EdgeInsets? margin; - final AlignmentGeometry? alignment; - - const TagWrapper({ - super.key, - required this.child, - this.margin, - this.alignment, - }); - - @override - Widget build(BuildContext context) { - bool showEnvAndVersionTag = false; - EnvController? envController; - bool envControllerExists = Get.isRegistered(); - if (envControllerExists) { - envController = Get.find(); - showEnvAndVersionTag = envController.config.showEnvAndVersionTag; - } - return showEnvAndVersionTag - ? Stack( - children: [ - child, - wrapAlignment( - alignment: alignment, - child: Container( - margin: margin, - child: tagWidget( - color: dangerColor, - envType: envController!.config.envType, - version: envController.config.version, - build: envController.config.build, - ), - ), - ), - ], - ) - : child; - } -} - -Widget tagWidget({ - required Color color, - required String envType, - required String version, - required String build, -}) { - return Container( - padding: const EdgeInsets.symmetric(vertical: 2, horizontal: 4), - decoration: BoxDecoration( - border: Border.all( - color: dangerColor, - width: 2, - ), - borderRadius: BorderRadius.circular(20), - ), - child: Text( - '$envType $version +$build', - style: TextStyle( - fontSize: 12.spExt, - color: color, - fontWeight: FontWeight.bold, - ), - ), - ); -} - -Widget wrapAlignment({AlignmentGeometry? alignment, required Widget child}) { - if (alignment != null) { - return Align( - alignment: alignment, - child: child, - ); - } - return child; -} diff --git a/lib/vaahextendflutter/tag/tag_panel.dart b/lib/vaahextendflutter/tag/tag_panel.dart index 0b01df29..af4700dd 100644 --- a/lib/vaahextendflutter/tag/tag_panel.dart +++ b/lib/vaahextendflutter/tag/tag_panel.dart @@ -11,8 +11,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../env.dart'; -import '../helpers/constants.dart'; -import '../helpers/styles.dart'; +import '../helpers/constant_helpers.dart'; +import '../helpers/styles_helper.dart'; const double constHandleWidth = 180.0; // tag handle width const double constHandleHeight = 28.0; // tag handle height @@ -160,11 +160,11 @@ class TagPanelHostState extends State padding: EdgeInsets.only( top: MediaQuery.of(context).padding.top + - kDeafaultPadding + + deafaultPadding + _handleHeight, - bottom: kDeafaultPadding, - left: kDeafaultPadding, - right: kDeafaultPadding, + bottom: deafaultPadding, + left: deafaultPadding, + right: deafaultPadding, ), children: [ SelectableText( diff --git a/lib/view/pages/home/home.dart b/lib/view/pages/home/home.dart new file mode 100644 index 00000000..b7f30432 --- /dev/null +++ b/lib/view/pages/home/home.dart @@ -0,0 +1,37 @@ + +import 'package:flutter/material.dart'; + +import '../../../vaahextendflutter/base/base_stateful.dart'; + +class TeamHomePage extends StatefulWidget { + static Route route() { + return MaterialPageRoute( + settings: const RouteSettings(name: '/'), + builder: (_) => const TeamHomePage(), + ); + } + + const TeamHomePage({super.key}); + + @override + State createState() => _TeamHomePageState(); +} + +class _TeamHomePageState extends BaseStateful { + + @override + void afterFirstBuild(BuildContext context) { + super.afterFirstBuild(context); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + appBar: AppBar(), + body: const Center( + child: Text('Team App Home Page'), + ), + ); + } +} diff --git a/lib/view/widgets/demo/demo_controller.dart b/lib/view/widgets/demo/demo_controller.dart deleted file mode 100644 index b066ebaf..00000000 --- a/lib/view/widgets/demo/demo_controller.dart +++ /dev/null @@ -1,20 +0,0 @@ -import 'dart:async'; - -import 'package:get/get.dart' as getx; - -import '../../../vaahextendflutter/helpers/console.dart'; -import '../../../vaahextendflutter/services/api.dart'; - -class DemoController extends getx.GetxController { - Future getDemoURL() async { - await Api.ajax( - url: '/api/data/', - callback: getDemoURLAfter, - // alertType: 'dialog' - ); - } - - Future getDemoURLAfter(dynamic data, dynamic resp) async { - Console.info('>>> callback <<< data: $data'); - } -} diff --git a/lib/view/widgets/demo/demo_ui.dart b/lib/view/widgets/demo/demo_ui.dart deleted file mode 100644 index 10ec4034..00000000 --- a/lib/view/widgets/demo/demo_ui.dart +++ /dev/null @@ -1,61 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:get/get.dart'; - -import '../../../env.dart'; -import '../../../vaahextendflutter/base/base_stateful.dart'; -import 'demo_controller.dart'; - -class DemoUI extends StatefulWidget { - const DemoUI({super.key}); - - @override - State createState() => _DemoUIState(); -} - -class _DemoUIState extends BaseStateful { - bool _isLoading = false; - DemoController? _controller; - - @override - void afterFirstBuild(BuildContext context) async { - super.afterFirstBuild(context); - Get.put(DemoController()); - _controller = Get.find(); - await load(); - } - - Future load({ - bool showLoading = true, - }) async { - setState(() { - _isLoading = true; - }); - await _controller!.getDemoURL(); - setState(() { - _isLoading = false; - }); - } - - @override - Widget build(BuildContext context) { - super.build(context); - return SizedBox( - height: 40, - width: 150, - child: ElevatedButton( - onPressed: () => load(), - child: _isLoading - ? const Center( - child: SizedBox( - height: 20, - width: 20, - child: CircularProgressIndicator( - color: whiteColor, - ), - ), - ) - : const Text('Request'), - ), - ); - } -} diff --git a/test/widget_test.dart b/test/widget_test.dart index 140f44a3..2753e01d 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,6 +1,6 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:team/main.dart'; +import 'package:team/app/app.dart'; void main() { testWidgets('test', (WidgetTester tester) async { From 2dea6f00094ece77a900efb4fbedbbef0ca19b0c Mon Sep 17 00:00:00 2001 From: Prajapati Chintan Date: Tue, 25 Oct 2022 21:20:04 +0530 Subject: [PATCH 13/15] fixed: dynamic assignment of app theme colors. --- lib/app/app.dart | 4 +- lib/controllers/base_controller.dart | 2 + lib/env.dart | 4 +- lib/theme.dart | 60 ++++++++-------------- lib/vaahextendflutter/base/base_theme.dart | 35 ++++++------- lib/vaahextendflutter/helpers/helpers.dart | 12 ++--- lib/vaahextendflutter/services/api.dart | 13 ++--- 7 files changed, 55 insertions(+), 75 deletions(-) diff --git a/lib/app/app.dart b/lib/app/app.dart index c4e1d4d2..21c90b13 100644 --- a/lib/app/app.dart +++ b/lib/app/app.dart @@ -3,9 +3,9 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../routes/routes.dart'; +import '../theme.dart'; import '../vaahextendflutter/tag/tag_panel.dart'; import '../view/pages/home/home.dart'; -import '../theme.dart'; final _navigatorKey = GlobalKey(); @@ -17,7 +17,7 @@ class TeamApp extends StatelessWidget { return GetMaterialApp( title: 'WebReinvent Team', theme: ThemeData( - primarySwatch: AppTheme.primaryColor, + primarySwatch: AppTheme.colors['primary'], ), onGenerateInitialRoutes: (String initialRoute) { return [TeamHomePage.route()]; diff --git a/lib/controllers/base_controller.dart b/lib/controllers/base_controller.dart index 04904f64..9009c2ae 100644 --- a/lib/controllers/base_controller.dart +++ b/lib/controllers/base_controller.dart @@ -1,6 +1,7 @@ import 'package:get/get.dart'; import '../env.dart'; +import '../theme.dart'; import '../vaahextendflutter/helpers/console_log_helper.dart'; import '../vaahextendflutter/services/api.dart'; @@ -18,5 +19,6 @@ class BaseController extends GetxController { 'Version: ${envController.config.version}+${envController.config.build}', ); await Api.initApi(); + AppTheme.init(); } } diff --git a/lib/env.dart b/lib/env.dart index 448a1a5d..d4f5a423 100644 --- a/lib/env.dart +++ b/lib/env.dart @@ -12,7 +12,7 @@ import 'vaahextendflutter/helpers/console_log_helper.dart'; const String version = '1.0.0'; // version format 1.0.0 (major.minor.patch) const String build = '2022100901'; // build no format 'YYYYMMDDNUMBER' -EnvironmentConfig defaultConfig = EnvironmentConfig( +final EnvironmentConfig defaultConfig = EnvironmentConfig( envType: 'default', version: version, build: build, @@ -24,7 +24,7 @@ EnvironmentConfig defaultConfig = EnvironmentConfig( enableLocalLogs: true, enableApiLogs: true, showEnvAndVersionTag: true, - envAndVersionTagColor: AppTheme.blackColor.withOpacity(0.7), + envAndVersionTagColor: AppTheme.colors['black']!.withOpacity(0.7), ); // To add new configuration add new key, value pair in envConfigs diff --git a/lib/theme.dart b/lib/theme.dart index befccb43..bfcb8e2c 100644 --- a/lib/theme.dart +++ b/lib/theme.dart @@ -3,44 +3,26 @@ import 'package:flutter/material.dart'; import 'vaahextendflutter/base/base_theme.dart'; class AppTheme { - static const MaterialColor primaryColor = BaseTheme.primaryColor; - static const MaterialColor infoColor = BaseTheme.infoColor; - static const MaterialColor successColor = BaseTheme.successColor; - static const MaterialColor warningColor = BaseTheme.warningColor; - static const MaterialColor dangerColor = BaseTheme.dangerColor; - static const MaterialColor whiteColor = BaseTheme.whiteColor; - static const MaterialColor blackColor = BaseTheme.blackColor; -} - -// extends, implements, with + static final Map colors = Map.of(BaseTheme.colors); -// class AppThemeNew { -// static Map? colors; - -// static init() { -// Map tempColors = BaseThemeNew.colors; -// if (tempColors.containsKey('newPrimaryColor')) { -// tempColors.update('newPrimaryColor', (value) => newPrimaryColor); -// } else { -// Map color = {'newPrimaryColor': newPrimaryColor}; -// tempColors.addAll(color); -// } -// colors = tempColors; -// } -// } + static void init() { + colors['primary'] = newPrimaryColor; + colors['secondary'] = newPrimaryColor; + } +} -// const MaterialColor newPrimaryColor = MaterialColor( -// 0xFF4FB52D, -// { -// 50: Color(0xFFE9FBD5), -// 100: Color(0xFFE9FBD5), -// 200: Color(0xFFCFF7AD), -// 300: Color(0xFFA8E87F), -// 400: Color(0xFF81D25B), -// 500: Color(0xFF4FB52D), -// 600: Color(0xFF369B20), -// 700: Color(0xFF228216), -// 800: Color(0xFF11680E), -// 900: Color(0xFF08560B), -// }, -// ); +const MaterialColor newPrimaryColor = MaterialColor( + 0xFFFF1F6A, + { + 50: Color(0xFFFFD4D2), + 100: Color(0xFFFFD4D2), + 200: Color(0xFFFFA5A8), + 300: Color(0xFFFF788B), + 400: Color(0xFFFF577E), + 500: Color(0xFFFF1F6A), + 600: Color(0xFFDB166B), + 700: Color(0xFFB70F68), + 800: Color(0xFF930960), + 900: Color(0xFF7A055A), + }, +); diff --git a/lib/vaahextendflutter/base/base_theme.dart b/lib/vaahextendflutter/base/base_theme.dart index 3a854f25..6c85de60 100644 --- a/lib/vaahextendflutter/base/base_theme.dart +++ b/lib/vaahextendflutter/base/base_theme.dart @@ -1,23 +1,18 @@ import 'package:flutter/material.dart'; -class BaseThemeNew { +class BaseTheme { static const Map colors = { - 'primaryColor': _primaryColor, - 'infoColor': _infoColor, + 'primary': _primary, + 'info': _info, + 'success': _success, + 'warning': _warning, + 'danger': _danger, + 'white': _white, + 'black': _black, }; } -class BaseTheme { - static const MaterialColor primaryColor = _primaryColor; - static const MaterialColor infoColor = _infoColor; - static const MaterialColor successColor = _successColor; - static const MaterialColor warningColor = _warningColor; - static const MaterialColor dangerColor = _dangerColor; - static const MaterialColor whiteColor = _whiteColor; - static const MaterialColor blackColor = _blackColor; -} - -const MaterialColor _primaryColor = MaterialColor( +const MaterialColor _primary = MaterialColor( 0xFF3366FF, { 50: Color(0xFFD6E4FF), @@ -33,7 +28,7 @@ const MaterialColor _primaryColor = MaterialColor( }, ); -const MaterialColor _successColor = MaterialColor( +const MaterialColor _success = MaterialColor( 0xFF4FB52D, { 50: Color(0xFFE9FBD5), @@ -49,7 +44,7 @@ const MaterialColor _successColor = MaterialColor( }, ); -const MaterialColor _infoColor = MaterialColor( +const MaterialColor _info = MaterialColor( 0xFF4CA8FF, { 50: Color(0xFFDBF4FF), @@ -65,7 +60,7 @@ const MaterialColor _infoColor = MaterialColor( }, ); -const MaterialColor _warningColor = MaterialColor( +const MaterialColor _warning = MaterialColor( 0xFFFFBF00, { 50: Color(0xFFFFF7CC), @@ -81,7 +76,7 @@ const MaterialColor _warningColor = MaterialColor( }, ); -const MaterialColor _dangerColor = MaterialColor( +const MaterialColor _danger = MaterialColor( 0xFFFF382D, { 50: Color(0xFFFFE5D5), @@ -97,7 +92,7 @@ const MaterialColor _dangerColor = MaterialColor( }, ); -const MaterialColor _whiteColor = MaterialColor( +const MaterialColor _white = MaterialColor( 0xFFFFFFFF, { 50: Color(0xFFFFFFFF), @@ -113,7 +108,7 @@ const MaterialColor _whiteColor = MaterialColor( }, ); -const MaterialColor _blackColor = MaterialColor( +const MaterialColor _black = MaterialColor( 0xFF000000, { 50: Color(0xFFF2F2F2), diff --git a/lib/vaahextendflutter/helpers/helpers.dart b/lib/vaahextendflutter/helpers/helpers.dart index 7e84ce99..73e26148 100644 --- a/lib/vaahextendflutter/helpers/helpers.dart +++ b/lib/vaahextendflutter/helpers/helpers.dart @@ -16,8 +16,8 @@ class Helpers { msg: content, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM, - backgroundColor: AppTheme.whiteColor, - textColor: AppTheme.blackColor, + backgroundColor: AppTheme.colors['white'], + textColor: AppTheme.colors['black'], fontSize: 16.0, ); } @@ -28,7 +28,7 @@ class Helpers { List? content, String? hint, List? actions, - Color color = AppTheme.whiteColor, + Color color = Colors.white, }) { return Get.dialog( AlertDialog( @@ -79,7 +79,7 @@ class Helpers { child: Text( 'Ok', style: TextStyle( - color: color == AppTheme.whiteColor ? AppTheme.blackColor : AppTheme.whiteColor, + color: color == AppTheme.colors['white'] ? AppTheme.colors['black'] : AppTheme.colors['white'], ), ), onPressed: () { @@ -114,7 +114,7 @@ class Helpers { content: content, hint: hint, actions: actions, - color: AppTheme.dangerColor, + color: AppTheme.colors['danger']!, ); } @@ -129,7 +129,7 @@ class Helpers { content: content, hint: hint, actions: actions, - color: AppTheme.successColor, + color: AppTheme.colors['success']!, ); } } diff --git a/lib/vaahextendflutter/services/api.dart b/lib/vaahextendflutter/services/api.dart index 99b60bce..92eb7b7d 100644 --- a/lib/vaahextendflutter/services/api.dart +++ b/lib/vaahextendflutter/services/api.dart @@ -2,6 +2,7 @@ import 'dart:async'; import 'package:dio/dio.dart'; import 'package:flutter/cupertino.dart'; +import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart' as getx; @@ -258,7 +259,7 @@ class Api { await Helpers.showErrorToast(content: 'Invalid request type!'); break; } - _showToast(content: 'ERR: Invalid request type!', color: AppTheme.dangerColor); + _showToast(content: 'ERR: Invalid request type!', color: AppTheme.colors['danger']!,); break; } } @@ -316,7 +317,7 @@ class Api { } else { _showToast( content: responseMessages?.join('\n') ?? 'Successful', - color: AppTheme.successColor, + color: AppTheme.colors['success']!, ); } } @@ -359,7 +360,7 @@ class Api { } _showToast( content: 'ERR: Check your internet connection!', - color: AppTheme.successColor, + color: AppTheme.colors['success']!, ); } } @@ -462,7 +463,7 @@ class Api { } _showToast( content: errors.isEmpty ? 'Error' : 'ERR: ${errors.join('\n')}', - color: AppTheme.successColor, + color: AppTheme.colors['success']!, ); } } @@ -476,14 +477,14 @@ class Api { static void _showToast({ required String content, - Color color = AppTheme.whiteColor, + Color color = Colors.white, }) { Fluttertoast.showToast( msg: content, toastLength: Toast.LENGTH_SHORT, gravity: ToastGravity.BOTTOM, backgroundColor: color.withOpacity(0.5), - textColor: color == AppTheme.whiteColor ? AppTheme.blackColor : AppTheme.whiteColor, + textColor: color == AppTheme.colors['white'] ? AppTheme.colors['black'] : AppTheme.colors['whiteColor'], fontSize: 16.0, ); } From 13834afd2c7d3141566d6a05f72544ab7e89dc4f Mon Sep 17 00:00:00 2001 From: Prajapati Chintan Date: Fri, 28 Oct 2022 17:21:10 +0530 Subject: [PATCH 14/15] gitflow-feature-stash: add-login-page --- lib/{app/app.dart => app_config.dart} | 17 ++++++++-------- lib/{theme.dart => app_theme.dart} | 10 +++++++--- lib/controllers/base_controller.dart | 2 +- lib/env.dart | 23 ++++++++++++++++++---- lib/main.dart | 6 +++--- lib/routes/routes.dart | 12 ++++++----- lib/vaahextendflutter/helpers/helpers.dart | 2 +- lib/vaahextendflutter/services/api.dart | 2 +- test/widget_test.dart | 6 +++--- 9 files changed, 51 insertions(+), 29 deletions(-) rename lib/{app/app.dart => app_config.dart} (65%) rename lib/{theme.dart => app_theme.dart} (74%) diff --git a/lib/app/app.dart b/lib/app_config.dart similarity index 65% rename from lib/app/app.dart rename to lib/app_config.dart index 21c90b13..8a64d21b 100644 --- a/lib/app/app.dart +++ b/lib/app_config.dart @@ -1,21 +1,22 @@ - import 'package:flutter/material.dart'; import 'package:get/get.dart'; +import 'env.dart'; -import '../routes/routes.dart'; -import '../theme.dart'; -import '../vaahextendflutter/tag/tag_panel.dart'; -import '../view/pages/home/home.dart'; +import 'routes/routes.dart'; +import 'app_theme.dart'; +import 'vaahextendflutter/tag/tag_panel.dart'; +import 'view/pages/home/home.dart'; final _navigatorKey = GlobalKey(); -class TeamApp extends StatelessWidget { - const TeamApp({super.key}); +class AppConfig extends StatelessWidget { + const AppConfig({super.key}); @override Widget build(BuildContext context) { + EnvironmentConfig env = EnvironmentConfig.getEnvConfig(); return GetMaterialApp( - title: 'WebReinvent Team', + title: env.appTitle, theme: ThemeData( primarySwatch: AppTheme.colors['primary'], ), diff --git a/lib/theme.dart b/lib/app_theme.dart similarity index 74% rename from lib/theme.dart rename to lib/app_theme.dart index bfcb8e2c..d824953f 100644 --- a/lib/theme.dart +++ b/lib/app_theme.dart @@ -6,12 +6,16 @@ class AppTheme { static final Map colors = Map.of(BaseTheme.colors); static void init() { - colors['primary'] = newPrimaryColor; - colors['secondary'] = newPrimaryColor; + // colors['primary'] = pink; + // colors['secondary'] = gray; } } -const MaterialColor newPrimaryColor = MaterialColor( + + +// To define new color developer should visit https://colors.eva.design/ + +const MaterialColor pink = MaterialColor( 0xFFFF1F6A, { 50: Color(0xFFFFD4D2), diff --git a/lib/controllers/base_controller.dart b/lib/controllers/base_controller.dart index 9009c2ae..a509e370 100644 --- a/lib/controllers/base_controller.dart +++ b/lib/controllers/base_controller.dart @@ -1,7 +1,7 @@ import 'package:get/get.dart'; import '../env.dart'; -import '../theme.dart'; +import '../app_theme.dart'; import '../vaahextendflutter/helpers/console_log_helper.dart'; import '../vaahextendflutter/services/api.dart'; diff --git a/lib/env.dart b/lib/env.dart index d4f5a423..299b7fa7 100644 --- a/lib/env.dart +++ b/lib/env.dart @@ -1,9 +1,10 @@ +// ignore_for_file: public_member_api_docs, sort_constructors_first import 'dart:io'; import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'theme.dart'; +import 'app_theme.dart'; import 'vaahextendflutter/helpers/console_log_helper.dart'; // After changing any const you will need to restart the app (Hot-reload won't work). @@ -13,6 +14,8 @@ const String version = '1.0.0'; // version format 1.0.0 (major.minor.patch) const String build = '2022100901'; // build no format 'YYYYMMDDNUMBER' final EnvironmentConfig defaultConfig = EnvironmentConfig( + appTitle: 'WebReinvent Team', + appTitleShort: 'Team', envType: 'default', version: version, build: build, @@ -73,6 +76,8 @@ class EnvController extends GetxController { } class EnvironmentConfig { + final String appTitle; + final String appTitleShort; final String envType; final String version; final String build; @@ -87,6 +92,8 @@ class EnvironmentConfig { final Color envAndVersionTagColor; const EnvironmentConfig({ + required this.appTitle, + required this.appTitleShort, required this.envType, required this.version, required this.build, @@ -101,7 +108,14 @@ class EnvironmentConfig { required this.envAndVersionTagColor, }); + static EnvironmentConfig getEnvConfig() { + EnvController envController = Get.find(); + return envController.config; + } + EnvironmentConfig copyWith({ + String? appTitle, + String? appTitleShort, String? envType, String? version, String? build, @@ -116,6 +130,8 @@ class EnvironmentConfig { Color? envAndVersionTagColor, }) { return EnvironmentConfig( + appTitle: appTitle ?? this.appTitle, + appTitleShort: appTitleShort ?? this.appTitleShort, envType: envType ?? this.envType, version: version ?? this.version, build: build ?? this.build, @@ -127,8 +143,7 @@ class EnvironmentConfig { enableLocalLogs: enableLocalLogs ?? this.enableLocalLogs, enableApiLogs: enableApiLogs ?? this.enableApiLogs, showEnvAndVersionTag: showEnvAndVersionTag ?? this.showEnvAndVersionTag, - envAndVersionTagColor: - envAndVersionTagColor ?? this.envAndVersionTagColor, + envAndVersionTagColor: envAndVersionTagColor ?? this.envAndVersionTagColor, ); } -} \ No newline at end of file +} diff --git a/lib/main.dart b/lib/main.dart index 5722719c..796a91ff 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,12 +1,12 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; -import 'app/app.dart'; +import 'app_config.dart'; import 'controllers/base_controller.dart'; void main() async { WidgetsFlutterBinding.ensureInitialized(); BaseController baseController = Get.put(BaseController()); await baseController.init(); - runApp(const TeamApp()); -} + runApp(const AppConfig()); +} \ No newline at end of file diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart index a7d3d114..4fa088b6 100644 --- a/lib/routes/routes.dart +++ b/lib/routes/routes.dart @@ -2,9 +2,11 @@ import 'package:flutter/material.dart'; import '../view/pages/home/home.dart'; -Route? onGenerateRoute(RouteSettings settings) { - if (settings.name == '/') { - return TeamHomePage.route(); - } - return null; + +Map> routes = { + '/': TeamHomePage.route(), +}; + +Route? onGenerateRoute(RouteSettings route) { + return routes[route.name]; } diff --git a/lib/vaahextendflutter/helpers/helpers.dart b/lib/vaahextendflutter/helpers/helpers.dart index 73e26148..73dabc2d 100644 --- a/lib/vaahextendflutter/helpers/helpers.dart +++ b/lib/vaahextendflutter/helpers/helpers.dart @@ -2,7 +2,7 @@ import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart'; -import '../../theme.dart'; +import '../../app_theme.dart'; import 'constant_helpers.dart'; class Helpers { diff --git a/lib/vaahextendflutter/services/api.dart b/lib/vaahextendflutter/services/api.dart index 92eb7b7d..feebbd05 100644 --- a/lib/vaahextendflutter/services/api.dart +++ b/lib/vaahextendflutter/services/api.dart @@ -7,7 +7,7 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart' as getx; import '../../../env.dart'; -import '../../theme.dart'; +import '../../app_theme.dart'; import '../helpers/console_log_helper.dart'; import '../helpers/constant_helpers.dart'; import '../helpers/helpers.dart'; diff --git a/test/widget_test.dart b/test/widget_test.dart index 2753e01d..7fdc575f 100644 --- a/test/widget_test.dart +++ b/test/widget_test.dart @@ -1,10 +1,10 @@ import 'package:flutter_test/flutter_test.dart'; -import 'package:team/app/app.dart'; +import 'package:team/app_config.dart'; void main() { testWidgets('test', (WidgetTester tester) async { - await tester.pumpWidget(const TeamApp()); - expect(find.byType(TeamApp), findsOneWidget); + await tester.pumpWidget(const AppConfig()); + expect(find.byType(AppConfig), findsOneWidget); }); } From fb9a0fe5c60f8ee9f517ad254fb8f3e1b94891c4 Mon Sep 17 00:00:00 2001 From: Prajapati Chintan Date: Fri, 28 Oct 2022 18:21:14 +0530 Subject: [PATCH 15/15] updated: file names and directory structure --- lib/app_config.dart | 2 +- lib/controllers/base_controller.dart | 2 +- lib/env.dart | 2 +- lib/routes/routes.dart | 6 ++- lib/vaahextendflutter/base/base_stateful.dart | 2 +- .../base/base_stateless.dart | 2 +- .../{console_log_helper.dart => console.dart} | 0 .../{constant_helpers.dart => constants.dart} | 0 .../{date_time_helper.dart => date_time.dart} | 0 lib/vaahextendflutter/helpers/helpers.dart | 2 +- .../{local_log_helper.dart => local_log.dart} | 0 .../{screen_helper.dart => responsive.dart} | 0 .../{styles_helper.dart => styles.dart} | 2 +- lib/vaahextendflutter/services/api.dart | 4 +- lib/vaahextendflutter/tag/tag_panel.dart | 4 +- lib/view/pages/{home => }/home.dart | 2 +- lib/view/pages/something_went_wrong.dart | 39 +++++++++++++++++++ 17 files changed, 55 insertions(+), 14 deletions(-) rename lib/vaahextendflutter/helpers/{console_log_helper.dart => console.dart} (100%) rename lib/vaahextendflutter/helpers/{constant_helpers.dart => constants.dart} (100%) rename lib/vaahextendflutter/helpers/{date_time_helper.dart => date_time.dart} (100%) rename lib/vaahextendflutter/helpers/{local_log_helper.dart => local_log.dart} (100%) rename lib/vaahextendflutter/helpers/{screen_helper.dart => responsive.dart} (100%) rename lib/vaahextendflutter/helpers/{styles_helper.dart => styles.dart} (99%) rename lib/view/pages/{home => }/home.dart (92%) create mode 100644 lib/view/pages/something_went_wrong.dart diff --git a/lib/app_config.dart b/lib/app_config.dart index 8a64d21b..74d2422b 100644 --- a/lib/app_config.dart +++ b/lib/app_config.dart @@ -5,7 +5,7 @@ import 'env.dart'; import 'routes/routes.dart'; import 'app_theme.dart'; import 'vaahextendflutter/tag/tag_panel.dart'; -import 'view/pages/home/home.dart'; +import 'view/pages/home.dart'; final _navigatorKey = GlobalKey(); diff --git a/lib/controllers/base_controller.dart b/lib/controllers/base_controller.dart index a509e370..ec3b1b93 100644 --- a/lib/controllers/base_controller.dart +++ b/lib/controllers/base_controller.dart @@ -2,7 +2,7 @@ import 'package:get/get.dart'; import '../env.dart'; import '../app_theme.dart'; -import '../vaahextendflutter/helpers/console_log_helper.dart'; +import '../vaahextendflutter/helpers/console.dart'; import '../vaahextendflutter/services/api.dart'; class BaseController extends GetxController { diff --git a/lib/env.dart b/lib/env.dart index 299b7fa7..23c0c882 100644 --- a/lib/env.dart +++ b/lib/env.dart @@ -5,7 +5,7 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import 'app_theme.dart'; -import 'vaahextendflutter/helpers/console_log_helper.dart'; +import 'vaahextendflutter/helpers/console.dart'; // After changing any const you will need to restart the app (Hot-reload won't work). diff --git a/lib/routes/routes.dart b/lib/routes/routes.dart index 4fa088b6..fd3f153e 100644 --- a/lib/routes/routes.dart +++ b/lib/routes/routes.dart @@ -1,12 +1,14 @@ import 'package:flutter/material.dart'; -import '../view/pages/home/home.dart'; +import '../view/pages/home.dart'; +import '../view/pages/something_went_wrong.dart'; Map> routes = { '/': TeamHomePage.route(), + SomethingWentWrong.routeName: SomethingWentWrong.route(), }; Route? onGenerateRoute(RouteSettings route) { - return routes[route.name]; + return routes[route.name] ?? SomethingWentWrong.route(); } diff --git a/lib/vaahextendflutter/base/base_stateful.dart b/lib/vaahextendflutter/base/base_stateful.dart index 7a5e219f..9c3724fc 100644 --- a/lib/vaahextendflutter/base/base_stateful.dart +++ b/lib/vaahextendflutter/base/base_stateful.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../helpers/screen_helper.dart'; +import '../helpers/responsive.dart'; abstract class BaseStateful extends State with DynamicSize { diff --git a/lib/vaahextendflutter/base/base_stateless.dart b/lib/vaahextendflutter/base/base_stateless.dart index 02df13c9..ac034992 100644 --- a/lib/vaahextendflutter/base/base_stateless.dart +++ b/lib/vaahextendflutter/base/base_stateless.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import '../helpers/screen_helper.dart'; +import '../helpers/responsive.dart'; abstract class BaseStateless extends StatelessWidget with DynamicSize { const BaseStateless({super.key}); diff --git a/lib/vaahextendflutter/helpers/console_log_helper.dart b/lib/vaahextendflutter/helpers/console.dart similarity index 100% rename from lib/vaahextendflutter/helpers/console_log_helper.dart rename to lib/vaahextendflutter/helpers/console.dart diff --git a/lib/vaahextendflutter/helpers/constant_helpers.dart b/lib/vaahextendflutter/helpers/constants.dart similarity index 100% rename from lib/vaahextendflutter/helpers/constant_helpers.dart rename to lib/vaahextendflutter/helpers/constants.dart diff --git a/lib/vaahextendflutter/helpers/date_time_helper.dart b/lib/vaahextendflutter/helpers/date_time.dart similarity index 100% rename from lib/vaahextendflutter/helpers/date_time_helper.dart rename to lib/vaahextendflutter/helpers/date_time.dart diff --git a/lib/vaahextendflutter/helpers/helpers.dart b/lib/vaahextendflutter/helpers/helpers.dart index 73dabc2d..eefc12f9 100644 --- a/lib/vaahextendflutter/helpers/helpers.dart +++ b/lib/vaahextendflutter/helpers/helpers.dart @@ -3,7 +3,7 @@ import 'package:fluttertoast/fluttertoast.dart'; import 'package:get/get.dart'; import '../../app_theme.dart'; -import 'constant_helpers.dart'; +import 'constants.dart'; class Helpers { static logout() { diff --git a/lib/vaahextendflutter/helpers/local_log_helper.dart b/lib/vaahextendflutter/helpers/local_log.dart similarity index 100% rename from lib/vaahextendflutter/helpers/local_log_helper.dart rename to lib/vaahextendflutter/helpers/local_log.dart diff --git a/lib/vaahextendflutter/helpers/screen_helper.dart b/lib/vaahextendflutter/helpers/responsive.dart similarity index 100% rename from lib/vaahextendflutter/helpers/screen_helper.dart rename to lib/vaahextendflutter/helpers/responsive.dart diff --git a/lib/vaahextendflutter/helpers/styles_helper.dart b/lib/vaahextendflutter/helpers/styles.dart similarity index 99% rename from lib/vaahextendflutter/helpers/styles_helper.dart rename to lib/vaahextendflutter/helpers/styles.dart index b6889f31..f2ff9855 100644 --- a/lib/vaahextendflutter/helpers/styles_helper.dart +++ b/lib/vaahextendflutter/helpers/styles.dart @@ -1,6 +1,6 @@ import 'package:flutter/material.dart'; -import 'screen_helper.dart'; +import 'responsive.dart'; class TextStyles { const TextStyles._(); diff --git a/lib/vaahextendflutter/services/api.dart b/lib/vaahextendflutter/services/api.dart index feebbd05..fce477ca 100644 --- a/lib/vaahextendflutter/services/api.dart +++ b/lib/vaahextendflutter/services/api.dart @@ -8,8 +8,8 @@ import 'package:get/get.dart' as getx; import '../../../env.dart'; import '../../app_theme.dart'; -import '../helpers/console_log_helper.dart'; -import '../helpers/constant_helpers.dart'; +import '../helpers/console.dart'; +import '../helpers/constants.dart'; import '../helpers/helpers.dart'; // alertType : 'dialog', 'toast', diff --git a/lib/vaahextendflutter/tag/tag_panel.dart b/lib/vaahextendflutter/tag/tag_panel.dart index af4700dd..a8fd794e 100644 --- a/lib/vaahextendflutter/tag/tag_panel.dart +++ b/lib/vaahextendflutter/tag/tag_panel.dart @@ -11,8 +11,8 @@ import 'package:flutter/material.dart'; import 'package:get/get.dart'; import '../../env.dart'; -import '../helpers/constant_helpers.dart'; -import '../helpers/styles_helper.dart'; +import '../helpers/constants.dart'; +import '../helpers/styles.dart'; const double constHandleWidth = 180.0; // tag handle width const double constHandleHeight = 28.0; // tag handle height diff --git a/lib/view/pages/home/home.dart b/lib/view/pages/home.dart similarity index 92% rename from lib/view/pages/home/home.dart rename to lib/view/pages/home.dart index b7f30432..9b10f85c 100644 --- a/lib/view/pages/home/home.dart +++ b/lib/view/pages/home.dart @@ -1,7 +1,7 @@ import 'package:flutter/material.dart'; -import '../../../vaahextendflutter/base/base_stateful.dart'; +import '../../vaahextendflutter/base/base_stateful.dart'; class TeamHomePage extends StatefulWidget { static Route route() { diff --git a/lib/view/pages/something_went_wrong.dart b/lib/view/pages/something_went_wrong.dart new file mode 100644 index 00000000..8d85bb25 --- /dev/null +++ b/lib/view/pages/something_went_wrong.dart @@ -0,0 +1,39 @@ + +import 'package:flutter/material.dart'; + +import '../../vaahextendflutter/base/base_stateful.dart'; + +class SomethingWentWrong extends StatefulWidget { + static String routeName = '/something-went-wrong'; + + static Route route() { + return MaterialPageRoute( + settings: const RouteSettings(name: '/something-went-wrong'), + builder: (_) => const SomethingWentWrong(), + ); + } + + const SomethingWentWrong({super.key}); + + @override + State createState() => _SomethingWentWrongState(); +} + +class _SomethingWentWrongState extends BaseStateful { + + @override + void afterFirstBuild(BuildContext context) { + super.afterFirstBuild(context); + } + + @override + Widget build(BuildContext context) { + super.build(context); + return Scaffold( + appBar: AppBar(), + body: const Center( + child: Text('Something Went Wrong'), + ), + ); + } +}