diff --git a/example/pubspec.lock b/example/pubspec.lock index e5e6b47..f9887d5 100644 --- a/example/pubspec.lock +++ b/example/pubspec.lock @@ -102,18 +102,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -142,18 +142,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" path: dependency: transitive description: @@ -166,10 +166,10 @@ packages: dependency: transitive description: name: platform - sha256: "12220bb4b65720483f8fa9450b4332347737cf8213dd2840d8b2c823e47243ec" + sha256: "9b71283fc13df574056616011fb138fd3b793ea47cc509c189a6c3fa5f8a1a65" url: "https://pub.dev" source: hosted - version: "3.1.4" + version: "3.1.5" plugin_platform_interface: dependency: transitive description: @@ -250,10 +250,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" vector_math: dependency: transitive description: @@ -266,10 +266,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" webdriver: dependency: transitive description: diff --git a/lib/delayed_action_handler.dart b/lib/delayed_action_handler.dart index 013d2c9..a8b0fc2 100644 --- a/lib/delayed_action_handler.dart +++ b/lib/delayed_action_handler.dart @@ -25,6 +25,8 @@ class DelayedActionHandler { } /// Cancels any scheduled action. + /// + /// This can be useful if the action is no longer needed. void cancelDelayed() { _timer?.cancel(); _timer = null; @@ -35,7 +37,17 @@ class DelayedActionHandler { /// /// If no action is scheduled, this method does nothing. void _performAction() { - _action?.call(); - _action = null; + try { + _action?.call(); + } catch (e) { + print('Error executing delayed action: $e'); + } finally { + _action = null; // Clear the action after execution + } + } + + /// Returns `true` if an action is currently scheduled, otherwise `false`. + bool isActionScheduled() { + return _timer?.isActive ?? false; } } diff --git a/lib/qrcode_barcode_scanner.dart b/lib/qrcode_barcode_scanner.dart index 3f5d94a..e7999f3 100644 --- a/lib/qrcode_barcode_scanner.dart +++ b/lib/qrcode_barcode_scanner.dart @@ -1,4 +1,5 @@ import 'dart:async'; + import 'package:flutter/material.dart'; import 'package:flutter/services.dart'; import 'package:qrcode_barcode_scanner/qrcode_barcode_scanner_platform_interface.dart'; @@ -31,10 +32,6 @@ class QrcodeBarcodeScanner { /// A delayed action handler to handle delayed events. final DelayedActionHandler _actionHandler; - Future getPlatformVersion() { - return QrcodeBarcodeScannerPlatform.instance.getPlatformVersion(); - } - /// Creates a new instance of [QrcodeBarcodeScanner]. /// /// The [onScannedCallback] parameter is a required callback function @@ -48,11 +45,15 @@ class QrcodeBarcodeScanner { _controller.stream.where((char) => char != null).listen(onKeyEvent); } + /// Retrieves the platform version from the native platform. + Future getPlatformVersion() { + return QrcodeBarcodeScannerPlatform.instance.getPlatformVersion(); + } + /// Handles a keyboard event by adding the read character to the [_pressedKeys] list. /// /// [readChar] is the character read from the keyboard. If [readChar] is not null, it is added to the - /// [_pressedKeys] list. If [_pressedKeys] is not empty, it is joined together to form a string [scannedCode]. - /// The [scannedCode] is passed to the [onScannedCallback] function and the [_pressedKeys] list is cleared. + /// [_pressedKeys] list. After a short delay, the pressed keys are combined into a scanned code. void onKeyEvent(String? readChar) { if (readChar != null) { _pressedKeys.add(readChar); @@ -65,18 +66,25 @@ class QrcodeBarcodeScanner { } } + /// Cancels the currently scheduled scan action, if any. + /// + /// This method can be used to cancel any pending scan action, preventing the callback + /// from being triggered. + void cancelScan() { + _actionHandler.cancelDelayed(); + _pressedKeys.clear(); + } + /// The callback function that is called when a keyboard event occurs. /// /// [event] is the raw keyboard event that occurred. bool _keyBoardCallback(KeyEvent event) { if (_isTextInputFocused()) { - return false; // Bypass scanner logic + return false; // Bypass scanner logic when text input is focused } // Check if the event contains a character and add it to the controller - if (event.character != null && - event.character!.isNotEmpty && - event.character!.codeUnits.any((unit) => unit != 0)) { + if (_isValidCharacter(event.character)) { _controller.add(event.character); return true; } @@ -84,14 +92,19 @@ class QrcodeBarcodeScanner { return false; } - /// Checks if a text input field is focused. + /// Checks if the character is valid for scanning (non-null and non-empty). + bool _isValidCharacter(String? character) { + return character != null && + character.isNotEmpty && + character.codeUnits.any((unit) => unit != 0); + } + + /// Checks if a text input field is currently focused. bool _isTextInputFocused() { final focus = FocusManager.instance.primaryFocus; - if (focus != null && focus.context != null) { - final context = focus.context!; - return context.findAncestorWidgetOfExactType() != null || - context.findAncestorWidgetOfExactType() != null || - context.findAncestorWidgetOfExactType() != null; + if (focus != null) { + return focus.context?.findAncestorWidgetOfExactType() != + null; } return false; } diff --git a/lib/qrcode_barcode_scanner_method_channel.dart b/lib/qrcode_barcode_scanner_method_channel.dart index b670a2f..ac80b35 100644 --- a/lib/qrcode_barcode_scanner_method_channel.dart +++ b/lib/qrcode_barcode_scanner_method_channel.dart @@ -3,16 +3,31 @@ import 'package:flutter/services.dart'; import 'qrcode_barcode_scanner_platform_interface.dart'; -/// An implementation of [QrcodeBarcodeScannerPlatform] that uses method channels. +/// An implementation of [QrcodeBarcodeScannerPlatform] that uses method channels +/// to communicate with the native platform. class MethodChannelQrcodeBarcodeScanner extends QrcodeBarcodeScannerPlatform { /// The method channel used to interact with the native platform. + /// + /// This channel is responsible for sending messages to and receiving messages from + /// the native Android or iOS layer. @visibleForTesting final methodChannel = const MethodChannel('qrcode_barcode_scanner'); + /// Gets the platform version from the native platform. + /// + /// This method invokes the `getPlatformVersion` method on the native side and + /// returns the result as a [String]. If the method call fails, it catches the error + /// and returns `null`. @override Future getPlatformVersion() async { - final version = - await methodChannel.invokeMethod('getPlatformVersion'); - return version; + try { + final version = + await methodChannel.invokeMethod('getPlatformVersion'); + return version; + } on PlatformException catch (e) { + // Handle potential exceptions that may occur when invoking the method + debugPrint('Failed to get platform version: ${e.message}'); + return null; + } } } diff --git a/lib/qrcode_barcode_scanner_platform_interface.dart b/lib/qrcode_barcode_scanner_platform_interface.dart index cc4b2ae..fd51a77 100644 --- a/lib/qrcode_barcode_scanner_platform_interface.dart +++ b/lib/qrcode_barcode_scanner_platform_interface.dart @@ -2,8 +2,14 @@ import 'package:plugin_platform_interface/plugin_platform_interface.dart'; import 'qrcode_barcode_scanner_method_channel.dart'; +/// The interface that platform-specific implementations of QR code and barcode scanners must implement. +/// +/// This class defines the contract that all platform implementations of the QR/Barcode scanner must follow. +/// It ensures that the platform-specific implementations can be dynamically loaded and replaced as needed. abstract class QrcodeBarcodeScannerPlatform extends PlatformInterface { /// Constructs a QrcodeBarcodeScannerPlatform. + /// + /// The constructor ensures that the platform-specific implementations are validated using the provided token. QrcodeBarcodeScannerPlatform() : super(token: _token); static final Object _token = Object(); @@ -13,18 +19,24 @@ abstract class QrcodeBarcodeScannerPlatform extends PlatformInterface { /// The default instance of [QrcodeBarcodeScannerPlatform] to use. /// - /// Defaults to [MethodChannelQrcodeBarcodeScanner]. + /// This defaults to the [MethodChannelQrcodeBarcodeScanner] implementation, which uses method channels + /// to communicate with the native platform (iOS/Android). static QrcodeBarcodeScannerPlatform get instance => _instance; - /// Platform-specific implementations should set this with their own - /// platform-specific class that extends [QrcodeBarcodeScannerPlatform] when - /// they register themselves. + /// Allows platform-specific implementations to set their own instance. + /// + /// Platform-specific implementations should call this method to register their own implementation + /// when they initialize. static set instance(QrcodeBarcodeScannerPlatform instance) { PlatformInterface.verifyToken(instance, _token); _instance = instance; } + /// Retrieves the platform version. + /// + /// This method must be overridden by the platform-specific implementation. + /// If it's not implemented, an [UnimplementedError] will be thrown. Future getPlatformVersion() { - throw UnimplementedError('platformVersion() has not been implemented.'); + throw UnimplementedError('getPlatformVersion() has not been implemented.'); } } diff --git a/pubspec.lock b/pubspec.lock index e267d07..6043965 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -71,18 +71,18 @@ packages: dependency: transitive description: name: leak_tracker - sha256: "7f0df31977cb2c0b88585095d168e689669a2cc9b97c309665e3386f3e9d341a" + sha256: "3f87a60e8c63aecc975dda1ceedbc8f24de75f09e4856ea27daf8958f2f0ce05" url: "https://pub.dev" source: hosted - version: "10.0.4" + version: "10.0.5" leak_tracker_flutter_testing: dependency: transitive description: name: leak_tracker_flutter_testing - sha256: "06e98f569d004c1315b991ded39924b21af84cf14cc94791b8aea337d25b57f8" + sha256: "932549fb305594d82d7183ecd9fa93463e9914e1b67cacc34bc40906594a1806" url: "https://pub.dev" source: hosted - version: "3.0.3" + version: "3.0.5" leak_tracker_testing: dependency: transitive description: @@ -111,18 +111,18 @@ packages: dependency: transitive description: name: material_color_utilities - sha256: "0e0a020085b65b6083975e499759762399b4475f766c21668c4ecca34ea74e5a" + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec url: "https://pub.dev" source: hosted - version: "0.8.0" + version: "0.11.1" meta: dependency: transitive description: name: meta - sha256: "7687075e408b093f36e6bbf6c91878cc0d4cd10f409506f7bc996f68220b9136" + sha256: bdb68674043280c3428e9ec998512fb681678676b3c54e773629ffe74419f8c7 url: "https://pub.dev" source: hosted - version: "1.12.0" + version: "1.15.0" path: dependency: transitive description: @@ -188,10 +188,10 @@ packages: dependency: transitive description: name: test_api - sha256: "9955ae474176f7ac8ee4e989dadfb411a58c30415bcfb648fa04b2b8a03afa7f" + sha256: "5b8a98dafc4d5c4c9c72d8b31ab2b23fc13422348d2997120294d3bac86b4ddb" url: "https://pub.dev" source: hosted - version: "0.7.0" + version: "0.7.2" vector_math: dependency: transitive description: @@ -204,10 +204,10 @@ packages: dependency: transitive description: name: vm_service - sha256: "3923c89304b715fb1eb6423f017651664a03bf5f4b29983627c4da791f74a4ec" + sha256: "5c5f338a667b4c644744b661f309fb8080bb94b18a7e91ef1dbd343bed00ed6d" url: "https://pub.dev" source: hosted - version: "14.2.1" + version: "14.2.5" sdks: dart: ">=3.3.0 <4.0.0" flutter: ">=3.18.0-18.0.pre.54" diff --git a/test/qrcode_barcode_scanner_method_channel_test.dart b/test/qrcode_barcode_scanner_method_channel_test.dart index a2903fe..361b6c0 100644 --- a/test/qrcode_barcode_scanner_method_channel_test.dart +++ b/test/qrcode_barcode_scanner_method_channel_test.dart @@ -3,28 +3,42 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:qrcode_barcode_scanner/qrcode_barcode_scanner_method_channel.dart'; void main() { - TestWidgetsFlutterBinding.ensureInitialized(); - MethodChannelQrcodeBarcodeScanner platform = MethodChannelQrcodeBarcodeScanner(); const MethodChannel channel = MethodChannel('qrcode_barcode_scanner'); + TestWidgetsFlutterBinding.ensureInitialized(); + setUp(() { + // Mocking the method channel to return specific values for the test TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger - .setMockMethodCallHandler( - channel, - (MethodCall methodCall) async { + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + if (methodCall.method == 'getPlatformVersion') { return '42'; - }, - ); + } + return null; + }); }); tearDown(() { + // Clean up the mock handler after each test TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger .setMockMethodCallHandler(channel, null); }); - test('getPlatformVersion', () async { - expect(await platform.getPlatformVersion(), '42'); + test('getPlatformVersion returns correct value', () async { + final version = await platform.getPlatformVersion(); + expect(version, '42'); + }); + + test('getPlatformVersion returns null for unknown method', () async { + // Simulating an unknown method call that should return null + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger + .setMockMethodCallHandler(channel, (MethodCall methodCall) async { + return null; + }); + + final version = await platform.getPlatformVersion(); + expect(version, isNull); }); } diff --git a/test/qrcode_barcode_scanner_test.dart b/test/qrcode_barcode_scanner_test.dart index 8bef774..f4997c1 100644 --- a/test/qrcode_barcode_scanner_test.dart +++ b/test/qrcode_barcode_scanner_test.dart @@ -1,31 +1,45 @@ import 'package:flutter_test/flutter_test.dart'; import 'package:qrcode_barcode_scanner/qrcode_barcode_scanner.dart'; -import 'package:qrcode_barcode_scanner/qrcode_barcode_scanner_platform_interface.dart'; -import 'package:qrcode_barcode_scanner/qrcode_barcode_scanner_method_channel.dart'; -import 'package:plugin_platform_interface/plugin_platform_interface.dart'; - -class MockQrcodeBarcodeScannerPlatform - with MockPlatformInterfaceMixin - implements QrcodeBarcodeScannerPlatform { - @override - Future getPlatformVersion() => Future.value('42'); -} void main() { - final QrcodeBarcodeScannerPlatform initialPlatform = - QrcodeBarcodeScannerPlatform.instance; + TestWidgetsFlutterBinding + .ensureInitialized(); // Ensure Flutter bindings are initialized + + late QrcodeBarcodeScanner scanner; + String? scannedResult; + + setUp(() { + scannedResult = null; + scanner = QrcodeBarcodeScanner(onScannedCallback: (code) { + scannedResult = code; + }); + }); - test('$MethodChannelQrcodeBarcodeScanner is the default instance', () { - expect(initialPlatform, isInstanceOf()); + tearDown(() { + // Dispose the scanner after each test to free resources + scanner.dispose(); }); - test('getPlatformVersion', () async { - QrcodeBarcodeScanner qrcodeBarcodeScannerPlugin = - QrcodeBarcodeScanner(onScannedCallback: (String scannedCode) {}); - MockQrcodeBarcodeScannerPlatform fakePlatform = - MockQrcodeBarcodeScannerPlatform(); - QrcodeBarcodeScannerPlatform.instance = fakePlatform; + test('onScannedCallback is called with correct value', () { + // Simulate keyboard events + scanner.onKeyEvent('1'); + scanner.onKeyEvent('2'); + scanner.onKeyEvent('3'); + + // No result should be available immediately + expect(scannedResult, isNull); + + // Simulate a delay and check if the callback is triggered with the correct value + Future.delayed(const Duration(milliseconds: 150), () { + expect(scannedResult, '123'); + }); + }); - expect(await qrcodeBarcodeScannerPlugin.getPlatformVersion(), '42'); + test('cancelled scan does not call callback', () { + // Simulate a key event but cancel the delayed action + scanner.onKeyEvent('A'); + scanner.cancelScan(); // Use the new cancelScan method + expect(scannedResult, + isNull); // No result expected since the scan is cancelled }); }