From f293cd60b369eadb018d9ac1b5312f5504c849a2 Mon Sep 17 00:00:00 2001 From: shoaib-mohmad <shoaib.mohmad@tarento.com> Date: Fri, 4 Mar 2022 17:29:57 +0530 Subject: [PATCH] WOrking on offline capability. --- lib/constants/app_constants.dart | 16 ++++ lib/database/offline_model.dart | 73 +++++++++++++++++++ lib/l10n/app_en.arb | 4 + lib/pages/application_details_page.dart | 7 +- lib/pages/home_page.dart | 37 +++++++++- lib/repositories/application_repository.dart | 23 +++++- lib/repositories/login_repository.dart | 15 ++-- lib/services/base_service.dart | 7 +- lib/util/connectivity_helper.dart | 35 +++++++++ lib/util/helper.dart | 10 +-- lib/widgets/application_card.dart | 3 +- pubspec.lock | 70 ++++++++++++++++++ pubspec.yaml | 2 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 15 files changed, 279 insertions(+), 27 deletions(-) create mode 100644 lib/database/offline_model.dart create mode 100644 lib/util/connectivity_helper.dart diff --git a/lib/constants/app_constants.dart b/lib/constants/app_constants.dart index 5c65999..b1b4aa3 100644 --- a/lib/constants/app_constants.dart +++ b/lib/constants/app_constants.dart @@ -21,6 +21,7 @@ class InspectionStatus { static const String newInspection = 'NEW'; static const String sentForInspection = 'SENTFORINS'; static const String inspectionCompleted = 'INSCOMPLETED'; + static const String inspectionPending = 'PENDING'; static const String returned = 'RETURNED'; } @@ -28,3 +29,18 @@ class FieldValue { static const String correct = 'Correct'; static const String inCorrect = 'Incorrect'; } + +class AppDatabase { + static const String name = 'smf_db'; + static const String applicationsTable = 'applications'; + static const String inspectionTable = 'inspections'; +} + +class Storage { + static const String userId = 'smf_user_id'; + static const String username = 'smf_user_username'; + static const String email = 'smf_user_email'; + static const String firstname = 'smf_user_first_name'; + static const String lastname = 'smf_user_last_name'; + static const String authtoken = 'smf_user_auth_token'; +} diff --git a/lib/database/offline_model.dart b/lib/database/offline_model.dart new file mode 100644 index 0000000..bb6d3ec --- /dev/null +++ b/lib/database/offline_model.dart @@ -0,0 +1,73 @@ +import 'package:smf_mobile/constants/app_constants.dart'; +import 'package:sqflite/sqflite.dart' as sql; +import 'package:path/path.dart' as path; +import 'package:sqflite/sqlite_api.dart'; + +class OfflineModel { + static Future<Database> database() async { + final dbPath = await sql.getDatabasesPath(); + return sql.openDatabase(path.join(dbPath, AppDatabase.name), + onCreate: (db, version) async { + const String applicationsTable = AppDatabase.applicationsTable; + const String inspectionTable = AppDatabase.inspectionTable; + + await db.execute('''CREATE TABLE $applicationsTable ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + application_data TEXT + )'''); + await db.execute('''CREATE TABLE $inspectionTable ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + user_id INTEGER NOT NULL, + inspection_data TEXT + )'''); + }, version: 1); + } + + static Future<void> saveApplications(Map<String, Object> data) async { + final db = await OfflineModel.database(); + db.insert( + AppDatabase.applicationsTable, + data, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + static Future<Map> getApplications(int userId) async { + final db = await OfflineModel.database(); + List<dynamic> whereArgs = [userId]; + List<Map> rows = await db.query(AppDatabase.applicationsTable, + where: 'user_id = ?', orderBy: 'id DESC', whereArgs: whereArgs); + return rows[0]; + } + + static Future<void> deleteApplications(int userId) async { + Database db = await OfflineModel.database(); + List<dynamic> whereArgs = [userId]; + await db.delete(AppDatabase.applicationsTable, + where: 'user_id = ?', whereArgs: whereArgs); + } + + static Future<void> saveInspection(Map<String, Object> data) async { + final db = await OfflineModel.database(); + db.insert( + AppDatabase.inspectionTable, + data, + conflictAlgorithm: ConflictAlgorithm.replace, + ); + } + + static Future<List<Map<String, dynamic>>> getInspections(int userId) async { + final db = await OfflineModel.database(); + List<dynamic> whereArgs = [userId]; + return db.query(AppDatabase.inspectionTable, + where: 'user_id = ?', whereArgs: whereArgs); + } + + static Future<void> deleteInspections(int userId) async { + Database db = await OfflineModel.database(); + List<dynamic> whereArgs = [userId]; + await db.delete(AppDatabase.inspectionTable, + where: 'user_id = ?', whereArgs: whereArgs); + } +} diff --git a/lib/l10n/app_en.arb b/lib/l10n/app_en.arb index c319c9d..785d69b 100644 --- a/lib/l10n/app_en.arb +++ b/lib/l10n/app_en.arb @@ -1,5 +1,6 @@ { "login": "Login", + "logout": "Logout", "emailId": "Email Id", "getOtp": "GET OTP", "enterOtp": "Enter OTP", @@ -14,6 +15,9 @@ "next": "Next", "previous": "previous", "inspectionCompleted": "Inspection completed", + "completed": "Completed", + "pending": "Pending", + "noApplications": "No applications at the moment", "sessionExpiredMessage": "Your session has expired.", "isGivenInformationCorrect": "Is the given information found correct?", "typeHere": "Type here", diff --git a/lib/pages/application_details_page.dart b/lib/pages/application_details_page.dart index 63f8c16..d96f39b 100644 --- a/lib/pages/application_details_page.dart +++ b/lib/pages/application_details_page.dart @@ -59,7 +59,7 @@ class _ApplicationDetailsPageState extends State<ApplicationDetailsPage> } Future<void> _checkInspectorRole() async { - String id = await Helper.getUser('smf_user_id'); + String id = await Helper.getUser(Storage.userId); int userId = int.parse(id); if (widget.application.leadInspector.isNotEmpty) { _leadInspectorId = widget.application.leadInspector[0]; @@ -67,7 +67,8 @@ class _ApplicationDetailsPageState extends State<ApplicationDetailsPage> setState(() { _isleadInspector = true; }); - } else { + } else if (widget.application.status == + InspectionStatus.inspectionCompleted) { _inspectionSummary = widget.application.inspectorSummaryDataObject['Inspection Summary'][ widget.application @@ -370,7 +371,7 @@ class _ApplicationDetailsPageState extends State<ApplicationDetailsPage> 4), ), child: Text( - 'Status: ${Helper.getInspectionStatus(widget.application.status)}', + 'Status: ${Helper.getInspectionStatus(context, widget.application.status)}', textAlign: TextAlign .center, style: GoogleFonts diff --git a/lib/pages/home_page.dart b/lib/pages/home_page.dart index 2df0c63..4636a74 100644 --- a/lib/pages/home_page.dart +++ b/lib/pages/home_page.dart @@ -11,6 +11,9 @@ import 'package:smf_mobile/repositories/login_repository.dart'; import 'package:smf_mobile/util/helper.dart'; import 'package:smf_mobile/widgets/application_card.dart'; import 'package:flutter_gen/gen_l10n/app_localizations.dart'; +import 'dart:async'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:smf_mobile/util/connectivity_helper.dart'; // import 'dart:developer' as developer; @@ -23,6 +26,8 @@ class HomePage extends StatefulWidget { } class _HomePageState extends State<HomePage> { + Map _source = {ConnectivityResult.none: false}; + final MyConnectivity _connectivity = MyConnectivity.instance; List<Application> _allApplications = []; final List<Application> _pendingApplications = []; final List<Application> _upcomingApplications = []; @@ -30,6 +35,10 @@ class _HomePageState extends State<HomePage> { @override void initState() { super.initState(); + _connectivity.initialise(); + _connectivity.myStream.listen((source) { + setState(() => _source = source); + }); } void _validateUser() async { @@ -42,11 +51,31 @@ class _HomePageState extends State<HomePage> { } } + bool _isInternetConnected() { + bool connected; + switch (_source.keys.toList()[0]) { + case ConnectivityResult.mobile: + print('connected to mobile...'); + connected = true; + break; + case ConnectivityResult.wifi: + print('connected to wifi...'); + connected = true; + break; + case ConnectivityResult.none: + default: + print('offline...'); + connected = false; + } + return connected; + } + Future<dynamic> _getApplications() async { + print('_getApplications...'); _validateUser(); _allApplications = await Provider.of<ApplicationRespository>(context, listen: false) - .getApplications(); + .getApplications(_isInternetConnected()); String _errorMessage = Provider.of<ApplicationRespository>(context, listen: false) .errorMessage; @@ -127,7 +156,7 @@ class _HomePageState extends State<HomePage> { PopupMenuItem<String>( value: 'logout', child: Text( - 'Logout', + AppLocalizations.of(context)!.logout, style: GoogleFonts.lato( color: AppColors.black87, fontSize: 14.0, @@ -168,7 +197,9 @@ class _HomePageState extends State<HomePage> { margin: const EdgeInsets.only( top: 10, bottom: 20), child: Center( - child: Text('No applications at the moment', + child: Text( + AppLocalizations.of(context)! + .noApplications, style: GoogleFonts.lato( color: AppColors.black87, fontSize: 16.0, diff --git a/lib/repositories/application_repository.dart b/lib/repositories/application_repository.dart index 3dbe036..d50600d 100644 --- a/lib/repositories/application_repository.dart +++ b/lib/repositories/application_repository.dart @@ -1,17 +1,34 @@ +import 'dart:async'; import 'dart:convert'; import 'package:flutter/widgets.dart'; +import 'package:smf_mobile/constants/app_constants.dart'; +import 'package:smf_mobile/database/offline_model.dart'; import 'package:smf_mobile/models/application_model.dart'; import 'package:smf_mobile/services/application_service.dart'; +import 'package:smf_mobile/util/helper.dart'; class ApplicationRespository with ChangeNotifier { late Map _data; List<Application> _applications = []; String _errorMessage = ''; - Future<dynamic> getApplications() async { + Future<dynamic> getApplications(bool internetConnected) async { + String rawUserId = await Helper.getUser(Storage.userId); + int userId = int.parse(rawUserId); try { - final request = await ApplicationService.getApplications(); - _data = json.decode(request.body); + if (internetConnected) { + final request = await ApplicationService.getApplications(); + _data = json.decode(request.body); + Map<String, Object> data = { + 'user_id': userId, + 'application_data': request.body + }; + await OfflineModel.deleteApplications(userId); + await OfflineModel.saveApplications(data); + } else { + var applications = await OfflineModel.getApplications(userId); + _data = json.decode(applications['application_data']); + } } catch (_) { return _; } diff --git a/lib/repositories/login_repository.dart b/lib/repositories/login_repository.dart index 6f733ad..a6fa747 100644 --- a/lib/repositories/login_repository.dart +++ b/lib/repositories/login_repository.dart @@ -1,6 +1,7 @@ import 'dart:convert'; import 'package:firebase_messaging/firebase_messaging.dart'; import 'package:flutter/widgets.dart'; +import 'package:smf_mobile/constants/app_constants.dart'; import 'package:smf_mobile/models/login_model.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:smf_mobile/services/login_service.dart'; @@ -43,14 +44,12 @@ class LoginRespository with ChangeNotifier { _errorMessage = _data['statusInfo']['errorMessage']; } else { _loginDetails = Login.fromJson(_data['responseData']); - _storage.write(key: 'smf_user_id', value: '${_loginDetails.id}'); - _storage.write(key: 'smf_user_username', value: _loginDetails.username); - _storage.write(key: 'smf_user_email', value: _loginDetails.email); - _storage.write( - key: 'smf_user_first_name', value: _loginDetails.firstName); - _storage.write(key: 'smf_user_last_name', value: _loginDetails.lastName); - _storage.write( - key: 'smf_user_auth_token', value: _loginDetails.authToken); + _storage.write(key: Storage.userId, value: '${_loginDetails.id}'); + _storage.write(key: Storage.username, value: _loginDetails.username); + _storage.write(key: Storage.email, value: _loginDetails.email); + _storage.write(key: Storage.firstname, value: _loginDetails.firstName); + _storage.write(key: Storage.lastname, value: _loginDetails.lastName); + _storage.write(key: Storage.authtoken, value: _loginDetails.authToken); _firebaseMessaging.getToken().then((token) async { final request = await LoginService.updateUserDeviceToken( token.toString(), diff --git a/lib/services/base_service.dart b/lib/services/base_service.dart index ae76f99..268d176 100644 --- a/lib/services/base_service.dart +++ b/lib/services/base_service.dart @@ -1,7 +1,6 @@ import 'dart:io'; -import 'package:flutter_secure_storage/flutter_secure_storage.dart'; - -const _storage = FlutterSecureStorage(); +import 'package:smf_mobile/constants/app_constants.dart'; +import 'package:smf_mobile/util/helper.dart'; abstract class BaseService { final HttpClient client; @@ -12,7 +11,7 @@ abstract class BaseService { 'Accept': 'application/json', 'Content-Type': 'application/json; charset=utf-8', }; - var authToken = await _storage.read(key: 'smf_user_auth_token'); + var authToken = await Helper.getUser(Storage.authtoken); if (authToken != '' && authToken != null) { headers['Authorization'] = authToken; } diff --git a/lib/util/connectivity_helper.dart b/lib/util/connectivity_helper.dart new file mode 100644 index 0000000..5704601 --- /dev/null +++ b/lib/util/connectivity_helper.dart @@ -0,0 +1,35 @@ +import 'dart:io'; +import 'dart:async'; +import 'package:connectivity_plus/connectivity_plus.dart'; +import 'package:smf_mobile/constants/api_endpoints.dart'; + +class MyConnectivity { + MyConnectivity._(); + + static final _instance = MyConnectivity._(); + static MyConnectivity get instance => _instance; + final _connectivity = Connectivity(); + final _controller = StreamController.broadcast(); + Stream get myStream => _controller.stream; + + void initialise() async { + ConnectivityResult result = await _connectivity.checkConnectivity(); + _checkStatus(result); + _connectivity.onConnectivityChanged.listen((result) { + _checkStatus(result); + }); + } + + void _checkStatus(ConnectivityResult result) async { + bool isOnline = false; + try { + final result = await InternetAddress.lookup(ApiUrl.baseUrl); + isOnline = result.isNotEmpty && result[0].rawAddress.isNotEmpty; + } on SocketException catch (_) { + isOnline = false; + } + _controller.sink.add({result: isOnline}); + } + + void disposeStream() => _controller.close(); +} diff --git a/lib/util/helper.dart b/lib/util/helper.dart index cf6800d..0faaf21 100644 --- a/lib/util/helper.dart +++ b/lib/util/helper.dart @@ -1,11 +1,11 @@ import 'dart:math'; - import 'package:flutter/material.dart'; import 'package:fluttertoast/fluttertoast.dart'; import 'package:intl/intl.dart'; import 'package:jwt_decoder/jwt_decoder.dart'; import 'package:flutter_secure_storage/flutter_secure_storage.dart'; import 'package:smf_mobile/constants/app_constants.dart'; +import 'package:flutter_gen/gen_l10n/app_localizations.dart'; const _storage = FlutterSecureStorage(); @@ -37,7 +37,7 @@ class Helper { static Future<bool> isTokenExpired() async { bool isTokenExpired = true; - var authToken = await _storage.read(key: 'smf_user_auth_token'); + var authToken = await _storage.read(key: Storage.authtoken); if (authToken != null) { isTokenExpired = JwtDecoder.isExpired(authToken); } @@ -74,12 +74,12 @@ class Helper { ? '${string[0].toUpperCase()}${string.substring(1).toLowerCase()}' : ''; - static getInspectionStatus(String status) { + static getInspectionStatus(BuildContext context, String status) { String _inspectionStatus = ''; if (status == InspectionStatus.inspectionCompleted) { - _inspectionStatus = 'Completed'; + _inspectionStatus = AppLocalizations.of(context)!.completed; } else if (status == InspectionStatus.sentForInspection) { - _inspectionStatus = 'Pending'; + _inspectionStatus = AppLocalizations.of(context)!.pending; } else { _inspectionStatus = capitalize(_inspectionStatus); } diff --git a/lib/widgets/application_card.dart b/lib/widgets/application_card.dart index 4a3a02f..c532651 100644 --- a/lib/widgets/application_card.dart +++ b/lib/widgets/application_card.dart @@ -93,7 +93,8 @@ class _ApplicationCardState extends State<ApplicationCard> { Padding( padding: const EdgeInsets.only(bottom: 10), child: Text( - Helper.getInspectionStatus(widget.application.status), + Helper.getInspectionStatus( + context, widget.application.status), style: GoogleFonts.lato( color: AppColors.black60, fontSize: 14.0, diff --git a/pubspec.lock b/pubspec.lock index bac53e3..4816bf3 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -64,6 +64,48 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.15.0" + connectivity_plus: + dependency: "direct main" + description: + name: connectivity_plus + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.1" + connectivity_plus_linux: + dependency: transitive + description: + name: connectivity_plus_linux + url: "https://pub.dartlang.org" + source: hosted + version: "1.3.0" + connectivity_plus_macos: + dependency: transitive + description: + name: connectivity_plus_macos + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.1" + connectivity_plus_platform_interface: + dependency: transitive + description: + name: connectivity_plus_platform_interface + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + connectivity_plus_web: + dependency: transitive + description: + name: connectivity_plus_web + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" + connectivity_plus_windows: + dependency: transitive + description: + name: connectivity_plus_windows + url: "https://pub.dartlang.org" + source: hosted + version: "1.2.0" crypto: dependency: transitive description: @@ -336,6 +378,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.0.0" + nm: + dependency: transitive + description: + name: nm + url: "https://pub.dartlang.org" + source: hosted + version: "0.5.0" otp_text_field: dependency: "direct main" description: @@ -446,6 +495,20 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.8.1" + sqflite: + dependency: "direct main" + description: + name: sqflite + url: "https://pub.dartlang.org" + source: hosted + version: "2.0.2" + sqflite_common: + dependency: transitive + description: + name: sqflite_common + url: "https://pub.dartlang.org" + source: hosted + version: "2.2.0" stack_trace: dependency: transitive description: @@ -467,6 +530,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.1.0" + synchronized: + dependency: transitive + description: + name: synchronized + url: "https://pub.dartlang.org" + source: hosted + version: "3.0.0" term_glyph: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index c936985..206d2b9 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -48,6 +48,8 @@ dependencies: firebase_core: ^1.12.0 flutter_local_notifications: ^9.3.2 unique_identifier: ^0.2.2 + sqflite: ^2.0.2 + connectivity_plus: ^2.2.1 dev_dependencies: flutter_test: diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 0c50753..9993ebd 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,9 +6,12 @@ #include "generated_plugin_registrant.h" +#include <connectivity_plus_windows/connectivity_plus_windows_plugin.h> #include <flutter_secure_storage_windows/flutter_secure_storage_windows_plugin.h> void RegisterPlugins(flutter::PluginRegistry* registry) { + ConnectivityPlusWindowsPluginRegisterWithRegistrar( + registry->GetRegistrarForPlugin("ConnectivityPlusWindowsPlugin")); FlutterSecureStorageWindowsPluginRegisterWithRegistrar( registry->GetRegistrarForPlugin("FlutterSecureStorageWindowsPlugin")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index 69b5ae4..7991aad 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + connectivity_plus_windows flutter_secure_storage_windows ) -- GitLab