diff --git a/.github/labeler.yml b/.github/labeler.yml
index 3e0a4af1d..e0414405b 100644
--- a/.github/labeler.yml
+++ b/.github/labeler.yml
@@ -13,6 +13,8 @@
- packages/firebase_core/**/*
"p: flutter_app_badger":
- packages/flutter_app_badger/**/*
+"p: flutter_reactive_ble":
+ - packages/flutter_reactive_ble/**/*
"p: flutter_secure_storage":
- packages/flutter_secure_storage/**/*
"p: flutter_tts":
diff --git a/.github/recipe.yaml b/.github/recipe.yaml
index 0ea4c5d66..f7ec7e8aa 100644
--- a/.github/recipe.yaml
+++ b/.github/recipe.yaml
@@ -23,6 +23,7 @@ plugins:
wakelock: ["wearable-5.5"]
# No tests.
+ flutter_reactive_ble: []
google_sign_in: []
image_picker: []
firebase_core: []
diff --git a/README.md b/README.md
index c99cf8f18..4f269dd39 100644
--- a/README.md
+++ b/README.md
@@ -21,6 +21,7 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina
| [**device_info_plus_tizen**](packages/device_info_plus) | [device_info_plus](https://pub.dev/packages/device_info_plus) (1st-party) | [![pub package](https://img.shields.io/pub/v/device_info_plus_tizen.svg)](https://pub.dev/packages/device_info_plus_tizen) | No |
| [**firebase_core_tizen**](packages/firebase_core) | [firebase_core](https://pub.dev/packages/firebase_core) | [![pub package](https://img.shields.io/pub/v/firebase_core_tizen.svg)](https://pub.dev/packages/firebase_core_tizen) | No |
| [**flutter_app_badger_tizen**](packages/flutter_app_badger) | [flutter_app_badger](https://pub.dev/packages/flutter_app_badger) (3rd-party) | [![pub package](https://img.shields.io/pub/v/flutter_app_badger_tizen.svg)](https://pub.dev/packages/flutter_app_badger_tizen) | No |
+| [**flutter_reactive_ble_tizen**](packages/flutter_reactive_ble) | [flutter_reactive_ble](https://pub.dev/packages/flutter_reactive_ble) (3rd-party) | [![pub package](https://img.shields.io/pub/v/flutter_reactive_ble_tizen.svg)](https://pub.dev/packages/flutter_reactive_ble_tizen) | No |
| [**flutter_secure_storage_tizen**](packages/flutter_secure_storage) | [flutter_secure_storage](https://pub.dev/packages/flutter_secure_storage) (3rd-party) | [![pub package](https://img.shields.io/pub/v/flutter_secure_storage_tizen.svg)](https://pub.dev/packages/flutter_secure_storage_tizen) | No |
| [**flutter_tts_tizen**](packages/flutter_tts) | [flutter_tts](https://pub.dev/packages/flutter_tts) (3rd-party) | [![pub package](https://img.shields.io/pub/v/flutter_tts_tizen.svg)](https://pub.dev/packages/flutter_tts_tizen) | No |
| [**flutter_webrtc_tizen**](packages/flutter_webrtc) | [flutter_webrtc](https://pub.dev/packages/flutter_webrtc) (3rd-party) | [![pub package](https://img.shields.io/pub/v/flutter_webrtc_tizen.svg)](https://pub.dev/packages/flutter_webrtc_tizen) | No |
@@ -63,6 +64,7 @@ The _"non-endorsed"_ status means that the plugin is not endorsed by the origina
| [**device_info_plus_tizen**](packages/device_info_plus) | 4.0 | ✔️ | ✔️ | ✔️ | ✔️ |
| [**firebase_core**](packages/firebase_core) | 4.0 | ✔️ | ✔️ | ✔️ | ✔️ |
| [**flutter_app_badger_tizen**](packages/flutter_app_badger) | 4.0 | ✔️ | ✔️ | ❌ | ❌ | API not supported |
+| [**flutter_reactive_ble_tizen**](packages/flutter_reactive_ble) | 4.0 | ✔️ | ❌ | ✔️ | ❌ | API not supported on emulator |
| [**flutter_secure_storage_tizen**](packages/flutter_secure_storage) | 4.0 | ✔️ | ✔️ | ✔️ | ✔️ |
| [**flutter_tts_tizen**](packages/flutter_tts) | 4.0 | ✔️ | ✔️ | ✔️ | ✔️ |
| [**flutter_webrtc_tizen**](packages/flutter_webrtc) | 6.0 | ❌ | ❌ | ✔️ | ❌ | No camera |
diff --git a/packages/flutter_reactive_ble/.gitignore b/packages/flutter_reactive_ble/.gitignore
new file mode 100644
index 000000000..d87f6f060
--- /dev/null
+++ b/packages/flutter_reactive_ble/.gitignore
@@ -0,0 +1,28 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# VS Code related
+.vscode/
+
+# Flutter/Dart/Pub related
+# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
+/pubspec.lock
+**/doc/api/
+.dart_tool/
+.packages
+build/
diff --git a/packages/flutter_reactive_ble/CHANGELOG.md b/packages/flutter_reactive_ble/CHANGELOG.md
new file mode 100644
index 000000000..607323422
--- /dev/null
+++ b/packages/flutter_reactive_ble/CHANGELOG.md
@@ -0,0 +1,3 @@
+## 0.1.0
+
+* Initial release.
diff --git a/packages/flutter_reactive_ble/LICENSE b/packages/flutter_reactive_ble/LICENSE
new file mode 100644
index 000000000..2ce17b2ff
--- /dev/null
+++ b/packages/flutter_reactive_ble/LICENSE
@@ -0,0 +1,26 @@
+Copyright (c) 2023 Samsung Electronics Co., Ltd. All rights reserved.
+Copyright (c) 2019 Signify Holding. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without modification,
+are permitted provided that the following conditions are met:
+
+ * Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+ copyright notice, this list of conditions and the following
+ disclaimer in the documentation and/or other materials provided
+ with the distribution.
+ * Neither the names of the copyright holders nor the names of the
+ contributors may be used to endorse or promote products derived
+ from this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
+ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
+(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
+LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON
+ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
+SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/packages/flutter_reactive_ble/README.md b/packages/flutter_reactive_ble/README.md
new file mode 100644
index 000000000..9d1c86098
--- /dev/null
+++ b/packages/flutter_reactive_ble/README.md
@@ -0,0 +1,38 @@
+# flutter_reactive_ble_tizen
+
+[![pub package](https://img.shields.io/pub/v/flutter_reactive_ble_tizen.svg)](https://pub.dev/packages/flutter_reactive_ble_tizen)
+
+The Tizen implementation of [`flutter_reactive_ble`](https://pub.dev/packages/flutter_reactive_ble).
+
+## Usage
+
+This package is not an _endorsed_ implementation of `flutter_reactive_ble`. Therefore, you have to include `flutter_reactive_ble_tizen` alongside `flutter_reactive_ble` as dependencies in your `pubspec.yaml` file.
+
+```yaml
+dependencies:
+ flutter_reactive_ble: ^5.0.3
+ flutter_reactive_ble_tizen: ^0.1.0
+```
+
+Then you can import `flutter_reactive_ble` in your Dart code:
+
+```dart
+import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
+```
+
+For detailed usage, see https://pub.dev/packages/flutter_reactive_ble#usage.
+
+## Required privileges
+
+The bluetooth privilege must be added to your `tizen-manifest.xml` file to use this plugin.
+
+```xml
+
+ http://tizen.org/privilege/bluetooth
+
+```
+
+## Known issues
+
+* The plugin sometimes doesn't respond to characteristic read or notify/indicate requests, requiring the user to press the same button twice in the example app.
+* Tizen's BLE module often fails to retrieve the names of discovered devices, which is not the case on other platforms such as Android.
diff --git a/packages/flutter_reactive_ble/analysis_options.yaml b/packages/flutter_reactive_ble/analysis_options.yaml
new file mode 100644
index 000000000..e8a8c4589
--- /dev/null
+++ b/packages/flutter_reactive_ble/analysis_options.yaml
@@ -0,0 +1,161 @@
+include: package:flutter_lints/flutter.yaml
+
+analyzer:
+ strong-mode:
+ implicit-casts: false
+ implicit-dynamic: false
+ exclude:
+ - "bin/cache/**"
+ - "**/*.freezed.dart"
+ - "**/*.g.dart"
+ - "**/*.mocks.dart"
+ - "**/generated/**"
+
+linter:
+ rules:
+ # Errors
+ - avoid_empty_else
+ - avoid_relative_lib_imports
+ - avoid_returning_null_for_future
+ - avoid_slow_async_io
+ - avoid_types_as_parameter_names
+ - cancel_subscriptions
+ - close_sinks
+ # - comment_references -- DISABLED: collision with mockito generation
+ - control_flow_in_finally
+ # - diagnostic_describe_all_properties -- DISABLED: experimental feature
+ - empty_statements
+ - hash_and_equals
+ - iterable_contains_unrelated_type
+ - list_remove_unrelated_type
+ - literal_only_boolean_expressions
+ - no_adjacent_strings_in_list
+ - no_duplicate_case_values
+ - prefer_void_to_null
+ - test_types_in_equals
+ - throw_in_finally
+ - unnecessary_statements
+ - unrelated_type_equality_checks
+ - valid_regexps
+ # Style checks
+ - always_declare_return_types
+ # - always_put_control_body_on_new_line -- DISABLED: DARTFMT INCOMPATIBLE
+ - always_put_required_named_parameters_first
+ - always_require_non_null_named_parameters
+ # - always_specify_types -- DISABLED: LEADS TO FLUFFY CODE
+ - annotate_overrides
+ # - avoid_annotating_with_dynamic -- DISABLED: Gives false positives for function arguments where it is needed
+ # - avoid_as
+ - avoid_bool_literals_in_conditional_expressions
+ - avoid_catches_without_on_clauses
+ - avoid_catching_errors
+ - avoid_classes_with_only_static_members
+ - avoid_double_and_int_checks
+ - avoid_field_initializers_in_const_classes
+ # - avoid_function_literals_in_foreach_calls -- DISABLED: forEach is often clearer than for-loop
+ - avoid_implementing_value_types
+ - avoid_init_to_null
+ - avoid_js_rounded_ints
+ - avoid_null_checks_in_equality_operators
+ - avoid_positional_boolean_parameters
+ - avoid_private_typedef_functions
+ - avoid_renaming_method_parameters
+ - avoid_return_types_on_setters
+ - avoid_returning_null
+ - avoid_returning_null_for_void
+ - avoid_returning_this
+ # - avoid_setters_without_getters -- DISABLED: Sometimes it makes sense to have write-only member
+ # - avoid_shadowing_type_parameters -- TODO: Enable new option
+ - avoid_single_cascade_in_expression_statements
+ # - avoid_types_on_closure_parameters -- DISABLED: INCOMPATIBLE WITH IMPLICIT TYPE CASTS DISABLED
+ - avoid_unused_constructor_parameters
+ - avoid_void_async
+ - await_only_futures
+ - camel_case_types
+ - cascade_invocations
+ - constant_identifier_names
+ # - curly_braces_in_flow_control_structures -- DISABLED: WE DO NOT WANT THIS
+ - directives_ordering
+ - empty_catches
+ - empty_constructor_bodies
+ - file_names
+ # - flutter_style_todos -- DISABLED: TOO MUCH?
+ - implementation_imports
+ - join_return_with_assignment
+ - library_names
+ - library_prefixes
+ # - lines_longer_than_80_chars -- DISABLED: WE DO NOT WANT THIS
+ - non_constant_identifier_names
+ - null_closures
+ - omit_local_variable_types
+ # - one_member_abstracts -- DISABLED: OBSTRUCTS OO DESIGN
+ - only_throw_errors
+ - overridden_fields
+ - package_api_docs
+ - package_prefixed_library_names
+ - parameter_assignments
+ - prefer_adjacent_string_concatenation
+ - prefer_asserts_in_initializer_lists
+ # - prefer_asserts_with_message -- DISABLED: BOOLEAN CONDITIONS ARE GOOD ENOUGH
+ - prefer_collection_literals
+ - prefer_conditional_assignment
+ - prefer_const_constructors
+ - prefer_const_constructors_in_immutables
+ - prefer_const_declarations
+ - prefer_const_literals_to_create_immutables
+ - prefer_constructors_over_static_methods
+ - prefer_contains
+ # - prefer_double_quotes -- DISABLED: This rule is to be supported in a future Dart release
+ - prefer_equal_for_default_values
+ - prefer_expression_function_bodies
+ - prefer_final_fields
+ - prefer_final_in_for_each
+ - prefer_final_locals
+ - prefer_for_elements_to_map_fromIterable
+ - prefer_foreach
+ - prefer_function_declarations_over_variables
+ - prefer_generic_function_type_aliases
+ - prefer_if_elements_to_conditional_expressions
+ - prefer_initializing_formals
+ - prefer_inlined_adds
+ # - prefer_int_literals -- DISABLED: ADDED VALUE IS UNCLEAR
+ - prefer_interpolation_to_compose_strings
+ - prefer_is_empty
+ - prefer_is_not_empty
+ - prefer_iterable_whereType
+ - prefer_mixin
+ - prefer_null_aware_operators
+ # - prefer_single_quotes -- DISABLED: ADDED VALUE IS UNCLEAR
+ - prefer_spread_collections
+ - prefer_typing_uninitialized_variables
+ # - provide_deprecation_messages -- DISABLED: This rule is to be supported in a future Dart release
+ # - public_member_api_docs
+ - recursive_getters
+ - slash_for_doc_comments
+ # - sort_child_properties_last -- This rule is to be supported in a future Dart release
+ # - sort_constructors_first -- DISABLED: ADDED VALUE IS UNCLEAR
+ # - sort_unnamed_constructors_first -- DISABLED: ADDED VALUE IS UNCLEAR
+ # - type_annotate_public_apis -- DISABLED: ADDED VALUE IS UNCLEAR
+ - type_init_formals
+ - unawaited_futures
+ - unnecessary_await_in_return
+ - unnecessary_brace_in_string_interps
+ - unnecessary_const
+ - unnecessary_getters_setters
+ - unnecessary_lambdas
+ - unnecessary_new
+ - unnecessary_null_aware_assignments
+ - unnecessary_null_in_if_null_operators
+ - unnecessary_overrides
+ - unnecessary_parenthesis
+ - unnecessary_this
+ - use_full_hex_values_for_flutter_colors
+ - use_function_type_syntax_for_parameters
+ - use_rethrow_when_possible
+ - use_setters_to_change_properties
+ - use_string_buffers
+ - use_to_and_as_if_applicable
+ - void_checks
+ # Pub Rules
+ - package_names
+ - sort_pub_dependencies
diff --git a/packages/flutter_reactive_ble/example/.gitignore b/packages/flutter_reactive_ble/example/.gitignore
new file mode 100644
index 000000000..ad3c2ca04
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/.gitignore
@@ -0,0 +1,42 @@
+# Miscellaneous
+*.class
+*.log
+*.pyc
+*.swp
+.DS_Store
+.atom/
+.buildlog/
+.history
+.svn/
+migrate_working_dir/
+
+# IntelliJ related
+*.iml
+*.ipr
+*.iws
+.idea/
+
+# VS Code related
+.vscode/
+
+# Flutter/Dart/Pub related
+**/doc/api/
+**/ios/Flutter/.last_build_id
+.dart_tool/
+.flutter-plugins
+.flutter-plugins-dependencies
+.packages
+.pub-cache/
+.pub/
+/build/
+
+# Symbolication related
+app.*.symbols
+
+# Obfuscation related
+app.*.map.json
+
+# Android Studio will place build artifacts here
+/android/app/debug
+/android/app/profile
+/android/app/release
diff --git a/packages/flutter_reactive_ble/example/README.md b/packages/flutter_reactive_ble/example/README.md
new file mode 100644
index 000000000..bb0a376dd
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/README.md
@@ -0,0 +1,7 @@
+# flutter_reactive_ble_tizen_example
+
+Demonstrates how to use the flutter_reactive_ble_tizen plugin.
+
+## Getting Started
+
+To run this app on your Tizen device, use [flutter-tizen](https://github.com/flutter-tizen/flutter-tizen).
diff --git a/packages/flutter_reactive_ble/example/lib/main.dart b/packages/flutter_reactive_ble/example/lib/main.dart
new file mode 100644
index 000000000..1e55b6969
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/lib/main.dart
@@ -0,0 +1,87 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
+import 'package:flutter_reactive_ble_example/src/ble/ble_device_connector.dart';
+import 'package:flutter_reactive_ble_example/src/ble/ble_device_interactor.dart';
+import 'package:flutter_reactive_ble_example/src/ble/ble_scanner.dart';
+import 'package:flutter_reactive_ble_example/src/ble/ble_status_monitor.dart';
+import 'package:flutter_reactive_ble_example/src/ui/ble_status_screen.dart';
+import 'package:flutter_reactive_ble_example/src/ui/device_list.dart';
+import 'package:provider/provider.dart';
+
+import 'src/ble/ble_logger.dart';
+
+const _themeColor = Colors.lightGreen;
+
+void main() {
+ WidgetsFlutterBinding.ensureInitialized();
+
+ final _ble = FlutterReactiveBle();
+ final _bleLogger = BleLogger(ble: _ble);
+ final _scanner = BleScanner(ble: _ble, logMessage: _bleLogger.addToLog);
+ final _monitor = BleStatusMonitor(_ble);
+ final _connector = BleDeviceConnector(
+ ble: _ble,
+ logMessage: _bleLogger.addToLog,
+ );
+ final _serviceDiscoverer = BleDeviceInteractor(
+ bleDiscoverServices: _ble.discoverServices,
+ readCharacteristic: _ble.readCharacteristic,
+ writeWithResponse: _ble.writeCharacteristicWithResponse,
+ writeWithOutResponse: _ble.writeCharacteristicWithoutResponse,
+ subscribeToCharacteristic: _ble.subscribeToCharacteristic,
+ logMessage: _bleLogger.addToLog,
+ );
+ runApp(
+ MultiProvider(
+ providers: [
+ Provider.value(value: _scanner),
+ Provider.value(value: _monitor),
+ Provider.value(value: _connector),
+ Provider.value(value: _serviceDiscoverer),
+ Provider.value(value: _bleLogger),
+ StreamProvider(
+ create: (_) => _scanner.state,
+ initialData: const BleScannerState(
+ discoveredDevices: [],
+ scanIsInProgress: false,
+ ),
+ ),
+ StreamProvider(
+ create: (_) => _monitor.state,
+ initialData: BleStatus.unknown,
+ ),
+ StreamProvider(
+ create: (_) => _connector.state,
+ initialData: const ConnectionStateUpdate(
+ deviceId: 'Unknown device',
+ connectionState: DeviceConnectionState.disconnected,
+ failure: null,
+ ),
+ ),
+ ],
+ child: MaterialApp(
+ title: 'Flutter Reactive BLE example',
+ color: _themeColor,
+ theme: ThemeData(primarySwatch: _themeColor),
+ home: const HomeScreen(),
+ ),
+ ),
+ );
+}
+
+class HomeScreen extends StatelessWidget {
+ const HomeScreen({
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) => Consumer(
+ builder: (_, status, __) {
+ if (status == BleStatus.ready) {
+ return const DeviceListScreen();
+ } else {
+ return BleStatusScreen(status: status ?? BleStatus.unknown);
+ }
+ },
+ );
+}
diff --git a/packages/flutter_reactive_ble/example/lib/src/ble/ble_device_connector.dart b/packages/flutter_reactive_ble/example/lib/src/ble/ble_device_connector.dart
new file mode 100644
index 000000000..9494149c8
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/lib/src/ble/ble_device_connector.dart
@@ -0,0 +1,58 @@
+import 'dart:async';
+
+import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
+import 'package:flutter_reactive_ble_example/src/ble/reactive_state.dart';
+
+class BleDeviceConnector extends ReactiveState {
+ BleDeviceConnector({
+ required FlutterReactiveBle ble,
+ required Function(String message) logMessage,
+ }) : _ble = ble,
+ _logMessage = logMessage;
+
+ final FlutterReactiveBle _ble;
+ final void Function(String message) _logMessage;
+
+ @override
+ Stream get state => _deviceConnectionController.stream;
+
+ final _deviceConnectionController = StreamController();
+
+ // ignore: cancel_subscriptions
+ late StreamSubscription _connection;
+
+ Future connect(String deviceId) async {
+ _logMessage('Start connecting to $deviceId');
+ _connection = _ble.connectToDevice(id: deviceId).listen(
+ (update) {
+ _logMessage(
+ 'ConnectionState for device $deviceId : ${update.connectionState}');
+ _deviceConnectionController.add(update);
+ },
+ onError: (Object e) =>
+ _logMessage('Connecting to device $deviceId resulted in error $e'),
+ );
+ }
+
+ Future disconnect(String deviceId) async {
+ try {
+ _logMessage('disconnecting to device: $deviceId');
+ await _connection.cancel();
+ } on Exception catch (e, _) {
+ _logMessage("Error disconnecting from a device: $e");
+ } finally {
+ // Since [_connection] subscription is terminated, the "disconnected" state cannot be received and propagated
+ _deviceConnectionController.add(
+ ConnectionStateUpdate(
+ deviceId: deviceId,
+ connectionState: DeviceConnectionState.disconnected,
+ failure: null,
+ ),
+ );
+ }
+ }
+
+ Future dispose() async {
+ await _deviceConnectionController.close();
+ }
+}
diff --git a/packages/flutter_reactive_ble/example/lib/src/ble/ble_device_interactor.dart b/packages/flutter_reactive_ble/example/lib/src/ble/ble_device_interactor.dart
new file mode 100644
index 000000000..3d09b5094
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/lib/src/ble/ble_device_interactor.dart
@@ -0,0 +1,110 @@
+import 'dart:async';
+
+import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
+
+class BleDeviceInteractor {
+ BleDeviceInteractor({
+ required Future> Function(String deviceId)
+ bleDiscoverServices,
+ required Future> Function(QualifiedCharacteristic characteristic)
+ readCharacteristic,
+ required Future Function(QualifiedCharacteristic characteristic,
+ {required List value})
+ writeWithResponse,
+ required Future Function(QualifiedCharacteristic characteristic,
+ {required List value})
+ writeWithOutResponse,
+ required void Function(String message) logMessage,
+ required Stream> Function(QualifiedCharacteristic characteristic)
+ subscribeToCharacteristic,
+ }) : _bleDiscoverServices = bleDiscoverServices,
+ _readCharacteristic = readCharacteristic,
+ _writeWithResponse = writeWithResponse,
+ _writeWithoutResponse = writeWithOutResponse,
+ _subScribeToCharacteristic = subscribeToCharacteristic,
+ _logMessage = logMessage;
+
+ final Future> Function(String deviceId)
+ _bleDiscoverServices;
+
+ final Future> Function(QualifiedCharacteristic characteristic)
+ _readCharacteristic;
+
+ final Future Function(QualifiedCharacteristic characteristic,
+ {required List value}) _writeWithResponse;
+
+ final Future Function(QualifiedCharacteristic characteristic,
+ {required List value}) _writeWithoutResponse;
+
+ final Stream> Function(QualifiedCharacteristic characteristic)
+ _subScribeToCharacteristic;
+
+ final void Function(String message) _logMessage;
+
+ Future> discoverServices(String deviceId) async {
+ try {
+ _logMessage('Start discovering services for: $deviceId');
+ final result = await _bleDiscoverServices(deviceId);
+ _logMessage('Discovering services finished');
+ return result;
+ } on Exception catch (e) {
+ _logMessage('Error occured when discovering services: $e');
+ rethrow;
+ }
+ }
+
+ Future> readCharacteristic(
+ QualifiedCharacteristic characteristic) async {
+ try {
+ final result = await _readCharacteristic(characteristic);
+
+ _logMessage('Read ${characteristic.characteristicId}: value = $result');
+ return result;
+ } on Exception catch (e, s) {
+ _logMessage(
+ 'Error occured when reading ${characteristic.characteristicId} : $e',
+ );
+ // ignore: avoid_print
+ print(s);
+ rethrow;
+ }
+ }
+
+ Future writeCharacterisiticWithResponse(
+ QualifiedCharacteristic characteristic, List value) async {
+ try {
+ _logMessage(
+ 'Write with response value : $value to ${characteristic.characteristicId}');
+ await _writeWithResponse(characteristic, value: value);
+ } on Exception catch (e, s) {
+ _logMessage(
+ 'Error occured when writing ${characteristic.characteristicId} : $e',
+ );
+ // ignore: avoid_print
+ print(s);
+ rethrow;
+ }
+ }
+
+ Future writeCharacterisiticWithoutResponse(
+ QualifiedCharacteristic characteristic, List value) async {
+ try {
+ await _writeWithoutResponse(characteristic, value: value);
+ _logMessage(
+ 'Write without response value: $value to ${characteristic.characteristicId}');
+ } on Exception catch (e, s) {
+ _logMessage(
+ 'Error occured when writing ${characteristic.characteristicId} : $e',
+ );
+ // ignore: avoid_print
+ print(s);
+ rethrow;
+ }
+ }
+
+ Stream> subScribeToCharacteristic(
+ QualifiedCharacteristic characteristic) {
+ _logMessage('Subscribing to: ${characteristic.characteristicId} ');
+ return _subScribeToCharacteristic(characteristic);
+ }
+}
diff --git a/packages/flutter_reactive_ble/example/lib/src/ble/ble_logger.dart b/packages/flutter_reactive_ble/example/lib/src/ble/ble_logger.dart
new file mode 100644
index 000000000..cfc3b6977
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/lib/src/ble/ble_logger.dart
@@ -0,0 +1,26 @@
+import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
+import 'package:intl/intl.dart';
+
+class BleLogger {
+ BleLogger({
+ required FlutterReactiveBle ble,
+ }) : _ble = ble;
+
+ final FlutterReactiveBle _ble;
+ final List _logMessages = [];
+ final DateFormat formatter = DateFormat('HH:mm:ss.SSS');
+
+ List get messages => _logMessages;
+
+ void addToLog(String message) {
+ final now = DateTime.now();
+ _logMessages.add('${formatter.format(now)} - $message');
+ }
+
+ void clearLogs() => _logMessages.clear();
+
+ bool get verboseLogging => _ble.logLevel == LogLevel.verbose;
+
+ void toggleVerboseLogging() =>
+ _ble.logLevel = verboseLogging ? LogLevel.none : LogLevel.verbose;
+}
diff --git a/packages/flutter_reactive_ble/example/lib/src/ble/ble_scanner.dart b/packages/flutter_reactive_ble/example/lib/src/ble/ble_scanner.dart
new file mode 100644
index 000000000..5efff29b5
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/lib/src/ble/ble_scanner.dart
@@ -0,0 +1,74 @@
+import 'dart:async';
+
+import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
+import 'package:flutter_reactive_ble_example/src/ble/reactive_state.dart';
+import 'package:meta/meta.dart';
+
+class BleScanner implements ReactiveState {
+ BleScanner({
+ required FlutterReactiveBle ble,
+ required Function(String message) logMessage,
+ }) : _ble = ble,
+ _logMessage = logMessage;
+
+ final FlutterReactiveBle _ble;
+ final void Function(String message) _logMessage;
+ final StreamController _stateStreamController =
+ StreamController();
+
+ final _devices = [];
+
+ @override
+ Stream get state => _stateStreamController.stream;
+
+ void startScan(List serviceIds) {
+ _logMessage('Start ble discovery');
+ _devices.clear();
+ _subscription?.cancel();
+ _subscription =
+ _ble.scanForDevices(withServices: serviceIds).listen((device) {
+ final knownDeviceIndex = _devices.indexWhere((d) => d.id == device.id);
+ if (knownDeviceIndex >= 0) {
+ _devices[knownDeviceIndex] = device;
+ } else {
+ _devices.add(device);
+ }
+ _pushState();
+ }, onError: (Object e) => _logMessage('Device scan fails with error: $e'));
+ _pushState();
+ }
+
+ void _pushState() {
+ _stateStreamController.add(
+ BleScannerState(
+ discoveredDevices: _devices,
+ scanIsInProgress: _subscription != null,
+ ),
+ );
+ }
+
+ Future stopScan() async {
+ _logMessage('Stop ble discovery');
+
+ await _subscription?.cancel();
+ _subscription = null;
+ _pushState();
+ }
+
+ Future dispose() async {
+ await _stateStreamController.close();
+ }
+
+ StreamSubscription? _subscription;
+}
+
+@immutable
+class BleScannerState {
+ const BleScannerState({
+ required this.discoveredDevices,
+ required this.scanIsInProgress,
+ });
+
+ final List discoveredDevices;
+ final bool scanIsInProgress;
+}
diff --git a/packages/flutter_reactive_ble/example/lib/src/ble/ble_status_monitor.dart b/packages/flutter_reactive_ble/example/lib/src/ble/ble_status_monitor.dart
new file mode 100644
index 000000000..cebbec76d
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/lib/src/ble/ble_status_monitor.dart
@@ -0,0 +1,11 @@
+import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
+import 'package:flutter_reactive_ble_example/src/ble/reactive_state.dart';
+
+class BleStatusMonitor implements ReactiveState {
+ const BleStatusMonitor(this._ble);
+
+ final FlutterReactiveBle _ble;
+
+ @override
+ Stream get state => _ble.statusStream;
+}
diff --git a/packages/flutter_reactive_ble/example/lib/src/ble/reactive_state.dart b/packages/flutter_reactive_ble/example/lib/src/ble/reactive_state.dart
new file mode 100644
index 000000000..f3441a399
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/lib/src/ble/reactive_state.dart
@@ -0,0 +1,3 @@
+abstract class ReactiveState {
+ Stream get state;
+}
diff --git a/packages/flutter_reactive_ble/example/lib/src/ui/ble_status_screen.dart b/packages/flutter_reactive_ble/example/lib/src/ui/ble_status_screen.dart
new file mode 100644
index 000000000..81ccc7a54
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/lib/src/ui/ble_status_screen.dart
@@ -0,0 +1,32 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
+
+class BleStatusScreen extends StatelessWidget {
+ const BleStatusScreen({required this.status, Key? key}) : super(key: key);
+
+ final BleStatus status;
+
+ String determineText(BleStatus status) {
+ switch (status) {
+ case BleStatus.unsupported:
+ return "This device does not support Bluetooth";
+ case BleStatus.unauthorized:
+ return "Authorize the FlutterReactiveBle example app to use Bluetooth and location";
+ case BleStatus.poweredOff:
+ return "Bluetooth is powered off on your device turn it on";
+ case BleStatus.locationServicesDisabled:
+ return "Enable location services";
+ case BleStatus.ready:
+ return "Bluetooth is up and running";
+ default:
+ return "Waiting to fetch Bluetooth status $status";
+ }
+ }
+
+ @override
+ Widget build(BuildContext context) => Scaffold(
+ body: Center(
+ child: Text(determineText(status)),
+ ),
+ );
+}
diff --git a/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/characteristic_interaction_dialog.dart b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/characteristic_interaction_dialog.dart
new file mode 100644
index 000000000..d2a252f52
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/characteristic_interaction_dialog.dart
@@ -0,0 +1,229 @@
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
+import 'package:flutter_reactive_ble_example/src/ble/ble_device_interactor.dart';
+import 'package:provider/provider.dart';
+
+class CharacteristicInteractionDialog extends StatelessWidget {
+ const CharacteristicInteractionDialog({
+ required this.characteristic,
+ Key? key,
+ }) : super(key: key);
+ final QualifiedCharacteristic characteristic;
+
+ @override
+ Widget build(BuildContext context) => Consumer(
+ builder: (context, interactor, _) => _CharacteristicInteractionDialog(
+ characteristic: characteristic,
+ readCharacteristic: interactor.readCharacteristic,
+ writeWithResponse: interactor.writeCharacterisiticWithResponse,
+ writeWithoutResponse:
+ interactor.writeCharacterisiticWithoutResponse,
+ subscribeToCharacteristic: interactor.subScribeToCharacteristic,
+ ));
+}
+
+class _CharacteristicInteractionDialog extends StatefulWidget {
+ const _CharacteristicInteractionDialog({
+ required this.characteristic,
+ required this.readCharacteristic,
+ required this.writeWithResponse,
+ required this.writeWithoutResponse,
+ required this.subscribeToCharacteristic,
+ Key? key,
+ }) : super(key: key);
+
+ final QualifiedCharacteristic characteristic;
+ final Future> Function(QualifiedCharacteristic characteristic)
+ readCharacteristic;
+ final Future Function(
+ QualifiedCharacteristic characteristic, List value)
+ writeWithResponse;
+
+ final Stream> Function(QualifiedCharacteristic characteristic)
+ subscribeToCharacteristic;
+
+ final Future Function(
+ QualifiedCharacteristic characteristic, List value)
+ writeWithoutResponse;
+
+ @override
+ _CharacteristicInteractionDialogState createState() =>
+ _CharacteristicInteractionDialogState();
+}
+
+class _CharacteristicInteractionDialogState
+ extends State<_CharacteristicInteractionDialog> {
+ late String readOutput;
+ late String writeOutput;
+ late String subscribeOutput;
+ late TextEditingController textEditingController;
+ late StreamSubscription>? subscribeStream;
+
+ @override
+ void initState() {
+ readOutput = '';
+ writeOutput = '';
+ subscribeOutput = '';
+ textEditingController = TextEditingController();
+ super.initState();
+ }
+
+ @override
+ void dispose() {
+ subscribeStream?.cancel();
+ super.dispose();
+ }
+
+ Future subscribeCharacteristic() async {
+ subscribeStream =
+ widget.subscribeToCharacteristic(widget.characteristic).listen((event) {
+ setState(() {
+ subscribeOutput = event.toString();
+ });
+ });
+ setState(() {
+ subscribeOutput = 'Notification set';
+ });
+ }
+
+ Future readCharacteristic() async {
+ final result = await widget.readCharacteristic(widget.characteristic);
+ setState(() {
+ readOutput = result.toString();
+ });
+ }
+
+ List _parseInput() => textEditingController.text
+ .split(',')
+ .map(
+ int.parse,
+ )
+ .toList();
+
+ Future writeCharacteristicWithResponse() async {
+ await widget.writeWithResponse(widget.characteristic, _parseInput());
+ setState(() {
+ writeOutput = 'Ok';
+ });
+ }
+
+ Future writeCharacteristicWithoutResponse() async {
+ await widget.writeWithoutResponse(widget.characteristic, _parseInput());
+ setState(() {
+ writeOutput = 'Done';
+ });
+ }
+
+ Widget sectionHeader(String text) => Text(
+ text,
+ style: const TextStyle(fontWeight: FontWeight.bold),
+ );
+
+ List get writeSection => [
+ sectionHeader('Write characteristic'),
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: TextField(
+ controller: textEditingController,
+ decoration: const InputDecoration(
+ border: OutlineInputBorder(),
+ labelText: 'Value',
+ ),
+ keyboardType: const TextInputType.numberWithOptions(
+ decimal: true,
+ signed: false,
+ ),
+ ),
+ ),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ ElevatedButton(
+ onPressed: writeCharacteristicWithResponse,
+ child: const Text('With response'),
+ ),
+ ElevatedButton(
+ onPressed: writeCharacteristicWithoutResponse,
+ child: const Text('Without response'),
+ ),
+ ],
+ ),
+ Padding(
+ padding: const EdgeInsetsDirectional.only(top: 8.0),
+ child: Text('Output: $writeOutput'),
+ ),
+ ];
+
+ List get readSection => [
+ sectionHeader('Read characteristic'),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ ElevatedButton(
+ onPressed: readCharacteristic,
+ child: const Text('Read'),
+ ),
+ Text('Output: $readOutput'),
+ ],
+ ),
+ ];
+
+ List get subscribeSection => [
+ sectionHeader('Subscribe / notify'),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ ElevatedButton(
+ onPressed: subscribeCharacteristic,
+ child: const Text('Subscribe'),
+ ),
+ Text('Output: $subscribeOutput'),
+ ],
+ ),
+ ];
+
+ Widget get divider => const Padding(
+ padding: EdgeInsets.symmetric(vertical: 12.0),
+ child: Divider(thickness: 2.0),
+ );
+
+ @override
+ Widget build(BuildContext context) => Dialog(
+ child: Padding(
+ padding: const EdgeInsets.all(20.0),
+ child: ListView(
+ shrinkWrap: true,
+ children: [
+ const Text(
+ 'Select an operation',
+ style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
+ ),
+ Padding(
+ padding: const EdgeInsets.symmetric(vertical: 8.0),
+ child: Text(
+ widget.characteristic.characteristicId.toString(),
+ ),
+ ),
+ divider,
+ ...readSection,
+ divider,
+ ...writeSection,
+ divider,
+ ...subscribeSection,
+ divider,
+ Align(
+ alignment: Alignment.bottomRight,
+ child: Padding(
+ padding: const EdgeInsets.only(top: 20.0),
+ child: ElevatedButton(
+ onPressed: () => Navigator.of(context).pop(),
+ child: const Text('close')),
+ ),
+ )
+ ],
+ ),
+ ),
+ );
+}
diff --git a/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_detail_screen.dart b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_detail_screen.dart
new file mode 100644
index 000000000..4a9df0b80
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_detail_screen.dart
@@ -0,0 +1,69 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
+import 'package:flutter_reactive_ble_example/src/ble/ble_device_connector.dart';
+import 'package:flutter_reactive_ble_example/src/ui/device_detail/device_log_tab.dart';
+import 'package:provider/provider.dart';
+
+import 'device_interaction_tab.dart';
+
+class DeviceDetailScreen extends StatelessWidget {
+ final DiscoveredDevice device;
+
+ const DeviceDetailScreen({required this.device, Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) => Consumer(
+ builder: (_, deviceConnector, __) => _DeviceDetail(
+ device: device,
+ disconnect: deviceConnector.disconnect,
+ ),
+ );
+}
+
+class _DeviceDetail extends StatelessWidget {
+ const _DeviceDetail({
+ required this.device,
+ required this.disconnect,
+ Key? key,
+ }) : super(key: key);
+
+ final DiscoveredDevice device;
+ final void Function(String deviceId) disconnect;
+ @override
+ Widget build(BuildContext context) => WillPopScope(
+ onWillPop: () async {
+ disconnect(device.id);
+ return true;
+ },
+ child: DefaultTabController(
+ length: 2,
+ child: Scaffold(
+ appBar: AppBar(
+ title: Text(device.name),
+ bottom: const TabBar(
+ tabs: [
+ Tab(
+ icon: Icon(
+ Icons.bluetooth_connected,
+ ),
+ ),
+ Tab(
+ icon: Icon(
+ Icons.find_in_page_sharp,
+ ),
+ ),
+ ],
+ ),
+ ),
+ body: TabBarView(
+ children: [
+ DeviceInteractionTab(
+ device: device,
+ ),
+ const DeviceLogTab(),
+ ],
+ ),
+ ),
+ ),
+ );
+}
diff --git a/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_interaction_tab.dart b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_interaction_tab.dart
new file mode 100644
index 000000000..4abe66d02
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_interaction_tab.dart
@@ -0,0 +1,284 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
+import 'package:flutter_reactive_ble_example/src/ble/ble_device_connector.dart';
+import 'package:flutter_reactive_ble_example/src/ble/ble_device_interactor.dart';
+import 'package:functional_data/functional_data.dart';
+import 'package:provider/provider.dart';
+
+import 'characteristic_interaction_dialog.dart';
+
+part 'device_interaction_tab.g.dart';
+//ignore_for_file: annotate_overrides
+
+class DeviceInteractionTab extends StatelessWidget {
+ final DiscoveredDevice device;
+
+ const DeviceInteractionTab({
+ required this.device,
+ Key? key,
+ }) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) =>
+ Consumer3(
+ builder: (_, deviceConnector, connectionStateUpdate, serviceDiscoverer,
+ __) =>
+ _DeviceInteractionTab(
+ viewModel: DeviceInteractionViewModel(
+ deviceId: device.id,
+ connectionStatus: connectionStateUpdate.connectionState,
+ deviceConnector: deviceConnector,
+ discoverServices: () =>
+ serviceDiscoverer.discoverServices(device.id)),
+ ),
+ );
+}
+
+@immutable
+@FunctionalData()
+class DeviceInteractionViewModel extends $DeviceInteractionViewModel {
+ const DeviceInteractionViewModel({
+ required this.deviceId,
+ required this.connectionStatus,
+ required this.deviceConnector,
+ required this.discoverServices,
+ });
+
+ final String deviceId;
+ final DeviceConnectionState connectionStatus;
+ final BleDeviceConnector deviceConnector;
+ @CustomEquality(Ignore())
+ final Future> Function() discoverServices;
+
+ bool get deviceConnected =>
+ connectionStatus == DeviceConnectionState.connected;
+
+ void connect() {
+ deviceConnector.connect(deviceId);
+ }
+
+ void disconnect() {
+ deviceConnector.disconnect(deviceId);
+ }
+}
+
+class _DeviceInteractionTab extends StatefulWidget {
+ const _DeviceInteractionTab({
+ required this.viewModel,
+ Key? key,
+ }) : super(key: key);
+
+ final DeviceInteractionViewModel viewModel;
+
+ @override
+ _DeviceInteractionTabState createState() => _DeviceInteractionTabState();
+}
+
+class _DeviceInteractionTabState extends State<_DeviceInteractionTab> {
+ late List discoveredServices;
+
+ @override
+ void initState() {
+ discoveredServices = [];
+ super.initState();
+ }
+
+ Future discoverServices() async {
+ final result = await widget.viewModel.discoverServices();
+ setState(() {
+ discoveredServices = result;
+ });
+ }
+
+ @override
+ Widget build(BuildContext context) => CustomScrollView(
+ slivers: [
+ SliverList(
+ delegate: SliverChildListDelegate.fixed(
+ [
+ Padding(
+ padding: const EdgeInsetsDirectional.only(
+ top: 8.0, bottom: 16.0, start: 16.0),
+ child: Text(
+ "ID: ${widget.viewModel.deviceId}",
+ style: const TextStyle(fontWeight: FontWeight.bold),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsetsDirectional.only(start: 16.0),
+ child: Text(
+ "Status: ${widget.viewModel.connectionStatus}",
+ style: const TextStyle(fontWeight: FontWeight.bold),
+ ),
+ ),
+ Padding(
+ padding: const EdgeInsets.only(top: 16.0),
+ child: Row(
+ mainAxisAlignment: MainAxisAlignment.spaceEvenly,
+ children: [
+ ElevatedButton(
+ onPressed: !widget.viewModel.deviceConnected
+ ? widget.viewModel.connect
+ : null,
+ child: const Text("Connect"),
+ ),
+ ElevatedButton(
+ onPressed: widget.viewModel.deviceConnected
+ ? widget.viewModel.disconnect
+ : null,
+ child: const Text("Disconnect"),
+ ),
+ ElevatedButton(
+ onPressed: widget.viewModel.deviceConnected
+ ? discoverServices
+ : null,
+ child: const Text("Discover Services"),
+ ),
+ ],
+ ),
+ ),
+ if (widget.viewModel.deviceConnected)
+ _ServiceDiscoveryList(
+ deviceId: widget.viewModel.deviceId,
+ discoveredServices: discoveredServices,
+ ),
+ ],
+ ),
+ ),
+ ],
+ );
+}
+
+class _ServiceDiscoveryList extends StatefulWidget {
+ const _ServiceDiscoveryList({
+ required this.deviceId,
+ required this.discoveredServices,
+ Key? key,
+ }) : super(key: key);
+
+ final String deviceId;
+ final List discoveredServices;
+
+ @override
+ _ServiceDiscoveryListState createState() => _ServiceDiscoveryListState();
+}
+
+class _ServiceDiscoveryListState extends State<_ServiceDiscoveryList> {
+ late final List _expandedItems;
+
+ @override
+ void initState() {
+ _expandedItems = [];
+ super.initState();
+ }
+
+ String _charactisticsSummary(DiscoveredCharacteristic c) {
+ final props = [];
+ if (c.isReadable) {
+ props.add("read");
+ }
+ if (c.isWritableWithoutResponse) {
+ props.add("write without response");
+ }
+ if (c.isWritableWithResponse) {
+ props.add("write with response");
+ }
+ if (c.isNotifiable) {
+ props.add("notify");
+ }
+ if (c.isIndicatable) {
+ props.add("indicate");
+ }
+
+ return props.join("\n");
+ }
+
+ Widget _characteristicTile(
+ DiscoveredCharacteristic characteristic, String deviceId) =>
+ ListTile(
+ onTap: () => showDialog(
+ context: context,
+ builder: (context) => CharacteristicInteractionDialog(
+ characteristic: QualifiedCharacteristic(
+ characteristicId: characteristic.characteristicId,
+ serviceId: characteristic.serviceId,
+ deviceId: deviceId),
+ )),
+ title: Text(
+ '${characteristic.characteristicId}\n(${_charactisticsSummary(characteristic)})',
+ style: const TextStyle(
+ fontSize: 14,
+ ),
+ ),
+ );
+
+ List buildPanels() {
+ final panels = [];
+
+ widget.discoveredServices.asMap().forEach(
+ (index, service) => panels.add(
+ ExpansionPanel(
+ body: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Padding(
+ padding: EdgeInsetsDirectional.only(start: 16.0),
+ child: Text(
+ 'Characteristics',
+ style: TextStyle(
+ fontWeight: FontWeight.bold,
+ ),
+ ),
+ ),
+ ListView.builder(
+ shrinkWrap: true,
+ itemBuilder: (context, index) => _characteristicTile(
+ service.characteristics[index],
+ widget.deviceId,
+ ),
+ itemCount: service.characteristicIds.length,
+ ),
+ ],
+ ),
+ headerBuilder: (context, isExpanded) => ListTile(
+ title: Text(
+ '${service.serviceId}',
+ style: const TextStyle(fontSize: 14),
+ ),
+ ),
+ isExpanded: _expandedItems.contains(index),
+ ),
+ ),
+ );
+
+ return panels;
+ }
+
+ @override
+ Widget build(BuildContext context) => widget.discoveredServices.isEmpty
+ ? const SizedBox()
+ : Padding(
+ padding: const EdgeInsetsDirectional.only(
+ top: 20.0,
+ start: 20.0,
+ end: 20.0,
+ ),
+ child: ExpansionPanelList(
+ expansionCallback: (int index, bool isExpanded) {
+ setState(() {
+ setState(() {
+ if (isExpanded) {
+ _expandedItems.remove(index);
+ } else {
+ _expandedItems.add(index);
+ }
+ });
+ });
+ },
+ children: [
+ ...buildPanels(),
+ ],
+ ),
+ );
+}
diff --git a/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_interaction_tab.g.dart b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_interaction_tab.g.dart
new file mode 100644
index 000000000..994c9ccee
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_interaction_tab.g.dart
@@ -0,0 +1,115 @@
+// GENERATED CODE - DO NOT MODIFY BY HAND
+
+part of 'device_interaction_tab.dart';
+
+// **************************************************************************
+// FunctionalDataGenerator
+// **************************************************************************
+
+abstract class $DeviceInteractionViewModel {
+ const $DeviceInteractionViewModel();
+
+ String get deviceId;
+ DeviceConnectionState get connectionStatus;
+ BleDeviceConnector get deviceConnector;
+ Future> Function() get discoverServices;
+
+ DeviceInteractionViewModel copyWith({
+ String? deviceId,
+ DeviceConnectionState? connectionStatus,
+ BleDeviceConnector? deviceConnector,
+ Future> Function()? discoverServices,
+ }) =>
+ DeviceInteractionViewModel(
+ deviceId: deviceId ?? this.deviceId,
+ connectionStatus: connectionStatus ?? this.connectionStatus,
+ deviceConnector: deviceConnector ?? this.deviceConnector,
+ discoverServices: discoverServices ?? this.discoverServices,
+ );
+
+ DeviceInteractionViewModel copyUsing(
+ void Function(DeviceInteractionViewModel$Change change) mutator) {
+ final change = DeviceInteractionViewModel$Change._(
+ this.deviceId,
+ this.connectionStatus,
+ this.deviceConnector,
+ this.discoverServices,
+ );
+ mutator(change);
+ return DeviceInteractionViewModel(
+ deviceId: change.deviceId,
+ connectionStatus: change.connectionStatus,
+ deviceConnector: change.deviceConnector,
+ discoverServices: change.discoverServices,
+ );
+ }
+
+ @override
+ String toString() =>
+ "DeviceInteractionViewModel(deviceId: $deviceId, connectionStatus: $connectionStatus, deviceConnector: $deviceConnector, discoverServices: $discoverServices)";
+
+ @override
+ // ignore: avoid_equals_and_hash_code_on_mutable_classes
+ bool operator ==(Object other) =>
+ other is DeviceInteractionViewModel &&
+ other.runtimeType == runtimeType &&
+ deviceId == other.deviceId &&
+ connectionStatus == other.connectionStatus &&
+ deviceConnector == other.deviceConnector &&
+ const Ignore().equals(discoverServices, other.discoverServices);
+
+ @override
+ // ignore: avoid_equals_and_hash_code_on_mutable_classes
+ int get hashCode {
+ var result = 17;
+ result = 37 * result + deviceId.hashCode;
+ result = 37 * result + connectionStatus.hashCode;
+ result = 37 * result + deviceConnector.hashCode;
+ result = 37 * result + const Ignore().hash(discoverServices);
+ return result;
+ }
+}
+
+class DeviceInteractionViewModel$Change {
+ DeviceInteractionViewModel$Change._(
+ this.deviceId,
+ this.connectionStatus,
+ this.deviceConnector,
+ this.discoverServices,
+ );
+
+ String deviceId;
+ DeviceConnectionState connectionStatus;
+ BleDeviceConnector deviceConnector;
+ Future> Function() discoverServices;
+}
+
+// ignore: avoid_classes_with_only_static_members
+class DeviceInteractionViewModel$ {
+ static final deviceId = Lens(
+ (deviceIdContainer) => deviceIdContainer.deviceId,
+ (deviceIdContainer, deviceId) =>
+ deviceIdContainer.copyWith(deviceId: deviceId),
+ );
+
+ static final connectionStatus =
+ Lens(
+ (connectionStatusContainer) => connectionStatusContainer.connectionStatus,
+ (connectionStatusContainer, connectionStatus) =>
+ connectionStatusContainer.copyWith(connectionStatus: connectionStatus),
+ );
+
+ static final deviceConnector =
+ Lens(
+ (deviceConnectorContainer) => deviceConnectorContainer.deviceConnector,
+ (deviceConnectorContainer, deviceConnector) =>
+ deviceConnectorContainer.copyWith(deviceConnector: deviceConnector),
+ );
+
+ static final discoverServices = Lens> Function()>(
+ (discoverServicesContainer) => discoverServicesContainer.discoverServices,
+ (discoverServicesContainer, discoverServices) =>
+ discoverServicesContainer.copyWith(discoverServices: discoverServices),
+ );
+}
diff --git a/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_log_tab.dart b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_log_tab.dart
new file mode 100644
index 000000000..08ab45dd5
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/lib/src/ui/device_detail/device_log_tab.dart
@@ -0,0 +1,32 @@
+import 'package:flutter/widgets.dart';
+import 'package:flutter_reactive_ble_example/src/ble/ble_logger.dart';
+import 'package:provider/provider.dart';
+
+class DeviceLogTab extends StatelessWidget {
+ const DeviceLogTab({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) => Consumer(
+ builder: (context, logger, _) => _DeviceLogTab(
+ messages: logger.messages,
+ ),
+ );
+}
+
+class _DeviceLogTab extends StatelessWidget {
+ const _DeviceLogTab({
+ required this.messages,
+ Key? key,
+ }) : super(key: key);
+
+ final List messages;
+
+ @override
+ Widget build(BuildContext context) => Padding(
+ padding: const EdgeInsets.all(16.0),
+ child: ListView.builder(
+ itemBuilder: (context, index) => Text(messages[index]),
+ itemCount: messages.length,
+ ),
+ );
+}
diff --git a/packages/flutter_reactive_ble/example/lib/src/ui/device_list.dart b/packages/flutter_reactive_ble/example/lib/src/ui/device_list.dart
new file mode 100644
index 000000000..a3fb58212
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/lib/src/ui/device_list.dart
@@ -0,0 +1,176 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_reactive_ble/flutter_reactive_ble.dart';
+import 'package:flutter_reactive_ble_example/src/ble/ble_scanner.dart';
+import 'package:provider/provider.dart';
+
+import '../ble/ble_logger.dart';
+import '../widgets.dart';
+import 'device_detail/device_detail_screen.dart';
+
+class DeviceListScreen extends StatelessWidget {
+ const DeviceListScreen({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) =>
+ Consumer3(
+ builder: (_, bleScanner, bleScannerState, bleLogger, __) => _DeviceList(
+ scannerState: bleScannerState ??
+ const BleScannerState(
+ discoveredDevices: [],
+ scanIsInProgress: false,
+ ),
+ startScan: bleScanner.startScan,
+ stopScan: bleScanner.stopScan,
+ toggleVerboseLogging: bleLogger.toggleVerboseLogging,
+ verboseLogging: bleLogger.verboseLogging,
+ ),
+ );
+}
+
+class _DeviceList extends StatefulWidget {
+ const _DeviceList({
+ required this.scannerState,
+ required this.startScan,
+ required this.stopScan,
+ required this.toggleVerboseLogging,
+ required this.verboseLogging,
+ });
+
+ final BleScannerState scannerState;
+ final void Function(List) startScan;
+ final VoidCallback stopScan;
+ final VoidCallback toggleVerboseLogging;
+ final bool verboseLogging;
+
+ @override
+ _DeviceListState createState() => _DeviceListState();
+}
+
+class _DeviceListState extends State<_DeviceList> {
+ late TextEditingController _uuidController;
+
+ @override
+ void initState() {
+ super.initState();
+ _uuidController = TextEditingController()
+ ..addListener(() => setState(() {}));
+ }
+
+ @override
+ void dispose() {
+ widget.stopScan();
+ _uuidController.dispose();
+ super.dispose();
+ }
+
+ bool _isValidUuidInput() {
+ final uuidText = _uuidController.text;
+ if (uuidText.isEmpty) {
+ return true;
+ } else {
+ try {
+ Uuid.parse(uuidText);
+ return true;
+ } on Exception {
+ return false;
+ }
+ }
+ }
+
+ void _startScanning() {
+ final text = _uuidController.text;
+ widget.startScan(text.isEmpty ? [] : [Uuid.parse(_uuidController.text)]);
+ }
+
+ @override
+ Widget build(BuildContext context) => Scaffold(
+ appBar: AppBar(
+ title: const Text('Scan for devices'),
+ ),
+ body: Column(
+ children: [
+ Padding(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ child: Column(
+ crossAxisAlignment: CrossAxisAlignment.start,
+ children: [
+ const SizedBox(height: 16),
+ const Text('Service UUID (2, 4, 16 bytes):'),
+ TextField(
+ controller: _uuidController,
+ enabled: !widget.scannerState.scanIsInProgress,
+ decoration: InputDecoration(
+ errorText:
+ _uuidController.text.isEmpty || _isValidUuidInput()
+ ? null
+ : 'Invalid UUID format'),
+ autocorrect: false,
+ ),
+ const SizedBox(height: 16),
+ Row(
+ mainAxisAlignment: MainAxisAlignment.spaceBetween,
+ children: [
+ ElevatedButton(
+ child: const Text('Scan'),
+ onPressed: !widget.scannerState.scanIsInProgress &&
+ _isValidUuidInput()
+ ? _startScanning
+ : null,
+ ),
+ ElevatedButton(
+ child: const Text('Stop'),
+ onPressed: widget.scannerState.scanIsInProgress
+ ? widget.stopScan
+ : null,
+ ),
+ ],
+ ),
+ ],
+ ),
+ ),
+ const SizedBox(height: 8),
+ Flexible(
+ child: ListView(
+ children: [
+ SwitchListTile(
+ title: const Text("Verbose logging"),
+ value: widget.verboseLogging,
+ onChanged: (_) => setState(widget.toggleVerboseLogging),
+ ),
+ ListTile(
+ title: Text(
+ !widget.scannerState.scanIsInProgress
+ ? 'Enter a UUID above and tap start to begin scanning'
+ : 'Tap a device to connect to it',
+ ),
+ trailing: (widget.scannerState.scanIsInProgress ||
+ widget.scannerState.discoveredDevices.isNotEmpty)
+ ? Text(
+ 'count: ${widget.scannerState.discoveredDevices.length}',
+ )
+ : null,
+ ),
+ ...widget.scannerState.discoveredDevices
+ .map(
+ (device) => ListTile(
+ title: Text(device.name),
+ subtitle: Text("${device.id}\nRSSI: ${device.rssi}"),
+ leading: const BluetoothIcon(),
+ onTap: () async {
+ widget.stopScan();
+ await Navigator.push(
+ context,
+ MaterialPageRoute(
+ builder: (_) =>
+ DeviceDetailScreen(device: device)));
+ },
+ ),
+ )
+ .toList(),
+ ],
+ ),
+ ),
+ ],
+ ),
+ );
+}
diff --git a/packages/flutter_reactive_ble/example/lib/src/utils.dart b/packages/flutter_reactive_ble/example/lib/src/utils.dart
new file mode 100644
index 000000000..a0293a035
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/lib/src/utils.dart
@@ -0,0 +1,2 @@
+// ignore: avoid_print
+void log(String text) => print("[FlutterReactiveBLEApp] $text");
diff --git a/packages/flutter_reactive_ble/example/lib/src/widgets.dart b/packages/flutter_reactive_ble/example/lib/src/widgets.dart
new file mode 100644
index 000000000..a6ec0672a
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/lib/src/widgets.dart
@@ -0,0 +1,27 @@
+import 'package:flutter/material.dart';
+
+class BluetoothIcon extends StatelessWidget {
+ const BluetoothIcon({Key? key}) : super(key: key);
+
+ @override
+ Widget build(BuildContext context) => const SizedBox(
+ width: 64,
+ height: 64,
+ child: Align(alignment: Alignment.center, child: Icon(Icons.bluetooth)),
+ );
+}
+
+class StatusMessage extends StatelessWidget {
+ const StatusMessage({
+ required this.text,
+ Key? key,
+ }) : super(key: key);
+
+ final String text;
+
+ @override
+ Widget build(BuildContext context) => Padding(
+ padding: const EdgeInsets.symmetric(vertical: 20),
+ child: Text(text, style: const TextStyle(fontWeight: FontWeight.bold)),
+ );
+}
diff --git a/packages/flutter_reactive_ble/example/pubspec.yaml b/packages/flutter_reactive_ble/example/pubspec.yaml
new file mode 100644
index 000000000..cefda07dd
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/pubspec.yaml
@@ -0,0 +1,18 @@
+name: flutter_reactive_ble_example
+description: Demonstrates how to use the flutter_reactive_ble_tizen plugin.
+publish_to: none
+
+environment:
+ sdk: ">=2.18.0 <3.0.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+ flutter_reactive_ble: ^5.0.3
+ flutter_reactive_ble_tizen:
+ path: ../
+ intl: ^0.17.0
+ provider: ^6.0.4
+
+flutter:
+ uses-material-design: true
diff --git a/packages/flutter_reactive_ble/example/tizen/.gitignore b/packages/flutter_reactive_ble/example/tizen/.gitignore
new file mode 100644
index 000000000..750f3af1b
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/tizen/.gitignore
@@ -0,0 +1,5 @@
+flutter/
+.vs/
+*.user
+bin/
+obj/
diff --git a/packages/flutter_reactive_ble/example/tizen/App.cs b/packages/flutter_reactive_ble/example/tizen/App.cs
new file mode 100644
index 000000000..6dd4a6356
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/tizen/App.cs
@@ -0,0 +1,20 @@
+using Tizen.Flutter.Embedding;
+
+namespace Runner
+{
+ public class App : FlutterApplication
+ {
+ protected override void OnCreate()
+ {
+ base.OnCreate();
+
+ GeneratedPluginRegistrant.RegisterPlugins(this);
+ }
+
+ static void Main(string[] args)
+ {
+ var app = new App();
+ app.Run(args);
+ }
+ }
+}
diff --git a/packages/flutter_reactive_ble/example/tizen/Runner.csproj b/packages/flutter_reactive_ble/example/tizen/Runner.csproj
new file mode 100644
index 000000000..f4e369d0c
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/tizen/Runner.csproj
@@ -0,0 +1,19 @@
+
+
+
+ Exe
+ tizen40
+
+
+
+
+
+
+
+
+
+ %(RecursiveDir)
+
+
+
+
diff --git a/packages/flutter_reactive_ble/example/tizen/shared/res/ic_launcher.png b/packages/flutter_reactive_ble/example/tizen/shared/res/ic_launcher.png
new file mode 100644
index 000000000..4d6372eeb
Binary files /dev/null and b/packages/flutter_reactive_ble/example/tizen/shared/res/ic_launcher.png differ
diff --git a/packages/flutter_reactive_ble/example/tizen/tizen-manifest.xml b/packages/flutter_reactive_ble/example/tizen/tizen-manifest.xml
new file mode 100644
index 000000000..ca17f5dc8
--- /dev/null
+++ b/packages/flutter_reactive_ble/example/tizen/tizen-manifest.xml
@@ -0,0 +1,13 @@
+
+
+
+
+
+ ic_launcher.png
+
+
+
+
+ http://tizen.org/privilege/bluetooth
+
+
diff --git a/packages/flutter_reactive_ble/lib/flutter_reactive_ble_tizen.dart b/packages/flutter_reactive_ble/lib/flutter_reactive_ble_tizen.dart
new file mode 100644
index 000000000..c4d45ea91
--- /dev/null
+++ b/packages/flutter_reactive_ble/lib/flutter_reactive_ble_tizen.dart
@@ -0,0 +1,173 @@
+// Copyright 2023 Samsung Electronics Co., Ltd. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+// ignore_for_file: prefer_expression_function_bodies
+
+import 'dart:async';
+
+import 'package:flutter/material.dart';
+import 'package:flutter/services.dart';
+import 'package:reactive_ble_platform_interface/reactive_ble_platform_interface.dart';
+
+import 'src/models.dart';
+
+/// Tizen implementation of [ReactiveBlePlatform].
+class ReactiveBleTizen extends ReactiveBlePlatform {
+ /// Registers this class as the default instance of [ReactiveBlePlatform].
+ static void register() {
+ ReactiveBlePlatform.instance = ReactiveBleTizen();
+ }
+
+ @visibleForTesting
+ final MethodChannel methodChannel =
+ const MethodChannel('flutter_reactive_ble_method');
+
+ @override
+ final Stream bleStatusStream =
+ const EventChannel('flutter_reactive_ble_status')
+ .receiveBroadcastStream()
+ .map((dynamic event) => BleStatus.values.byName(event as String));
+
+ @override
+ final Stream scanStream = const EventChannel(
+ 'flutter_reactive_ble_scan')
+ .receiveBroadcastStream()
+ .map((dynamic event) =>
+ ScanResultExtension.fromMap((event as Map).cast()));
+
+ @override
+ final Stream connectionUpdateStream =
+ const EventChannel('flutter_reactive_ble_connected_device')
+ .receiveBroadcastStream()
+ .map((dynamic event) => ConnectionStateUpdateExtension.fromMap(
+ (event as Map).cast()));
+
+ @override
+ final Stream charValueUpdateStream =
+ const EventChannel('flutter_reactive_ble_char_update')
+ .receiveBroadcastStream()
+ .map((dynamic event) => CharacteristicValueExtension.fromMap(
+ (event as Map).cast()));
+
+ @override
+ Future initialize() => methodChannel.invokeMethod('initialize');
+
+ @override
+ Future deinitialize() => methodChannel.invokeMethod('deinitialize');
+
+ @override
+ Stream scanForDevices({
+ required List withServices,
+ required ScanMode scanMode, // ignored
+ required bool requireLocationServicesEnabled, // ignored
+ }) {
+ return methodChannel.invokeMethod(
+ 'scanForDevices',
+ {
+ kServiceIds: withServices.map((Uuid uuid) => '$uuid').toList(),
+ },
+ ).asStream();
+ }
+
+ @override
+ Stream connectToDevice(
+ String id,
+ Map>? servicesWithCharacteristicsToDiscover,
+ Duration? connectionTimeout,
+ ) {
+ // TODO(swift-kim): Support the connectionTimeout parameter.
+ return methodChannel.invokeMethod(
+ 'connectToDevice',
+ {kDeviceId: id},
+ ).asStream();
+ }
+
+ @override
+ Future disconnectDevice(String deviceId) => methodChannel
+ .invokeMethod('disconnectDevice', {kDeviceId: deviceId});
+
+ @override
+ Future> discoverServices(String deviceId) async {
+ final result = await methodChannel.invokeListMethod