From d0feca1ba8897c94a7fdf5a3195c661d984d022b Mon Sep 17 00:00:00 2001 From: marco Date: Fri, 19 Sep 2025 20:43:38 +0200 Subject: [PATCH] added persistence using shared preferences --- android/app/proguard-rules.pro | 2 + .../example/maps_bookmarks/MainActivity.kt | 3 + devtools_options.yaml | 3 + lib/main.dart | 5 +- lib/model/bookmark.dart | 32 ++++- lib/model/collection.dart | 16 ++- lib/service/storage.dart | 117 +++++++++++++++ pubspec.lock | 135 +++++++++++++++++- pubspec.yaml | 3 +- 9 files changed, 301 insertions(+), 15 deletions(-) create mode 100644 android/app/proguard-rules.pro create mode 100644 devtools_options.yaml create mode 100644 lib/service/storage.dart diff --git a/android/app/proguard-rules.pro b/android/app/proguard-rules.pro new file mode 100644 index 0000000..a407603 --- /dev/null +++ b/android/app/proguard-rules.pro @@ -0,0 +1,2 @@ +-keep class io.flutter.plugins.sharedpreferences.** { *; } +-keep class io.flutter.plugins.sharedpreferences.LegacySharedPreferencesPlugin { *; } \ No newline at end of file diff --git a/android/app/src/main/kotlin/com/example/maps_bookmarks/MainActivity.kt b/android/app/src/main/kotlin/com/example/maps_bookmarks/MainActivity.kt index 0f2debc..ee8d0f5 100644 --- a/android/app/src/main/kotlin/com/example/maps_bookmarks/MainActivity.kt +++ b/android/app/src/main/kotlin/com/example/maps_bookmarks/MainActivity.kt @@ -5,6 +5,7 @@ import android.os.Bundle import io.flutter.embedding.android.FlutterActivity import io.flutter.embedding.engine.FlutterEngine import io.flutter.plugin.common.MethodChannel +import io.flutter.plugins.sharedpreferences.SharedPreferencesPlugin class MainActivity: FlutterActivity() { private var sharedText: String? = null @@ -31,6 +32,8 @@ class MainActivity: FlutterActivity() { } override fun configureFlutterEngine(flutterEngine: FlutterEngine) { + super.configureFlutterEngine(flutterEngine) + flutterEngine.plugins.add(SharedPreferencesPlugin()) MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL) .setMethodCallHandler { call, result -> if (call.method == "getSharedText") { diff --git a/devtools_options.yaml b/devtools_options.yaml new file mode 100644 index 0000000..fa0b357 --- /dev/null +++ b/devtools_options.yaml @@ -0,0 +1,3 @@ +description: This file stores settings for Dart & Flutter DevTools. +documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states +extensions: diff --git a/lib/main.dart b/lib/main.dart index 6d4cd63..b81a52d 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,7 +1,10 @@ import 'package:flutter/material.dart'; import 'pages/collections_page.dart'; +import 'service/storage.dart'; -void main() { +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + await Storage.initialize(); runApp(const MapsBookmarks()); } diff --git a/lib/model/bookmark.dart b/lib/model/bookmark.dart index 233ce5a..f10910a 100644 --- a/lib/model/bookmark.dart +++ b/lib/model/bookmark.dart @@ -1,11 +1,35 @@ class Bookmark { - Bookmark({required this.name, required this.link}); + Bookmark({ + required this.collectionId, + required this.name, + required this.link, + required this.description, + int? createdAt, + }) : createdAt = createdAt ?? DateTime.now().millisecondsSinceEpoch; - factory Bookmark.fromJson(Map json) => - Bookmark(name: json['name'] as String, link: json['link'] as String); + factory Bookmark.fromJson(Map json) => Bookmark( + collectionId: json['collectionId'] as int, + name: json['name'] as String, + link: json['link'] as String, + description: json['description'] as String, + createdAt: json['createdAt'] as int, + ); + int collectionId; String link; String name; + String description; + int createdAt; - Map toJson() => {'name': name, 'link': link}; + int get id => createdAt; + + DateTime get createdDate => DateTime.fromMillisecondsSinceEpoch(createdAt); + + Map toJson() => { + 'collectionId': collectionId, + 'name': name, + 'link': link, + 'description': description, + 'createdAt': createdAt, + }; } diff --git a/lib/model/collection.dart b/lib/model/collection.dart index c6e4b71..c3dac35 100644 --- a/lib/model/collection.dart +++ b/lib/model/collection.dart @@ -1,10 +1,18 @@ class Collection { - Collection({required this.name}); + Collection({required this.name, int? createdAt}) + : createdAt = createdAt ?? DateTime.now().millisecondsSinceEpoch; - factory Collection.fromJson(Map json) => - Collection(name: json['name'] as String); + factory Collection.fromJson(Map json) => Collection( + name: json['name'] as String, + createdAt: json['createdAt'] as int, + ); String name; + int createdAt; // used as Id with millisecondsSinceEpoch - Map toJson() => {'name': name}; + int get id => createdAt; + + DateTime get createdDate => DateTime.fromMillisecondsSinceEpoch(createdAt); + + Map toJson() => {'name': name, 'createdAt': createdAt}; } diff --git a/lib/service/storage.dart b/lib/service/storage.dart new file mode 100644 index 0000000..bc4aac4 --- /dev/null +++ b/lib/service/storage.dart @@ -0,0 +1,117 @@ +import 'dart:convert' show jsonDecode, jsonEncode; + +import 'package:shared_preferences/shared_preferences.dart'; + +import '../model/bookmark.dart'; +import '../model/collection.dart'; + +class Storage { + static const String _collectionsKey = 'collections'; + static const String _bookmarksKey = 'bookmarks'; + static const String _statsKey = 'stats'; + static SharedPreferencesWithCache? _prefsWithCache; + + static Future initialize() async { + _prefsWithCache = await SharedPreferencesWithCache.create( + cacheOptions: const SharedPreferencesWithCacheOptions( + allowList: {_collectionsKey, _bookmarksKey, _statsKey}, + ), + ); + } + + static SharedPreferencesWithCache get _prefs { + if (_prefsWithCache == null) { + throw StateError( + 'BookmarkStorage not initialized. Call initialize() first.', + ); + } + return _prefsWithCache!; + } + + static List loadCollections() { + final jsonString = _prefs.getString(_collectionsKey) ?? '[]'; + final jsonList = jsonDecode(jsonString) as List; + return jsonList + .map((json) => Collection.fromJson(json as Map)) + .toList(); + } + + static Future saveCollections(List collections) async { + final jsonList = collections.map((c) => c.toJson()).toList(); + await _prefs.setString(_collectionsKey, jsonEncode(jsonList)); + } + + static List loadAllBookmarks() { + final jsonString = _prefs.getString(_bookmarksKey) ?? '[]'; + final jsonList = jsonDecode(jsonString) as List; + return jsonList + .map((json) => Bookmark.fromJson(json as Map)) + .toList(); + } + + static Future saveAllBookmarks(List bookmarks) async { + final jsonList = bookmarks.map((b) => b.toJson()).toList(); + await _prefs.setString(_bookmarksKey, jsonEncode(jsonList)); + } + + static List loadBookmarksForCollection(int collectionId) { + final allBookmarks = loadAllBookmarks(); + return allBookmarks.where((b) => b.collectionId == collectionId).toList(); + } + + static Future addBookmark(Bookmark bookmark) async { + final bookmarks = loadAllBookmarks(); + bookmarks.add(bookmark); + await saveAllBookmarks(bookmarks); + } + + static Future deleteBookmarkById(int bookmarkId) async { + final bookmarks = loadAllBookmarks(); + bookmarks.removeWhere((b) => b.id == bookmarkId); + await saveAllBookmarks(bookmarks); + } + + static Future deleteBookmarksForCollection(int collectionId) async { + final bookmarks = loadAllBookmarks(); + bookmarks.removeWhere((b) => b.collectionId == collectionId); + await saveAllBookmarks(bookmarks); + } + + static Future updateBookmarkById( + int bookmarkId, { + String? name, + String? description, + }) async { + final bookmarks = loadAllBookmarks(); + final index = bookmarks.indexWhere((b) => b.id == bookmarkId); + + if (index == -1) return; + + if (name != null) bookmarks[index].name = name; + if (description != null) bookmarks[index].description = description; + + await saveAllBookmarks(bookmarks); + } + + static Map getStats() { + final statsJson = _prefs.getString(_statsKey) ?? '{}'; + final stats = jsonDecode(statsJson) as Map; + return { + 'totalCollections': stats['totalCollections'] ?? 0, + 'totalBookmarks': stats['totalBookmarks'] ?? 0, + }; + } + + static Future updateStats() async { + final collections = loadCollections(); + final bookmarks = loadAllBookmarks(); + + final stats = { + 'totalCollections': collections.length, + 'totalBookmarks': bookmarks.length, + 'lastUpdated': DateTime.now().millisecondsSinceEpoch, + }; + + await _prefs.setString(_statsKey, jsonEncode(stats)); + } +} diff --git a/pubspec.lock b/pubspec.lock index 7542381..f206905 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -65,6 +65,22 @@ packages: url: "https://pub.dev" source: hosted version: "1.3.3" + ffi: + dependency: transitive + description: + name: ffi + sha256: "289279317b4b16eb2bb7e271abccd4bf84ec9bdcbe999e278a94b804f5630418" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" flutter: dependency: "direct main" description: flutter @@ -74,15 +90,20 @@ packages: dependency: "direct dev" description: name: flutter_lints - sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + sha256: "3105dc8492f6183fb076ccf1f351ac3d60564bff92e20bfc4af9cc1651f4e7e1" url: "https://pub.dev" source: hosted - version: "5.0.0" + version: "6.0.0" flutter_test: dependency: "direct dev" description: flutter source: sdk version: "0.0.0" + flutter_web_plugins: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" leak_tracker: dependency: transitive description: @@ -111,10 +132,10 @@ packages: dependency: transitive description: name: lints - sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + sha256: a5e2b223cb7c9c8efdc663ef484fdd95bb243bff242ef5b13e26883547fce9a0 url: "https://pub.dev" source: hosted - version: "5.1.1" + version: "6.0.0" matcher: dependency: transitive description: @@ -147,6 +168,30 @@ packages: url: "https://pub.dev" source: hosted version: "1.9.1" + path_provider_linux: + dependency: transitive + description: + name: path_provider_linux + sha256: f7a1fe3a634fe7734c8d3f2766ad746ae2a2884abe22e241a8b301bf5cac3279 + url: "https://pub.dev" + source: hosted + version: "2.2.1" + path_provider_platform_interface: + dependency: transitive + description: + name: path_provider_platform_interface + sha256: "88f5779f72ba699763fa3a3b06aa4bf6de76c8e5de842cf6f29e2e06476c2334" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + path_provider_windows: + dependency: transitive + description: + name: path_provider_windows + sha256: bd6f00dbd873bfb70d0761682da2b3a2c2fccc2b9e84c495821639601d81afe7 + url: "https://pub.dev" + source: hosted + version: "2.3.0" platform: dependency: transitive description: @@ -155,6 +200,70 @@ packages: url: "https://pub.dev" source: hosted version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + shared_preferences: + dependency: "direct main" + description: + name: shared_preferences + sha256: "6e8bf70b7fef813df4e9a36f658ac46d107db4b4cfe1048b477d4e453a8159f5" + url: "https://pub.dev" + source: hosted + version: "2.5.3" + shared_preferences_android: + dependency: transitive + description: + name: shared_preferences_android + sha256: a2608114b1ffdcbc9c120eb71a0e207c71da56202852d4aab8a5e30a82269e74 + url: "https://pub.dev" + source: hosted + version: "2.4.12" + shared_preferences_foundation: + dependency: transitive + description: + name: shared_preferences_foundation + sha256: "6a52cfcdaeac77cad8c97b539ff688ccfc458c007b4db12be584fbe5c0e49e03" + url: "https://pub.dev" + source: hosted + version: "2.5.4" + shared_preferences_linux: + dependency: transitive + description: + name: shared_preferences_linux + sha256: "580abfd40f415611503cae30adf626e6656dfb2f0cee8f465ece7b6defb40f2f" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_platform_interface: + dependency: transitive + description: + name: shared_preferences_platform_interface + sha256: "57cbf196c486bc2cf1f02b85784932c6094376284b3ad5779d1b1c6c6a816b80" + url: "https://pub.dev" + source: hosted + version: "2.4.1" + shared_preferences_web: + dependency: transitive + description: + name: shared_preferences_web + sha256: c49bd060261c9a3f0ff445892695d6212ff603ef3115edbb448509d407600019 + url: "https://pub.dev" + source: hosted + version: "2.4.3" + shared_preferences_windows: + dependency: transitive + description: + name: shared_preferences_windows + sha256: "94ef0f72b2d71bc3e700e025db3710911bd51a71cefb65cc609dd0d9a982e3c1" + url: "https://pub.dev" + source: hosted + version: "2.4.1" sky_engine: dependency: transitive description: flutter @@ -224,6 +333,22 @@ packages: url: "https://pub.dev" source: hosted version: "15.0.2" + web: + dependency: transitive + description: + name: web + sha256: "868d88a33d8a87b18ffc05f9f030ba328ffefba92d6c127917a2ba740f9cfe4a" + url: "https://pub.dev" + source: hosted + version: "1.1.1" + xdg_directories: + dependency: transitive + description: + name: xdg_directories + sha256: "7a3f37b05d989967cdddcbb571f1ea834867ae2faa29725fd085180e0883aa15" + url: "https://pub.dev" + source: hosted + version: "1.1.0" sdks: dart: ">=3.9.2 <4.0.0" - flutter: ">=3.18.0-18.0.pre.54" + flutter: ">=3.29.0" diff --git a/pubspec.yaml b/pubspec.yaml index 5a22274..72f82c3 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -14,12 +14,13 @@ dependencies: cupertino_icons: ^1.0.8 android_intent_plus: ^6.0.0 + shared_preferences: ^2.3.2 dev_dependencies: flutter_test: sdk: flutter - flutter_lints: ^5.0.0 + flutter_lints: ^6.0.0 flutter: