Skip to content

Commit

Permalink
Storage and cache improvement (#26)
Browse files Browse the repository at this point in the history
* Add a new AppLocalStorage layer to manage storage.
* Remove get_storage, path_provider, fake_async and synchronized packages.
* Fix UniTests storage problem
* Fix a lot of reading storage
* Fix cached roles when first login
* Remove utils/storage.dart class
* Add account_repository_test
  • Loading branch information
cevheri authored Nov 18, 2024
1 parent 4b2e857 commit 15fa283
Show file tree
Hide file tree
Showing 34 changed files with 636 additions and 346 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build_and_test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,8 @@ jobs:
run: flutter pub get
- name: Analyze
run: flutter analyze
# - name: Run tests
# run: flutter test --concurrency=1 --test-randomize-ordering-seed=random
- name: Run tests
run: flutter test
- name: Build APK
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/sonar_scanner.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ name: SonarQube Cloud
on:
push:
branches:
- main
- sonar
pull_request:
types: [opened, synchronize, reopened]
jobs:
Expand Down
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,10 @@ Following test should run

- Run `flutter test`

Or 1 Thread

- Run `flutter test --concurrency=1 --test-randomize-ordering-seed=random`

---

## Code Quality Analysis with SonarQube
Expand Down
12 changes: 5 additions & 7 deletions assets/mock/POST_register.json
Original file line number Diff line number Diff line change
@@ -1,15 +1,13 @@
{
"id": "user-1",
"login": "admin",
"email": "admin@sekoya.tech",
"firstName": "Admin",
"id": "user-2",
"login": "user",
"email": "user@sekoya.tech",
"firstName": "User",
"lastName": "User",
"langKey": "en",
"createdBy": "system",
"createdDate": "2024-01-04T06:02:47.757Z",
"lastModifiedBy": "admin",
"lastModifiedDate": "2024-01-04T06:02:47.757Z",
"authorities": [
"ROLE_ADMIN", "ROLE_USER"
]
"authorities": ["ROLE_USER"]
}
129 changes: 129 additions & 0 deletions lib/configuration/local_storage.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';

class AppLocalStorageCached {
static late String? jwtToken;
static late List<String>? roles;
static late String? language;
static late String? username;

static Future<void> loadCache() async {
jwtToken = await AppLocalStorage().read(StorageKeys.jwtToken.name);
roles = await AppLocalStorage().read(StorageKeys.roles.name);
language = await AppLocalStorage().read(StorageKeys.language.name) ?? "en";
username = await AppLocalStorage().read(StorageKeys.username.name);
}
}

/// LocalStorage predefined keys
enum StorageKeys { jwtToken, roles, language, username }

// extension StorageKeysExtension on StorageKeys {
// String get name {
// switch (this) {
// case StorageKeys.jwtToken:
// return "TOKEN";
// case StorageKeys.roles:
// return "ROLES";
// case StorageKeys.language:
// return "LANGUAGE";
// case StorageKeys.username:
// return "USERNAME";
// default:
// return "";
// }
// }
// }

/// Application Local Storage
///
/// This class is used to store data locally with the help of shared preferences.
class AppLocalStorage {
static final AppLocalStorage _instance = AppLocalStorage._internal();

factory AppLocalStorage() {
return _instance;
}

AppLocalStorage._internal();

/// Shared Preferences private instance
Future<SharedPreferences> get _prefs async => SharedPreferences.getInstance();

/// Save data to local storage <br>
/// <br>
/// This method saves data to local storage. It takes a key and a value as parameters.<br>
/// Key is the string and value is dynamic.<br>
/// Supported values:<br>
/// - **String**
/// - **int**
/// - **double**
/// - **bool**
/// - **List String**
/// <br>
///
/// throws Exception if value type is not supported
Future<bool> save(String key, dynamic value) async {
final prefs = await _prefs;
try {
if (value is String) {
prefs.setString(key, value);
} else if (value is int) {
prefs.setInt(key, value);
} else if (value is double) {
prefs.setDouble(key, value);
} else if (value is bool) {
prefs.setBool(key, value);
} else if (value is List<String>) {
prefs.setStringList(key, value);
} else {
throw Exception("Unsupported value type");
}

await AppLocalStorageCached.loadCache();
debugPrint("Saved data to local storage: $key - $value");
return true;
} catch (e) {
debugPrint("Error saving data to local storage: $e");
return false;
}
}

/// Get data from local storage <br>
/// <br>
/// This method gets data from local storage. It takes a key as parameter. <br>
/// Supported values:<br>
/// - **String**
/// - **int**
/// - **double**
/// - **bool**
/// - **List String**
Future<dynamic> read(String key) async {
final prefs = await _prefs;
return prefs.get(key);
}

/// Remove data from local storage
///
/// This method removes data from local storage. It takes a key as parameter.
Future<bool> remove(String key) async {
try {
final prefs = await _prefs;
prefs.remove(key);
await AppLocalStorageCached.loadCache();
return true;
} catch (e) {
debugPrint("Error removing data from local storage: $e");
return false;
}
}

/// Clear all data from local storage
///
/// This method clears all data from local storage.
Future<void> clear() async {
final prefs = await _prefs;
prefs.clear();
await AppLocalStorageCached.loadCache();
}
}
10 changes: 5 additions & 5 deletions lib/configuration/routes.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import '../utils/storage.dart';
import 'package:flutter_bloc_advance/configuration/local_storage.dart';

/// Routes for the application
///
Expand All @@ -17,10 +17,10 @@ class ApplicationRoutes {
static final listUsers = '/admin/list-users';
}

String initialRouteControl(context) {
if (getStorageCache["jwtToken"] != null) {
return "/";
String initialRouteControl() {
if (AppLocalStorageCached.jwtToken != null) {
return ApplicationRoutes.home;
} else {
return '/login';
return ApplicationRoutes.login;
}
}
6 changes: 3 additions & 3 deletions lib/data/app_api_exception.dart
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
class AppException implements Exception {
abstract class AppException implements Exception {
final String? _message;
final String? _prefix;

Expand All @@ -14,11 +14,11 @@ class FetchDataException extends AppException {
FetchDataException(String message) : super(message, "Error During Communication: ");
}

class BadRequestException extends AppException {
final class BadRequestException extends AppException {
BadRequestException([message]) : super(message, "Invalid Request: ");
}

class UnauthorizedException extends AppException {
final class UnauthorizedException extends AppException {
UnauthorizedException([message]) : super(message, "Unauthorized: ");
}

Expand Down
13 changes: 6 additions & 7 deletions lib/data/http_utils.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,12 @@ import 'dart:io';
import 'package:dart_json_mapper/dart_json_mapper.dart';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_bloc_advance/configuration/allowed_paths.dart';
import 'package:flutter_bloc_advance/configuration/environment.dart';
import 'package:flutter_bloc_advance/configuration/local_storage.dart';
import 'package:flutter_bloc_advance/utils/app_constants.dart';
import 'package:http/http.dart' as http;

import '../configuration/allowed_paths.dart';
import '../configuration/environment.dart';
import '../utils/app_constants.dart';
import '../utils/storage.dart';
import 'app_api_exception.dart';

class MyHttpOverrides extends HttpOverrides {
Expand Down Expand Up @@ -51,7 +51,6 @@ class HttpUtils {
/// -H 'content-type: application/json' \
static Future<Map<String, String>> headers() async {
String? jwt = getStorageCache["jwtToken"];
Map<String, String> headerParameters = <String, String>{};

//custom http headers entries
Expand All @@ -64,8 +63,8 @@ class HttpUtils {
log("default headers : $_defaultHttpHeaders");
}

if (jwt != null && jwt != "") {
headerParameters['Authorization'] = 'Bearer $jwt';
if (AppLocalStorageCached.jwtToken != null) {
headerParameters['Authorization'] = 'Bearer ${AppLocalStorageCached.jwtToken}';
} else {
headerParameters.remove('Authorization');
}
Expand Down
57 changes: 47 additions & 10 deletions lib/data/repository/account_repository.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import 'dart:io';

import 'package:flutter/material.dart';
import 'package:flutter_bloc_advance/utils/storage.dart';
import 'package:flutter_bloc_advance/data/app_api_exception.dart';

import '../http_utils.dart';
import '../models/change_password.dart';
Expand All @@ -12,23 +12,52 @@ class AccountRepository {

final String _resource = "account";

Future<User?> register(User newUser) async {
Future<User?> register(User? newUser) async {
debugPrint("register repository start");
if (newUser == null) {
throw BadRequestException("User null");
}
if (newUser.email == null || newUser.email!.isEmpty || newUser.login == null || newUser.login!.isEmpty) {
throw BadRequestException("User email or login null");
}
if (newUser.langKey == null) {
newUser = newUser.copyWith(langKey: "en");
}
// when user is registered, it is a normal user
newUser = newUser.copyWith(authorities: ["ROLE_USER"]);
final httpResponse = await HttpUtils.postRequest<User>("/register", newUser);
var response = HttpUtils.decodeUTF8(httpResponse.body.toString());
return User.fromJsonString(response);
}

Future<int> changePassword(PasswordChangeDTO passwordChangeDTO) async {
debugPrint("changePassword repository start");
Future<int> changePassword(PasswordChangeDTO? passwordChangeDTO) async {
debugPrint("BEGIN:changePassword repository start");
if (passwordChangeDTO == null) {
throw BadRequestException("PasswordChangeDTO null");
}
if (passwordChangeDTO.currentPassword == null ||
passwordChangeDTO.currentPassword!.isEmpty ||
passwordChangeDTO.newPassword == null ||
passwordChangeDTO.newPassword!.isEmpty) {
throw BadRequestException("PasswordChangeDTO currentPassword or newPassword null");
}
final authenticateRequest = await HttpUtils.postRequest<PasswordChangeDTO>("/$_resource/change-password", passwordChangeDTO);
var result = authenticateRequest.statusCode;
debugPrint("changePassword successful - response: $result");
debugPrint("END:changePassword successful - response: $result");
return result;
}

Future<int> resetPassword(String mailAddress) async {
debugPrint("resetPassword repository start");
if (mailAddress.isEmpty) {
throw BadRequestException("Mail address null");
}

//valida mail address
if (!mailAddress.contains("@") || !mailAddress.contains(".")) {
throw BadRequestException("Mail address invalid");
}

HttpUtils.addCustomHttpHeader('Content-Type', 'text/plain');
HttpUtils.addCustomHttpHeader('Accept', '*/*');
final resetRequest = await HttpUtils.postRequest<String>("/$_resource/reset-password/init", mailAddress);
Expand All @@ -37,20 +66,25 @@ class AccountRepository {
}

Future<User> getAccount() async {
debugPrint("BEGIN: getAccount repository");
debugPrint("getAccount repository start");
final httpResponse = await HttpUtils.getRequest("/$_resource");

var response = HttpUtils.decodeUTF8(httpResponse.body.toString());
debugPrint(" GET Request Method result : $response");

var result = User.fromJsonString(response)!;
saveStorage(roles: result.authorities);
debugPrint("END: getAccount repository");
debugPrint("getAccount successful - response : $response}");
return result;
}

Future<String?> saveAccount(User user) async {
Future<User> saveAccount(User? user) async {
debugPrint("saveAccount repository start");
if (user == null) {
throw BadRequestException("User null");
}
if (user.id == null || user.id!.isEmpty) {
throw BadRequestException("User id not null");
}
final saveRequest = await HttpUtils.postRequest<User>("/$_resource", user);
String? result;
if (saveRequest.statusCode >= HttpStatus.badRequest) {
Expand All @@ -62,8 +96,11 @@ class AccountRepository {
} else {
result = HttpUtils.successResult;
}
var response = HttpUtils.decodeUTF8(saveRequest.body.toString());
var savedUser = User.fromJsonString(response)!;

debugPrint("saveAccount successful - response : $result");
return result;
return savedUser;
}

updateAccount(User account) async {
Expand Down
11 changes: 3 additions & 8 deletions lib/data/repository/login_repository.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import 'package:flutter_bloc_advance/utils/storage.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:flutter_bloc_advance/configuration/local_storage.dart';

import '../http_utils.dart';
import '../models/jwt_token.dart';
Expand Down Expand Up @@ -31,17 +30,13 @@ class LoginRepository {
result = JWTToken.fromJsonString(response.body);

if (result != null && result.idToken != null) {
saveStorage(jwtToken: result.idToken);
await AppLocalStorage().save(StorageKeys.jwtToken.name, result.idToken);
return result;
}
return JWTToken(idToken: null);
}

Future<void> logout() async {
final SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove('role');
await prefs.remove('jwtToken');
await prefs.clear();
clearStorage();
await AppLocalStorage().clear();
}
}
Loading

0 comments on commit 15fa283

Please sign in to comment.