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:
@@ -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
|
||||
@@ -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();
|
||||
@@ -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(',')
|
||||
|
||||
@@ -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')) ?? [];
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user