-
Notifications
You must be signed in to change notification settings - Fork 0
feat: setup caching with shared prefs with TTL #14
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,8 +1,15 @@ | ||
| import 'dart:ui'; | ||
| import 'package:flutter/services.dart'; | ||
|
|
||
| extension ColorExtensions on String { | ||
| Color toHexColor() { | ||
| final hexColor = replaceAll('#', ''); | ||
| return Color(int.parse(hexColor, radix: 16) + 0xFF000000); | ||
| } | ||
| } | ||
|
|
||
| extension ColorToHex on Color { | ||
| String toHexString() { | ||
| return '#${toARGB32().toRadixString(16).substring(2).toUpperCase()}'; | ||
| } | ||
|
koukibadr marked this conversation as resolved.
|
||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,50 @@ | ||
| import 'dart:convert'; | ||
|
|
||
| import 'package:flutter_skin/models/project_config.dart'; | ||
| import 'package:shared_preferences/shared_preferences.dart'; | ||
|
|
||
| class CacheService { | ||
| static final CacheService _instance = CacheService._(); | ||
|
|
||
| CacheService._(); | ||
|
|
||
| factory CacheService() { | ||
| return _instance; | ||
| } | ||
|
|
||
| final sharedPreferences = SharedPreferences.getInstance(); | ||
|
|
||
| Future<void> saveApiKey(String apiKey) async { | ||
| final prefs = await sharedPreferences; | ||
| await prefs.setString('apiKey', apiKey); | ||
| } | ||
|
|
||
| Future<String?> getApiKey() async { | ||
| final prefs = await sharedPreferences; | ||
| return prefs.getString('apiKey'); | ||
| } | ||
|
|
||
| Future<void> saveProjectConfig(Map<String, dynamic> projectConfig) async { | ||
| final prefs = await sharedPreferences; | ||
| await prefs.setString('projectConfig', jsonEncode(projectConfig)); | ||
| await prefs.setString('lastUpdated', DateTime.now().toIso8601String()); | ||
| } | ||
|
|
||
| Future<ProjectConfig?> getProjectConfig() async { | ||
| final prefs = await sharedPreferences; | ||
| final projectConfigString = prefs.getString('projectConfig'); | ||
|
|
||
| if (projectConfigString != null) { | ||
| final Map<String, dynamic> projectConfig = | ||
| jsonDecode(projectConfigString) as Map<String, dynamic>; | ||
| return ProjectConfig.fromMap(projectConfig); | ||
| } | ||
| return null; | ||
| } | ||
|
|
||
| Future<DateTime?> getLastUpdated() async { | ||
| final prefs = await sharedPreferences; | ||
| final value = prefs.getString('lastUpdated'); | ||
| return value != null ? DateTime.parse(value) : null; | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -2,6 +2,7 @@ import 'dart:convert'; | |||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| import 'package:flutter_skin/constants/fskin_constants.dart'; | ||||||||||||||||||||||||
| import 'package:flutter_skin/models/project_config.dart'; | ||||||||||||||||||||||||
| import 'package:flutter_skin/services/cache_service.dart'; | ||||||||||||||||||||||||
| import 'package:flutter_skin/services/fskin_logger.dart'; | ||||||||||||||||||||||||
| import 'package:http/http.dart' as http; | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
|
|
@@ -10,17 +11,19 @@ import 'package:http/http.dart' as http; | |||||||||||||||||||||||
| class SkinService { | ||||||||||||||||||||||||
| static final SkinService _instance = SkinService._(); | ||||||||||||||||||||||||
| final FskinLogger _logger = FskinLogger(); | ||||||||||||||||||||||||
| final CacheService _cacheService = CacheService(); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| SkinService._(); | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| factory SkinService() { | ||||||||||||||||||||||||
| return _instance; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Future<ProjectConfig?> fetchData(String apiKey) async { | ||||||||||||||||||||||||
| Future<ProjectConfig?> fetchData() async { | ||||||||||||||||||||||||
| _logger.logMessage('Fetching skin configuration for the provided apiKey.'); | ||||||||||||||||||||||||
| var client = http.Client(); | ||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||
| final apiKey = await _cacheService.getApiKey(); | ||||||||||||||||||||||||
| var response = await client | ||||||||||||||||||||||||
| .post( | ||||||||||||||||||||||||
| Uri.https(FskinConstants.baseUrl, 'fskin/skin'), | ||||||||||||||||||||||||
|
|
@@ -37,17 +40,37 @@ class SkinService { | |||||||||||||||||||||||
| 'Error fetching skin configuration: ${response.statusCode}', | ||||||||||||||||||||||||
| errorObject: response, | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||
| return await getCachedConfig(); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| var decodedResponse = | ||||||||||||||||||||||||
| jsonDecode(utf8.decode(response.bodyBytes)) as Map<String, dynamic>; | ||||||||||||||||||||||||
| return ProjectConfig.fromMap(decodedResponse); | ||||||||||||||||||||||||
| var projectConfig = ProjectConfig.fromMap(decodedResponse); | ||||||||||||||||||||||||
| _cacheService.saveProjectConfig(decodedResponse); | ||||||||||||||||||||||||
| return projectConfig; | ||||||||||||||||||||||||
| } catch (e) { | ||||||||||||||||||||||||
| _logger.logError('Error fetching skin configuration: $e', errorObject: e); | ||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||
| return await getCachedConfig(); | ||||||||||||||||||||||||
| } finally { | ||||||||||||||||||||||||
| client.close(); | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| Future<ProjectConfig?> getCachedConfig() async { | ||||||||||||||||||||||||
| final savedProjectConfig = await _cacheService.getProjectConfig(); | ||||||||||||||||||||||||
| final savedLastUpdated = await _cacheService.getLastUpdated(); | ||||||||||||||||||||||||
|
Comment on lines
+59
to
+61
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🩺 Stability & Availability | 🟠 Major | ⚡ Quick win Treat malformed cache data as a cache miss.
🤖 Prompt for AI AgentsSource: MCP tools |
||||||||||||||||||||||||
|
|
||||||||||||||||||||||||
| if (savedProjectConfig != null && savedLastUpdated != null) { | ||||||||||||||||||||||||
| final currentTime = DateTime.now(); | ||||||||||||||||||||||||
| final difference = currentTime.difference(savedLastUpdated); | ||||||||||||||||||||||||
| // If the cached data is less than or equal to 3 days old, return the cached configuration | ||||||||||||||||||||||||
| if (difference.inDays <= 3) { | ||||||||||||||||||||||||
|
Comment on lines
+63
to
+67
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win Enforce the TTL using the full duration.
Proposed fix- final difference = currentTime.difference(savedLastUpdated);
+ final age = currentTime.difference(savedLastUpdated);
// If the cached data is less than or equal to 3 days old, return the cached configuration
- if (difference.inDays <= 3) {
+ if (!age.isNegative &&
+ age.compareTo(const Duration(days: 3)) <= 0) {📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||
| _logger.logMessage( | ||||||||||||||||||||||||
| 'Using cached skin configuration. Last updated: $savedLastUpdated', | ||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||
| return savedProjectConfig; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| return null; | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| } | ||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,7 +26,7 @@ void main() { | |
|
|
||
| test('Verifying Service returning valid skin theme data', () async { | ||
| when( | ||
| () => mockSkinService?.fetchData(apiKey), | ||
| () => mockSkinService?.fetchData(), | ||
| ).thenAnswer((_) async => ProjectConfig(skin: skinModelMock)); | ||
|
Comment on lines
+29
to
30
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win Add regression tests for the cache path. These changes only update Mocktail calls. The test file does not exercise retryable HTTP failures, 401/403 handling, the exact three-day TTL boundary, or malformed cached data. Add focused tests for these paths before merging. (raw.githubusercontent.com) Also applies to: 48-49, 69-70, 95-96 🤖 Prompt for AI Agents |
||
|
|
||
| var remoteConfig = await FskinRemoteConfig.init( | ||
|
|
@@ -45,7 +45,7 @@ void main() { | |
|
|
||
| test('Verifying Service returning null skin theme data', () async { | ||
| when( | ||
| () => mockSkinService?.fetchData(apiKey), | ||
| () => mockSkinService?.fetchData(), | ||
| ).thenAnswer((_) async => ProjectConfig(skin: null)); | ||
|
|
||
| var remoteConfig = await FskinRemoteConfig.init( | ||
|
|
@@ -66,7 +66,7 @@ void main() { | |
| 'Verifying Service returning null skin value with fallback theme data', | ||
| () async { | ||
| when( | ||
| () => mockSkinService?.fetchData(apiKey), | ||
| () => mockSkinService?.fetchData(), | ||
| ).thenAnswer((_) async => ProjectConfig(skin: null)); | ||
|
|
||
| var remoteConfig = await FskinRemoteConfig.init( | ||
|
|
@@ -92,7 +92,7 @@ void main() { | |
| 'Verifying Service returning valid skin data with fallback theme data', | ||
| () async { | ||
| when( | ||
| () => mockSkinService?.fetchData(apiKey), | ||
| () => mockSkinService?.fetchData(), | ||
| ).thenAnswer((_) async => ProjectConfig(skin: skinModelMock)); | ||
|
|
||
| var remoteConfig = await FskinRemoteConfig.init( | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.