From a7bfb5fb2bf8b27c9c79f816fe554f4f901314e2 Mon Sep 17 00:00:00 2001 From: koukibadr Date: Sat, 15 Aug 2026 11:19:30 +0300 Subject: [PATCH 1/3] feat: setup caching with shared prefs with TTL --- example/lib/main.dart | 2 +- lib/extensions/color_extensions.dart | 6 +++ lib/extensions/color_scheme_extensions.dart | 15 +++++++ lib/models/skin_model.dart | 16 +++++++ lib/remote/fskin_remote_config.dart | 5 ++- lib/services/cache_service.dart | 50 +++++++++++++++++++++ lib/services/skin_service.dart | 25 ++++++++++- pubspec.yaml | 1 + 8 files changed, 116 insertions(+), 4 deletions(-) create mode 100644 lib/services/cache_service.dart diff --git a/example/lib/main.dart b/example/lib/main.dart index c5e1ef2..b8d124c 100644 --- a/example/lib/main.dart +++ b/example/lib/main.dart @@ -6,7 +6,7 @@ void main() async { WidgetsFlutterBinding.ensureInitialized(); await FlutterSkin.init( apiKey: - "fsk_b0ce429cfbded17bbca66eef6e68bd1f0b7fbf9d74be0c3a0ac8b2e0554b7919", + "fsk_dc0054468a27dde1671142669f2065f93870975bbb773f1af0ab5898797956db", ); runApp(const MyApp()); } diff --git a/lib/extensions/color_extensions.dart b/lib/extensions/color_extensions.dart index 849b295..15dcf8e 100644 --- a/lib/extensions/color_extensions.dart +++ b/lib/extensions/color_extensions.dart @@ -6,3 +6,9 @@ extension ColorExtensions on String { return Color(int.parse(hexColor, radix: 16) + 0xFF000000); } } + +extension ColorToHex on Color { + String toHexString() { + return '#${toARGB32().toRadixString(16).substring(2).toUpperCase()}'; + } +} diff --git a/lib/extensions/color_scheme_extensions.dart b/lib/extensions/color_scheme_extensions.dart index f663cd3..bef7011 100644 --- a/lib/extensions/color_scheme_extensions.dart +++ b/lib/extensions/color_scheme_extensions.dart @@ -19,3 +19,18 @@ ColorScheme colorSchemeFromJson(Map map) { : Brightness.light, ); } + +Map colorSchemeToJson(ColorScheme colorScheme) { + return { + 'primary': colorScheme.primary.toHexString(), + 'secondary': colorScheme.secondary.toHexString(), + 'secondaryContainer': colorScheme.secondaryContainer.toHexString(), + 'surface': colorScheme.surface.toHexString(), + 'error': colorScheme.error.toHexString(), + 'onPrimary': colorScheme.onPrimary.toHexString(), + 'onSecondary': colorScheme.onSecondary.toHexString(), + 'onSurface': colorScheme.onSurface.toHexString(), + 'onError': colorScheme.onError.toHexString(), + 'brightness': colorScheme.brightness == Brightness.dark ? 'dark' : 'light', + }; +} diff --git a/lib/models/skin_model.dart b/lib/models/skin_model.dart index d086ec1..4f6c38e 100644 --- a/lib/models/skin_model.dart +++ b/lib/models/skin_model.dart @@ -66,4 +66,20 @@ class SkinModel { return null; } } + + Map toMap() { + return { + 'id': id, + 'projectId': projectId, + 'isActive': isActive, + 'version': version, + 'createdAt': createdAt.toIso8601String(), + 'publishedAt': publishedAt?.toIso8601String(), + 'deletedAt': deletedAt?.toIso8601String(), + 'colors': colors != null ? colorSchemeToJson(colors!) : null, + 'font': fontFamily, + 'googleFont': googleFont, + 'TTL': 3600, + }; + } } diff --git a/lib/remote/fskin_remote_config.dart b/lib/remote/fskin_remote_config.dart index 21f279d..deeacfb 100644 --- a/lib/remote/fskin_remote_config.dart +++ b/lib/remote/fskin_remote_config.dart @@ -3,6 +3,7 @@ import 'dart:async'; import 'package:flutter/material.dart'; 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/skin_service.dart'; /// Singleton class responsible for managing the remote configuration of the skin. @@ -17,6 +18,7 @@ class FskinRemoteConfig { ProjectConfig? _cachedConfig; SkinService? skinService; + CacheService cacheService = CacheService(); Stream get onSkinChanged => _skinController.stream; @@ -58,6 +60,7 @@ class FskinRemoteConfig { } _instance!.apiKey = apiKey; + _instance!.cacheService.saveApiKey(apiKey); if (skinService != null) { _instance!.skinService = skinService; } else { @@ -70,7 +73,7 @@ class FskinRemoteConfig { Future fetchConfig() async { // Call the skin service to fetch skin for developer and project //final skin = await SkinService().getSkin(apiKey); - _cachedConfig = await skinService?.fetchData(apiKey); + _cachedConfig = await skinService?.fetchData(); _skinController.add(ThemeData(colorScheme: _cachedConfig?.skin?.colors)); } diff --git a/lib/services/cache_service.dart b/lib/services/cache_service.dart new file mode 100644 index 0000000..8d9c91b --- /dev/null +++ b/lib/services/cache_service.dart @@ -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 saveApiKey(String apiKey) async { + final prefs = await sharedPreferences; + await prefs.setString('apiKey', apiKey); + } + + Future getApiKey() async { + final prefs = await sharedPreferences; + return prefs.getString('apiKey'); + } + + Future saveProjectConfig(Map projectConfig) async { + final prefs = await sharedPreferences; + await prefs.setString('projectConfig', jsonEncode(projectConfig)); + await prefs.setString('lastUpdated', DateTime.now().toIso8601String()); + } + + Future getProjectConfig() async { + final prefs = await sharedPreferences; + final projectConfigString = prefs.getString('projectConfig'); + + if (projectConfigString != null) { + final Map projectConfig = + jsonDecode(projectConfigString) as Map; + return ProjectConfig.fromMap(projectConfig); + } + return null; + } + + Future getLastUpdated() async { + final prefs = await sharedPreferences; + final value = prefs.getString('lastUpdated'); + return value != null ? DateTime.parse(value) : null; + } +} diff --git a/lib/services/skin_service.dart b/lib/services/skin_service.dart index 8636fa6..1585634 100644 --- a/lib/services/skin_service.dart +++ b/lib/services/skin_service.dart @@ -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,6 +11,7 @@ import 'package:http/http.dart' as http; class SkinService { static final SkinService _instance = SkinService._(); final FskinLogger _logger = FskinLogger(); + final CacheService _cacheService = CacheService(); SkinService._(); @@ -17,10 +19,11 @@ class SkinService { return _instance; } - Future fetchData(String apiKey) async { + Future 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'), @@ -42,9 +45,27 @@ class SkinService { var decodedResponse = jsonDecode(utf8.decode(response.bodyBytes)) as Map; - 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); + + final savedProjectConfig = await _cacheService.getProjectConfig(); + final savedLastUpdated = await _cacheService.getLastUpdated(); + + 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) { + _logger.logMessage( + 'Using cached skin configuration. Last updated: $savedLastUpdated', + ); + return savedProjectConfig; + } + } + return null; } finally { client.close(); diff --git a/pubspec.yaml b/pubspec.yaml index db21136..b2a1b8c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -16,6 +16,7 @@ dependencies: sdk: flutter http: ^1.6.0 google_fonts: ^8.2.1 + shared_preferences: ^2.5.5 dev_dependencies: flutter_test: From b134dc88f810f5e71d928d6f6f5793867d63d4ba Mon Sep 17 00:00:00 2001 From: koukibadr Date: Sat, 15 Aug 2026 11:23:12 +0300 Subject: [PATCH 2/3] chore: update package unit and intergration test --- .../flutter_skin_integration_with_service.dart | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/flutter_skin/flutter_skin_integration_with_service.dart b/test/flutter_skin/flutter_skin_integration_with_service.dart index eb86ed0..f224161 100644 --- a/test/flutter_skin/flutter_skin_integration_with_service.dart +++ b/test/flutter_skin/flutter_skin_integration_with_service.dart @@ -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)); 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( From e6c48708f3991c75443066beb0b08f5831d4196c Mon Sep 17 00:00:00 2001 From: koukibadr Date: Sat, 15 Aug 2026 11:51:13 +0300 Subject: [PATCH 3/3] chore: update toJson method and caching strategy --- lib/extensions/color_extensions.dart | 1 + lib/models/skin_model.dart | 1 - lib/services/skin_service.dart | 36 +++++++++++++++------------- 3 files changed, 20 insertions(+), 18 deletions(-) diff --git a/lib/extensions/color_extensions.dart b/lib/extensions/color_extensions.dart index 15dcf8e..6f364e1 100644 --- a/lib/extensions/color_extensions.dart +++ b/lib/extensions/color_extensions.dart @@ -1,4 +1,5 @@ import 'dart:ui'; +import 'package:flutter/services.dart'; extension ColorExtensions on String { Color toHexColor() { diff --git a/lib/models/skin_model.dart b/lib/models/skin_model.dart index 4f6c38e..9f9f334 100644 --- a/lib/models/skin_model.dart +++ b/lib/models/skin_model.dart @@ -79,7 +79,6 @@ class SkinModel { 'colors': colors != null ? colorSchemeToJson(colors!) : null, 'font': fontFamily, 'googleFont': googleFont, - 'TTL': 3600, }; } } diff --git a/lib/services/skin_service.dart b/lib/services/skin_service.dart index 1585634..3f196c5 100644 --- a/lib/services/skin_service.dart +++ b/lib/services/skin_service.dart @@ -40,7 +40,7 @@ class SkinService { 'Error fetching skin configuration: ${response.statusCode}', errorObject: response, ); - return null; + return await getCachedConfig(); } var decodedResponse = @@ -50,25 +50,27 @@ class SkinService { return projectConfig; } catch (e) { _logger.logError('Error fetching skin configuration: $e', errorObject: e); + return await getCachedConfig(); + } finally { + client.close(); + } + } - final savedProjectConfig = await _cacheService.getProjectConfig(); - final savedLastUpdated = await _cacheService.getLastUpdated(); + Future getCachedConfig() async { + final savedProjectConfig = await _cacheService.getProjectConfig(); + final savedLastUpdated = await _cacheService.getLastUpdated(); - 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) { - _logger.logMessage( - 'Using cached skin configuration. Last updated: $savedLastUpdated', - ); - return savedProjectConfig; - } + 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) { + _logger.logMessage( + 'Using cached skin configuration. Last updated: $savedLastUpdated', + ); + return savedProjectConfig; } - - return null; - } finally { - client.close(); } + return null; } }