Integrate SharedPreferencesAsync for improved preferences handling

- Added a new `prefs_async.dart` file to encapsulate the usage of `SharedPreferencesAsync`.
- Updated `ScanController`, `StorageService`, and `ScannerScreen` to utilize the new async preferences methods, enhancing performance and code clarity.
- Modified `devtools_options.yaml` to enable the `shared_preferences` extension.
- Refactored the `FlashOnUpdateWidget` to improve the outline animation and added a customizable border radius.
- Adjusted the favorite services loading mechanism in `PeripheralDetailPage` to be asynchronous, ensuring better state management.
This commit is contained in:
Foti Dim
2026-02-09 22:24:41 +01:00
parent 485c0808ad
commit 8c75e3965f
8 changed files with 153 additions and 68 deletions
+1
View File
@@ -1,3 +1,4 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:
- shared_preferences: true
+5
View File
@@ -0,0 +1,5 @@
import 'package:shared_preferences/shared_preferences.dart';
/// Shared [SharedPreferencesAsync] instance for the example app.
/// Use this instead of the legacy [SharedPreferences.getInstance()] API.
final prefsAsync = SharedPreferencesAsync();
+12 -10
View File
@@ -1,7 +1,7 @@
import 'package:flutter/foundation.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:universal_ble/universal_ble.dart';
import 'package:universal_ble_example/data/company_identifier_service.dart';
import 'package:universal_ble_example/data/prefs_async.dart';
/// Shared controller for BLE scan state so the scan button can be shown
/// on both the scanner screen and the peripheral detail screen.
@@ -21,9 +21,8 @@ class ScanController extends ChangeNotifier {
Future<void> startScan() async {
if (_isScanning) return;
try {
final prefs = await SharedPreferences.getInstance();
final filter = await _loadFilterFromPrefs(prefs);
final platformConfig = _loadPlatformConfigFromPrefs(prefs);
final filter = await _loadFilterFromPrefs();
final platformConfig = await _loadPlatformConfigFromPrefs();
await UniversalBle.startScan(
scanFilter: filter,
@@ -49,11 +48,13 @@ class ScanController extends ChangeNotifier {
}
}
Future<ScanFilter?> _loadFilterFromPrefs(SharedPreferences prefs) async {
final services = prefs.getString('scan_filter_services') ?? '';
final namePrefix = prefs.getString('scan_filter_name_prefix') ?? '';
Future<ScanFilter?> _loadFilterFromPrefs() async {
final services =
(await prefsAsync.getString('scan_filter_services')) ?? '';
final namePrefix =
(await prefsAsync.getString('scan_filter_name_prefix')) ?? '';
final manufacturerData =
prefs.getString('scan_filter_manufacturer_data') ?? '';
(await prefsAsync.getString('scan_filter_manufacturer_data')) ?? '';
if (services.isEmpty &&
namePrefix.isEmpty &&
@@ -108,10 +109,11 @@ class ScanController extends ChangeNotifier {
);
}
PlatformConfig _loadPlatformConfigFromPrefs(SharedPreferences prefs) {
Future<PlatformConfig> _loadPlatformConfigFromPrefs() async {
WebOptions? web;
if (kIsWeb) {
final webServices = prefs.getString('scan_filter_web_services') ?? '';
final webServices =
(await prefsAsync.getString('scan_filter_web_services')) ?? '';
if (webServices.isNotEmpty) {
final list = webServices
.split(',')
+5 -9
View File
@@ -1,22 +1,18 @@
import 'package:shared_preferences/shared_preferences.dart';
import 'package:universal_ble_example/data/prefs_async.dart';
class StorageService {
StorageService._();
static StorageService? _instance;
static StorageService get instance => _instance ??= StorageService._();
late SharedPreferencesWithCache _preferences;
Future<void> init() async {
_preferences = await SharedPreferencesWithCache.create(
cacheOptions: const SharedPreferencesWithCacheOptions(),
);
// SharedPreferencesAsync is used via prefsAsync; no async init needed.
}
Future<void> setFavoriteServices(List<String> services) async {
await _preferences.setStringList('favorite_services', services);
await prefsAsync.setStringList('favorite_services', services);
}
List<String> getFavoriteServices() =>
_preferences.getStringList('favorite_services') ?? [];
Future<List<String>> getFavoriteServices() async =>
(await prefsAsync.getStringList('favorite_services')) ?? [];
}
+23 -20
View File
@@ -2,8 +2,8 @@ import 'dart:async';
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:universal_ble/universal_ble.dart';
import 'package:universal_ble_example/data/prefs_async.dart';
import 'package:universal_ble_example/data/company_identifier_service.dart';
import 'package:universal_ble_example/data/mock_universal_ble.dart';
import 'package:universal_ble_example/data/scan_controller.dart';
@@ -42,7 +42,7 @@ class _ScannerScreenState extends State<ScannerScreen> {
AvailabilityState? bleAvailabilityState;
ScanFilter? scanFilter;
ScanOrder _scanOrder = ScanOrder.lastSeen;
ScanOrder _scanOrder = ScanOrder.rssi;
final Map<String, bool> _isExpanded = {};
final Map<String, int> _deviceAdFlashTrigger = {};
@@ -107,14 +107,17 @@ class _ScannerScreenState extends State<ScannerScreen> {
Future<void> _loadScanFilters() async {
try {
final prefs = await SharedPreferences.getInstance();
final services = prefs.getString('scan_filter_services') ?? '';
final namePrefix = prefs.getString('scan_filter_name_prefix') ?? '';
final services =
(await prefsAsync.getString('scan_filter_services')) ?? '';
final namePrefix =
(await prefsAsync.getString('scan_filter_name_prefix')) ?? '';
final manufacturerData =
prefs.getString('scan_filter_manufacturer_data') ?? '';
final searchFilter = prefs.getString('scan_filter_search') ?? '';
final webServices = prefs.getString('scan_filter_web_services') ?? '';
final scanOrderStr = prefs.getString('scan_order');
(await prefsAsync.getString('scan_filter_manufacturer_data')) ?? '';
final searchFilter =
(await prefsAsync.getString('scan_filter_search')) ?? '';
final webServices =
(await prefsAsync.getString('scan_filter_web_services')) ?? '';
final scanOrderStr = await prefsAsync.getString('scan_order');
servicesFilterController.text = services;
namePrefixController.text = namePrefix;
@@ -151,17 +154,17 @@ class _ScannerScreenState extends State<ScannerScreen> {
Future<void> _saveScanFilters() async {
try {
final prefs = await SharedPreferences.getInstance();
await prefs.setString(
await prefsAsync.setString(
'scan_filter_services', servicesFilterController.text);
await prefs.setString(
await prefsAsync.setString(
'scan_filter_name_prefix', namePrefixController.text);
await prefs.setString(
await prefsAsync.setString(
'scan_filter_manufacturer_data', manufacturerDataController.text);
await prefs.setString('scan_filter_search', _searchFilterController.text);
await prefs.setString(
await prefsAsync.setString(
'scan_filter_search', _searchFilterController.text);
await prefsAsync.setString(
'scan_filter_web_services', _webServicesController.text);
await prefs.setString('scan_order', _scanOrder.name);
await prefsAsync.setString('scan_order', _scanOrder.name);
} catch (e) {
debugPrint('Failed to save scan filters: $e');
}
@@ -693,6 +696,10 @@ class _ScannerScreenState extends State<ScannerScreen> {
_saveScanFilters();
},
itemBuilder: (context) => [
const PopupMenuItem(
value: ScanOrder.rssi,
child: Text('RSSI'),
),
const PopupMenuItem(
value: ScanOrder.lastSeen,
child: Text('Last seen'),
@@ -701,10 +708,6 @@ class _ScannerScreenState extends State<ScannerScreen> {
value: ScanOrder.name,
child: Text('Name'),
),
const PopupMenuItem(
value: ScanOrder.rssi,
child: Text('RSSI'),
),
],
child: Padding(
padding: const EdgeInsets.symmetric(horizontal: 8),
@@ -72,6 +72,7 @@ class _ScannedItemWidgetState extends State<ScannedItemWidget> {
return FlashOnUpdateWidget(
trigger: widget.adFlashTrigger,
borderRadius: 16,
child: Card(
elevation: 2,
margin: EdgeInsets.zero,
@@ -53,11 +53,13 @@ class _PeripheralDetailPageState extends State<PeripheralDetailPage> {
final Set<String> _favoriteServices = {};
Set<CharacteristicProperty>? _currentPropertyFilters;
void _loadFavoriteServices() {
final favorites = StorageService.instance.getFavoriteServices();
setState(() {
_favoriteServices.addAll(favorites);
});
Future<void> _loadFavoriteServices() async {
final favorites = await StorageService.instance.getFavoriteServices();
if (mounted) {
setState(() {
_favoriteServices.addAll(favorites);
});
}
}
Future<void> _saveFavoriteServices() async {
+99 -24
View File
@@ -1,49 +1,124 @@
import 'package:flutter/material.dart';
/// Wraps [child] and briefly highlights it when [trigger] changes.
/// Uses a semi-opaque overlay that fades to transparent over ~300ms.
class FlashOnUpdateWidget extends StatelessWidget {
/// Wraps [child] and briefly shows an outline when [trigger] changes.
/// Draws only a stroked border that fades out smoothly; does not block interaction.
class FlashOnUpdateWidget extends StatefulWidget {
const FlashOnUpdateWidget({
super.key,
required this.trigger,
required this.child,
this.highlightColor,
this.borderRadius = 16,
});
/// When this value changes, the flash animation runs once.
/// When this value changes, the outline animation runs (or restarts).
final Object trigger;
final Widget child;
/// If null, [Theme.colorScheme.primaryContainer] is used with opacity.
/// If null, [Theme.colorScheme.primaryContainer] is used for the outline.
final Color? highlightColor;
/// Border radius of the outline to match the child (e.g. Card) corners.
final double borderRadius;
@override
State<FlashOnUpdateWidget> createState() => _FlashOnUpdateWidgetState();
}
class _FlashOnUpdateWidgetState extends State<FlashOnUpdateWidget>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
late final Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: const Duration(milliseconds: 450),
vsync: this,
);
_animation = CurvedAnimation(
parent: _controller,
curve: Curves.easeOut,
);
}
@override
void didUpdateWidget(FlashOnUpdateWidget oldWidget) {
super.didUpdateWidget(oldWidget);
if (widget.trigger != oldWidget.trigger) {
_controller.forward(from: 0);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
final colorScheme = Theme.of(context).colorScheme;
final color = highlightColor ??
colorScheme.primaryContainer.withValues(alpha: 0.6);
final color = widget.highlightColor ?? colorScheme.primaryContainer;
return TweenAnimationBuilder<double>(
key: ValueKey(trigger),
tween: Tween<double>(begin: 1, end: 0),
duration: const Duration(milliseconds: 300),
builder: (context, value, childWidget) {
return Stack(
children: [
childWidget!,
if (value > 0)
Positioned.fill(
child: IgnorePointer(
child: Container(
color: color.withValues(alpha: value * 0.6),
return Stack(
children: [
widget.child,
AnimatedBuilder(
animation: _animation,
builder: (context, _) {
final opacity = 1 - _animation.value;
if (opacity <= 0) return const SizedBox.shrink();
return Positioned.fill(
child: IgnorePointer(
child: CustomPaint(
painter: _OutlinePainter(
color: color,
opacity: opacity,
borderRadius: widget.borderRadius,
),
),
),
],
);
},
child: child,
);
},
),
],
);
}
}
class _OutlinePainter extends CustomPainter {
_OutlinePainter({
required this.color,
required this.opacity,
required this.borderRadius,
});
static const double _strokeWidth = 2;
final Color color;
final double opacity;
final double borderRadius;
@override
void paint(Canvas canvas, Size size) {
if (opacity <= 0) return;
final paint = Paint()
..color = color.withValues(alpha: opacity)
..style = PaintingStyle.stroke
..strokeWidth = _strokeWidth;
final half = _strokeWidth / 2;
final rect = Rect.fromLTWH(half, half, size.width - _strokeWidth, size.height - _strokeWidth);
final rrect = RRect.fromRectAndRadius(rect, Radius.circular(borderRadius - half));
canvas.drawRRect(rrect, paint);
}
@override
bool shouldRepaint(_OutlinePainter oldDelegate) {
return oldDelegate.opacity != opacity ||
oldDelegate.color != color ||
oldDelegate.borderRadius != borderRadius;
}
}