diff --git a/example/devtools_options.yaml b/example/devtools_options.yaml index fa0b357..4dcfde9 100644 --- a/example/devtools_options.yaml +++ b/example/devtools_options.yaml @@ -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 \ No newline at end of file diff --git a/example/lib/data/prefs_async.dart b/example/lib/data/prefs_async.dart new file mode 100644 index 0000000..fa6b7c2 --- /dev/null +++ b/example/lib/data/prefs_async.dart @@ -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(); diff --git a/example/lib/data/scan_controller.dart b/example/lib/data/scan_controller.dart index 2144666..a3fbf85 100644 --- a/example/lib/data/scan_controller.dart +++ b/example/lib/data/scan_controller.dart @@ -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 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 _loadFilterFromPrefs(SharedPreferences prefs) async { - final services = prefs.getString('scan_filter_services') ?? ''; - final namePrefix = prefs.getString('scan_filter_name_prefix') ?? ''; + Future _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 _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(',') diff --git a/example/lib/data/storage_service.dart b/example/lib/data/storage_service.dart index c52f3a3..4abb72d 100644 --- a/example/lib/data/storage_service.dart +++ b/example/lib/data/storage_service.dart @@ -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 init() async { - _preferences = await SharedPreferencesWithCache.create( - cacheOptions: const SharedPreferencesWithCacheOptions(), - ); + // SharedPreferencesAsync is used via prefsAsync; no async init needed. } Future setFavoriteServices(List services) async { - await _preferences.setStringList('favorite_services', services); + await prefsAsync.setStringList('favorite_services', services); } - List getFavoriteServices() => - _preferences.getStringList('favorite_services') ?? []; + Future> getFavoriteServices() async => + (await prefsAsync.getStringList('favorite_services')) ?? []; } diff --git a/example/lib/home/scanner_screen.dart b/example/lib/home/scanner_screen.dart index 2b0b415..5430580 100644 --- a/example/lib/home/scanner_screen.dart +++ b/example/lib/home/scanner_screen.dart @@ -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 { AvailabilityState? bleAvailabilityState; ScanFilter? scanFilter; - ScanOrder _scanOrder = ScanOrder.lastSeen; + ScanOrder _scanOrder = ScanOrder.rssi; final Map _isExpanded = {}; final Map _deviceAdFlashTrigger = {}; @@ -107,14 +107,17 @@ class _ScannerScreenState extends State { Future _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 { Future _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 { _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 { value: ScanOrder.name, child: Text('Name'), ), - const PopupMenuItem( - value: ScanOrder.rssi, - child: Text('RSSI'), - ), ], child: Padding( padding: const EdgeInsets.symmetric(horizontal: 8), diff --git a/example/lib/home/widgets/scanned_item_widget.dart b/example/lib/home/widgets/scanned_item_widget.dart index 94022aa..73b4786 100644 --- a/example/lib/home/widgets/scanned_item_widget.dart +++ b/example/lib/home/widgets/scanned_item_widget.dart @@ -72,6 +72,7 @@ class _ScannedItemWidgetState extends State { return FlashOnUpdateWidget( trigger: widget.adFlashTrigger, + borderRadius: 16, child: Card( elevation: 2, margin: EdgeInsets.zero, diff --git a/example/lib/peripheral_details/peripheral_detail_page.dart b/example/lib/peripheral_details/peripheral_detail_page.dart index c64fc9d..3b95e52 100644 --- a/example/lib/peripheral_details/peripheral_detail_page.dart +++ b/example/lib/peripheral_details/peripheral_detail_page.dart @@ -53,11 +53,13 @@ class _PeripheralDetailPageState extends State { final Set _favoriteServices = {}; Set? _currentPropertyFilters; - void _loadFavoriteServices() { - final favorites = StorageService.instance.getFavoriteServices(); - setState(() { - _favoriteServices.addAll(favorites); - }); + Future _loadFavoriteServices() async { + final favorites = await StorageService.instance.getFavoriteServices(); + if (mounted) { + setState(() { + _favoriteServices.addAll(favorites); + }); + } } Future _saveFavoriteServices() async { diff --git a/example/lib/widgets/flash_on_update_widget.dart b/example/lib/widgets/flash_on_update_widget.dart index 9c69df1..a5b173f 100644 --- a/example/lib/widgets/flash_on_update_widget.dart +++ b/example/lib/widgets/flash_on_update_widget.dart @@ -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 createState() => _FlashOnUpdateWidgetState(); +} + +class _FlashOnUpdateWidgetState extends State + with SingleTickerProviderStateMixin { + late final AnimationController _controller; + late final Animation _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( - key: ValueKey(trigger), - tween: Tween(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; + } +}