From a56d89c872684087e73fe15d6a64546051b48887 Mon Sep 17 00:00:00 2001 From: Jhonacode Date: Thu, 19 Dec 2024 15:43:30 +1300 Subject: [PATCH 1/4] Some improvements on example, reactive async, protected, etc. --- .gitignore | 1 + example/reactive_notifier_example.dart | 145 ++---------------- example/service/connection_service.dart | 7 + .../viewmodel/connection_state_viewmodel.dart | 129 ++++++++++++++++ lib/reactive_notifier.dart | 1 - lib/src/builder/reactive_async_builder.dart | 121 ++++++++++++++- lib/src/builder/reactive_builder.dart | 33 ++-- lib/src/builder/reactive_future_builder.dart | 85 ---------- lib/src/builder/reactive_stream_builder.dart | 4 +- lib/src/handler/async_state.dart | 2 +- lib/src/implements/notifier_impl.dart | 70 ++++----- lib/src/reactive_notifier.dart | 29 ++-- lib/src/viewmodel/async_viewmodel_impl.dart | 74 --------- lib/src/viewmodel/viewmodel_impl.dart | 6 + pubspec.yaml | 2 +- test/circular_references_test.dart | 8 +- test/golden_reactive_builder.test.dart | 6 +- test/reactive_notifier_test.dart | 120 +++++++-------- 18 files changed, 396 insertions(+), 447 deletions(-) create mode 100644 example/service/connection_service.dart create mode 100644 example/viewmodel/connection_state_viewmodel.dart delete mode 100644 lib/src/builder/reactive_future_builder.dart delete mode 100644 lib/src/viewmodel/async_viewmodel_impl.dart diff --git a/.gitignore b/.gitignore index ac5aa98..0ec8206 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ migrate_working_dir/ **/doc/api/ .dart_tool/ build/ +/example/app/ diff --git a/example/reactive_notifier_example.dart b/example/reactive_notifier_example.dart index 018e58f..f12dd31 100644 --- a/example/reactive_notifier_example.dart +++ b/example/reactive_notifier_example.dart @@ -1,143 +1,19 @@ -import 'dart:async'; -import 'dart:math'; - -import 'package:flutter/material.dart'; +import 'package:flutter/material.dart' hide ConnectionState; import 'package:reactive_notifier/reactive_notifier.dart'; -enum ConnectionState { - connected, - disconnected, - connecting, - error, - uploading, - waiting, - offline, - syncError, - syncing, - synced, - pendingSync -} - -extension ConnectionStateX on ConnectionState { - bool get isConnected => this == ConnectionState.connected; - bool get isError => - this == ConnectionState.error || this == ConnectionState.syncError; - bool get isSyncing => - this == ConnectionState.syncing || this == ConnectionState.pendingSync; - - IconData get icon { - return switch (this) { - ConnectionState.connected => Icons.cloud_done, - ConnectionState.disconnected => Icons.cloud_off, - ConnectionState.connecting => Icons.cloud_sync, - ConnectionState.error => Icons.error_outline, - ConnectionState.uploading => Icons.upload, - ConnectionState.waiting => Icons.hourglass_empty, - ConnectionState.offline => Icons.signal_wifi_off, - ConnectionState.syncError => Icons.sync_problem, - ConnectionState.syncing => Icons.sync, - ConnectionState.synced => Icons.sync_alt, - ConnectionState.pendingSync => Icons.pending, - }; - } - - Color get color { - return switch (this) { - ConnectionState.connected => Colors.green, - ConnectionState.synced => Colors.lightGreen, - ConnectionState.uploading || - ConnectionState.syncing || - ConnectionState.connecting => - Colors.blue, - ConnectionState.waiting || ConnectionState.pendingSync => Colors.orange, - _ => Colors.red, - }; - } - - String get message { - return switch (this) { - ConnectionState.connected => 'Connected to server', - ConnectionState.disconnected => 'Connection lost', - ConnectionState.connecting => 'Establishing connection...', - ConnectionState.error => 'Connection error', - ConnectionState.uploading => 'Uploading data...', - ConnectionState.waiting => 'Waiting for connection', - ConnectionState.offline => 'Device offline', - ConnectionState.syncError => 'Sync failed', - ConnectionState.syncing => 'Syncing data...', - ConnectionState.synced => 'Data synchronized', - ConnectionState.pendingSync => 'Pending sync', - }; - } -} - -class ConnectionManager extends ViewModelStateImpl { - ConnectionManager() : super(ConnectionState.offline); - - Timer? _reconnectTimer; - bool _isReconnecting = false; - - @override - void init() { - simulateNetworkConditions(); - } - - void simulateNetworkConditions() { - _reconnectTimer?.cancel(); - _reconnectTimer = Timer.periodic(const Duration(seconds: 3), (timer) { - if (_isReconnecting) return; - _simulateStateChange(); - }); - } - - Future _simulateStateChange() async { - _isReconnecting = true; - - try { - updateState(ConnectionState.connecting); - await Future.delayed(const Duration(seconds: 1)); - - if (Random().nextDouble() < 0.7) { - updateState(ConnectionState.connected); - await Future.delayed(const Duration(seconds: 1)); - updateState(ConnectionState.syncing); - await Future.delayed(const Duration(seconds: 1)); - updateState(ConnectionState.synced); - } else { - if (Random().nextBool()) { - updateState(ConnectionState.error); - } else { - updateState(ConnectionState.syncError); - } - } - } finally { - _isReconnecting = false; - } - } - - void manualReconnect() { - if (!_isReconnecting) _simulateStateChange(); - } - - @override - void dispose() { - _reconnectTimer?.cancel(); - super.dispose(); - } -} - -final connectionManager = - ReactiveNotifier(() => ConnectionManager()); +import 'service/connection_service.dart'; +import 'viewmodel/connection_state_viewmodel.dart'; class ConnectionStateWidget extends StatelessWidget { const ConnectionStateWidget({super.key}); @override Widget build(BuildContext context) { - return ReactiveBuilder( - valueListenable: connectionManager.value, - builder: (context, manager, keep) { - final state = manager; + return ReactiveBuilder( + valueListenable: ConnectionService.instance, + builder: ( service, keep) { + + final state = service.notifier; return Card( elevation: 4, @@ -148,7 +24,7 @@ class ConnectionStateWidget extends StatelessWidget { children: [ CircleAvatar( radius: 30, - backgroundColor: state.color.withOpacity(0.2), + backgroundColor: state.color.withValues(alpha: 255 * 0.2), child: Icon( state.icon, color: state.color, @@ -164,8 +40,7 @@ class ConnectionStateWidget extends StatelessWidget { if (state.isError || state == ConnectionState.disconnected) keep( ElevatedButton.icon( - onPressed: () => - connectionManager.value.manualReconnect(), + onPressed: () => service.manualReconnect(), icon: const Icon(Icons.refresh), label: const Text('Retry Connection'), ), diff --git a/example/service/connection_service.dart b/example/service/connection_service.dart new file mode 100644 index 0000000..cf19442 --- /dev/null +++ b/example/service/connection_service.dart @@ -0,0 +1,7 @@ +import 'package:reactive_notifier/reactive_notifier.dart'; + +import '../viewmodel/connection_state_viewmodel.dart'; + +mixin ConnectionService{ + static final ReactiveNotifier instance = ReactiveNotifier(() => ConnectionManager()); +} \ No newline at end of file diff --git a/example/viewmodel/connection_state_viewmodel.dart b/example/viewmodel/connection_state_viewmodel.dart new file mode 100644 index 0000000..a791fb3 --- /dev/null +++ b/example/viewmodel/connection_state_viewmodel.dart @@ -0,0 +1,129 @@ +import 'dart:async'; +import 'dart:math'; + +import 'package:flutter/material.dart'; +import 'package:reactive_notifier/reactive_notifier.dart'; + +class ConnectionManager extends ViewModelStateImpl { + ConnectionManager() : super(ConnectionState.offline); + + Timer? _reconnectTimer; + bool _isReconnecting = false; + + @override + void init() { + simulateNetworkConditions(); + } + + void simulateNetworkConditions() { + _reconnectTimer?.cancel(); + _reconnectTimer = Timer.periodic(const Duration(seconds: 3), (timer) { + if (_isReconnecting) return; + _simulateStateChange(); + }); + } + + Future _simulateStateChange() async { + _isReconnecting = true; + + try { + updateState(ConnectionState.connecting); + await Future.delayed(const Duration(seconds: 1)); + + if (Random().nextDouble() < 0.7) { + updateState(ConnectionState.connected); + await Future.delayed(const Duration(seconds: 1)); + updateState(ConnectionState.syncing); + await Future.delayed(const Duration(seconds: 1)); + updateState(ConnectionState.synced); + } else { + if (Random().nextBool()) { + updateState(ConnectionState.error); + } else { + updateState(ConnectionState.syncError); + } + } + } finally { + _isReconnecting = false; + } + } + + void manualReconnect() { + if (!_isReconnecting) _simulateStateChange(); + } + + @override + void dispose() { + _reconnectTimer?.cancel(); + super.dispose(); + } + + +} + +enum ConnectionState { + connected, + disconnected, + connecting, + error, + uploading, + waiting, + offline, + syncError, + syncing, + synced, + pendingSync +} + +extension ConnectionStateX on ConnectionState { + bool get isConnected => this == ConnectionState.connected; + bool get isError => + this == ConnectionState.error || this == ConnectionState.syncError; + bool get isSyncing => + this == ConnectionState.syncing || this == ConnectionState.pendingSync; + + IconData get icon { + return switch (this) { + ConnectionState.connected => Icons.cloud_done, + ConnectionState.disconnected => Icons.cloud_off, + ConnectionState.connecting => Icons.cloud_sync, + ConnectionState.error => Icons.error_outline, + ConnectionState.uploading => Icons.upload, + ConnectionState.waiting => Icons.hourglass_empty, + ConnectionState.offline => Icons.signal_wifi_off, + ConnectionState.syncError => Icons.sync_problem, + ConnectionState.syncing => Icons.sync, + ConnectionState.synced => Icons.sync_alt, + ConnectionState.pendingSync => Icons.pending, + }; + } + + Color get color { + return switch (this) { + ConnectionState.connected => Colors.green, + ConnectionState.synced => Colors.lightGreen, + ConnectionState.uploading || + ConnectionState.syncing || + ConnectionState.connecting => + Colors.blue, + ConnectionState.waiting || ConnectionState.pendingSync => Colors.orange, + _ => Colors.red, + }; + } + + String get message { + return switch (this) { + ConnectionState.connected => 'Connected to server', + ConnectionState.disconnected => 'Connection lost', + ConnectionState.connecting => 'Establishing connection...', + ConnectionState.error => 'Connection error', + ConnectionState.uploading => 'Uploading data...', + ConnectionState.waiting => 'Waiting for connection', + ConnectionState.offline => 'Device offline', + ConnectionState.syncError => 'Sync failed', + ConnectionState.syncing => 'Syncing data...', + ConnectionState.synced => 'Data synchronized', + ConnectionState.pendingSync => 'Pending sync', + }; + } +} \ No newline at end of file diff --git a/lib/reactive_notifier.dart b/lib/reactive_notifier.dart index cb59aa6..c5c1da9 100644 --- a/lib/reactive_notifier.dart +++ b/lib/reactive_notifier.dart @@ -21,7 +21,6 @@ export 'package:reactive_notifier/src/builder/reactive_async_builder.dart'; export 'package:reactive_notifier/src/builder/reactive_stream_builder.dart'; /// Export ViewModelImpl -export 'package:reactive_notifier/src/viewmodel/async_viewmodel_impl.dart'; export 'package:reactive_notifier/src/viewmodel/viewmodel_impl.dart'; /// Export RepositoryImpl diff --git a/lib/src/builder/reactive_async_builder.dart b/lib/src/builder/reactive_async_builder.dart index 1dbd21c..9ec0fda 100644 --- a/lib/src/builder/reactive_async_builder.dart +++ b/lib/src/builder/reactive_async_builder.dart @@ -1,5 +1,6 @@ +import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; -import 'package:reactive_notifier/src/viewmodel/async_viewmodel_impl.dart'; +import 'package:reactive_notifier/src/handler/async_state.dart'; class ReactiveAsyncBuilder extends StatelessWidget { final AsyncViewModelImpl viewModel; @@ -22,17 +23,127 @@ class ReactiveAsyncBuilder extends StatelessWidget { return AnimatedBuilder( animation: viewModel, builder: (context, _) { - return viewModel.value.when( + return viewModel.when( initial: () => buildInitial?.call() ?? const SizedBox.shrink(), loading: () => buildLoading?.call() ?? const Center(child: CircularProgressIndicator.adaptive()), success: (data) => buildSuccess(data), - error: (error, stackTrace) => - buildError?.call(error, stackTrace) ?? - Center(child: Text('Error: $error')), + error: (error, stackTrace) => buildError != null ? buildError!(error, stackTrace) : Center(child: Text('Error: $error')), ); }, ); } } + + +/// Base ViewModel implementation for handling asynchronous operations with state management. +/// +/// Provides a standardized way to handle loading, success, and error states for async data. + +/// Base ViewModel implementation for handling asynchronous operations with state management. +abstract class AsyncViewModelImpl extends ChangeNotifier { + + late AsyncState _state; + late bool loadOnInit; + + AsyncViewModelImpl(this._state,{ this.loadOnInit = true }) :super() { + + if (kFlutterMemoryAllocationsEnabled) { + ChangeNotifier.maybeDispatchObjectCreation(this); + } + + if (loadOnInit) { + _initializeAsync(); + } + + } + + /// Internal initialization method that properly handles async initialization + Future _initializeAsync() async { + try { + await reload(); + } catch (error, stackTrace) { + errorState(error, stackTrace); + } + } + + /// Public method to reload data + @protected + Future reload() async { + loadOnInit = false; + if (_state.isLoading) return; + + loadingState(); + try { + final result = await loadData(); + updateState(result); + } catch (error, stackTrace) { + errorState(error, stackTrace); + } + } + + /// Override this method to provide the async data loading logic + @protected + Future loadData(); + + /// Update data directly + + @protected + void updateState(T data) { + _state = AsyncState.success(data); + notifyListeners(); + } + + @protected + void loadingState(){ + _state = AsyncState.loading(); + notifyListeners(); + } + + /// Set error state + + @protected + void errorState(Object error, [StackTrace? stackTrace]) { + _state = AsyncState.error(error, stackTrace); + notifyListeners(); + } + + @protected + void cleanState(){ + _state = AsyncState.initial(); + } + + + + /// Check if any operation is in progress + bool get isLoading => _state.isLoading; + + /// Get current error if any + Object? get error => _state.error; + + /// Get current stack trace if there's an error + StackTrace? get stackTrace => _state.stackTrace; + + /// Check if the state contains valid data + bool get hasData => _state.isSuccess; + + /// Get the current data (may be null if not in success state) + T? get data => _state.isSuccess ? _state.data : null; + + @protected + R when({ + required R Function() initial, + required R Function() loading, + required R Function(T data) success, + required R Function(Object? err, StackTrace? stackTrace) error, + }){ + return _state.when( + initial: initial, + loading: loading, + success: (infoData) => success(infoData), + error: error, + ); + } + +} \ No newline at end of file diff --git a/lib/src/builder/reactive_builder.dart b/lib/src/builder/reactive_builder.dart index d64904d..c0f5731 100644 --- a/lib/src/builder/reactive_builder.dart +++ b/lib/src/builder/reactive_builder.dart @@ -2,13 +2,13 @@ import 'dart:async'; import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; +import 'package:reactive_notifier/src/implements/notifier_impl.dart'; import 'package:reactive_notifier/src/reactive_notifier.dart'; class ReactiveBuilder extends StatefulWidget { - final ValueListenable valueListenable; + final NotifierImpl valueListenable; final Widget Function( - BuildContext context, - T value, + T state, Widget Function(Widget child) keep, ) builder; @@ -18,20 +18,6 @@ class ReactiveBuilder extends StatefulWidget { required this.builder, }); - /// Recommended constructor for handling simple states. - const ReactiveBuilder.notifier({ - Key? key, - required ReactiveNotifier notifier, - required Widget Function( - BuildContext context, - T value, - Widget Function(Widget child) keep, - ) builder, - }) : this( - key: key, - valueListenable: notifier, - builder: builder, - ); @override State> createState() => _ReactiveBuilderState(); @@ -45,7 +31,7 @@ class _ReactiveBuilderState extends State> { @override void initState() { super.initState(); - value = widget.valueListenable.value; + value = widget.valueListenable.notifier; widget.valueListenable.addListener(_valueChanged); } @@ -54,7 +40,7 @@ class _ReactiveBuilderState extends State> { super.didUpdateWidget(oldWidget); if (oldWidget.valueListenable != widget.valueListenable) { oldWidget.valueListenable.removeListener(_valueChanged); - value = widget.valueListenable.value; + value = widget.valueListenable.notifier; widget.valueListenable.addListener(_valueChanged); } } @@ -74,12 +60,12 @@ class _ReactiveBuilderState extends State> { if (!isTesting) { debounceTimer = Timer(const Duration(milliseconds: 100), () { setState(() { - value = widget.valueListenable.value; + value = widget.valueListenable.notifier; }); }); } else { setState(() { - value = widget.valueListenable.value; + value = widget.valueListenable.notifier; }); } } @@ -94,7 +80,7 @@ class _ReactiveBuilderState extends State> { @override Widget build(BuildContext context) { - return widget.builder(context, value, _noRebuild); + return widget.builder( value, _noRebuild); } } @@ -121,3 +107,6 @@ class _NoRebuildWrapperState extends State<_NoRebuildWrapper> { } bool get isTesting => const bool.fromEnvironment('dart.vm.product') == true; + + + diff --git a/lib/src/builder/reactive_future_builder.dart b/lib/src/builder/reactive_future_builder.dart deleted file mode 100644 index 2013b84..0000000 --- a/lib/src/builder/reactive_future_builder.dart +++ /dev/null @@ -1,85 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:reactive_notifier/reactive_notifier.dart'; - -class ReactiveFutureBuilder extends StatefulWidget { - final Future Function() futureBuilder; - final Widget Function(T data)? onData; - final Widget Function(Object error, StackTrace? stackTrace) onError; - final Widget Function()? onLoading; - final bool autoLoad; - - const ReactiveFutureBuilder({ - super.key, - required this.futureBuilder, - this.onData, - required this.onError, - this.onLoading, - this.autoLoad = true, - }); - - @override - _ReactiveFutureBuilderState createState() => _ReactiveFutureBuilderState(); -} - -class _ReactiveFutureBuilderState extends State> { - AsyncState _state = AsyncState.initial(); - bool _isMounted = false; - - @override - void initState() { - super.initState(); - _isMounted = true; - if (widget.autoLoad) { - _loadData(); - } - } - - @override - void dispose() { - _isMounted = false; - super.dispose(); - } - - Future _loadData() async { - if (_state.isLoading) return; - - setState(() { - _state = AsyncState.loading(); - }); - - try { - final result = await widget.futureBuilder(); - if (!_isMounted) return; - - setState(() { - _state = AsyncState.success(result); - }); - } catch (error, stackTrace) { - if (!_isMounted) return; - - setState(() { - _state = AsyncState.error(error, stackTrace); - }); - } - } - - @override - Widget build(BuildContext context) { - return _state.when( - initial: () => widget.onLoading?.call() ?? const SizedBox.shrink(), - loading: () => widget.onLoading?.call() ?? - const Center(child: CircularProgressIndicator.adaptive()), - success: (data) => widget.onData?.call(data) ?? const SizedBox.shrink(), - error: (error, stackTrace) { - - if(error != null) return widget.onError(error, stackTrace); - - return const SizedBox.shrink(); - }, - ); - } - - void reload() { - _loadData(); - } -} \ No newline at end of file diff --git a/lib/src/builder/reactive_stream_builder.dart b/lib/src/builder/reactive_stream_builder.dart index ce00921..130224d 100644 --- a/lib/src/builder/reactive_stream_builder.dart +++ b/lib/src/builder/reactive_stream_builder.dart @@ -35,7 +35,7 @@ class _ReactiveStreamBuilderState extends State> { void initState() { super.initState(); widget.streamNotifier.addListener(_onStreamChanged); - _subscribe(widget.streamNotifier.value); + _subscribe(widget.streamNotifier.notifier); } @override @@ -47,7 +47,7 @@ class _ReactiveStreamBuilderState extends State> { void _onStreamChanged() { _unsubscribe(); - _subscribe(widget.streamNotifier.value); + _subscribe(widget.streamNotifier.notifier); } void _subscribe(Stream stream) { diff --git a/lib/src/handler/async_state.dart b/lib/src/handler/async_state.dart index 39d16c0..931cf28 100644 --- a/lib/src/handler/async_state.dart +++ b/lib/src/handler/async_state.dart @@ -26,7 +26,7 @@ class AsyncState { required R Function() initial, required R Function() loading, required R Function(T data) success, - required R Function(Object? error, StackTrace? stackTrace) error, + required R Function(Object? err, StackTrace? stackTrace) error, }) { switch (status) { case AsyncStatus.initial: diff --git a/lib/src/implements/notifier_impl.dart b/lib/src/implements/notifier_impl.dart index 6ad7ccc..1cfab34 100644 --- a/lib/src/implements/notifier_impl.dart +++ b/lib/src/implements/notifier_impl.dart @@ -3,28 +3,27 @@ import 'package:flutter/foundation.dart'; @protected /// value return. -abstract class NotifierImpl extends ChangeNotifier - implements ValueListenable { - T _value; - NotifierImpl(this._value) { +abstract class NotifierImpl extends ChangeNotifier { + T _notifier; + NotifierImpl(this._notifier) { if (kFlutterMemoryAllocationsEnabled) { ChangeNotifier.maybeDispatchObjectCreation(this); } } - @override - T get value => _value; + + T get notifier => _notifier; /// [updateState] /// Updates the state and notifies listeners if the value has changed. /// @protected void updateState(T newState) { - if (_value == newState) { + if (_notifier == newState) { return; } - _value = newState; + _notifier = newState; notifyListeners(); } @@ -33,60 +32,51 @@ abstract class NotifierImpl extends ChangeNotifier /// @protected void updateSilently(T newState) { - _value = newState; - } - @protected - @override - String toString() => '${describeIdentity(this)}($value)'; - - @protected - @override - void addListener(VoidCallback listener) { - super.addListener(listener); + _notifier = newState; } @protected @override - void removeListener(VoidCallback listener) => super.removeListener(listener); + String toString() => '${describeIdentity(this)}($_notifier)'; - @protected - @override - void dispose() => super.dispose(); @immutable - @protected - @override - void notifyListeners() => super.notifyListeners(); - - @immutable - @protected @override bool get hasListeners => super.hasListeners; } @protected -abstract class StateNotifierImpl extends ChangeNotifier - implements ValueListenable { - T _value; - StateNotifierImpl(this._value) { +abstract class StateNotifierImpl extends ChangeNotifier { + T _notifier; + StateNotifierImpl(this._notifier) { if (kFlutterMemoryAllocationsEnabled) { ChangeNotifier.maybeDispatchObjectCreation(this); } } @protected - @override - T get value => _value; + T get notifier => _notifier; /// [updateState] /// Updates the state and notifies listeners if the value has changed. - @protected + + void updateState(T newState) { - if (_value == newState) { + if (_notifier.hashCode == newState.hashCode) { return; } - _value = newState; + _notifier = newState; + notifyListeners(); + } + + + void transformState( T Function(T data) data){ + final dataNotifier = data(_notifier); + if(dataNotifier.hashCode == _notifier.hashCode){ + return; + } + _notifier = data(_notifier); notifyListeners(); } @@ -94,12 +84,12 @@ abstract class StateNotifierImpl extends ChangeNotifier /// Updates the value silently without notifying listeners. @protected void updateSilently(T newState) { - _value = newState; + _notifier = newState; } @protected @override - String toString() => '${describeIdentity(this)}($value)'; + String toString() => '${describeIdentity(this)}($notifier)'; @protected @override @@ -124,4 +114,4 @@ abstract class StateNotifierImpl extends ChangeNotifier @protected @override bool get hasListeners => super.hasListeners; -} +} \ No newline at end of file diff --git a/lib/src/reactive_notifier.dart b/lib/src/reactive_notifier.dart index ec1c37e..85eaeb8 100644 --- a/lib/src/reactive_notifier.dart +++ b/lib/src/reactive_notifier.dart @@ -38,7 +38,7 @@ class ReactiveNotifier extends NotifierImpl { related?.forEach((child) { child._parents.add(this); assert(() { - log('βž• Added parent-child relation: $T -> ${child.value.runtimeType}', + log('βž• Added parent-child relation: $T -> ${child.notifier.runtimeType}', level: 10); return true; }()); @@ -59,7 +59,7 @@ class ReactiveNotifier extends NotifierImpl { assert(() { log(''' πŸ“¦ Creating ReactiveNotifier<$T> -${related != null ? 'πŸ”— With related types: ${related.map((r) => r.value.runtimeType).join(', ')}' : ''} +${related != null ? 'πŸ”— With related types: ${related.map((r) => r.notifier.runtimeType).join(', ')}' : ''} ''', level: 5); return true; }()); @@ -105,7 +105,7 @@ Location: $trace @override void updateState(T newState) { - if (value != newState) { + if (notifier != newState) { // Prevent circular update if (_updatingNotifiers.contains(this)) { return; @@ -115,7 +115,7 @@ Location: $trace _checkNotificationOverflow(); assert(() { - log('πŸ“ Updating state for $T: $value -> ${newState.runtimeType}', level: 10); + log('πŸ“ Updating state for $T: $notifier -> ${newState.runtimeType}', level: 10); return true; }()); @@ -163,7 +163,7 @@ Location: $trace ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ Notifier: ${describeIdentity(this)} Type: $T -Current Value: $value +Current Value: $notifier Location: ${StackTrace.current} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ $_notificationCount notifications in ${_thresholdTimeWindow.inMilliseconds}ms @@ -224,8 +224,8 @@ $_notificationCount notifications in ${_thresholdTimeWindow.inMilliseconds}ms String _formatNotifierInfo(ReactiveNotifier notifier) { return ''' - Type: ${notifier.value.runtimeType} - Value: ${notifier.value} + Type: ${notifier.notifier.runtimeType} + Value: ${notifier.notifier} Key: ${notifier.keyNotifier}'''; } @@ -265,7 +265,7 @@ $_notificationCount notifications in ${_thresholdTimeWindow.inMilliseconds}ms Set pathKeys, ) { final cycle = [...pathKeys, child.keyNotifier] - .map((key) => '${_instances[key]?.value.runtimeType}($key)') + .map((key) => '${_instances[key]?.notifier.runtimeType}($key)') .join(' -> '); throw StateError(''' @@ -376,29 +376,29 @@ Requested type: $R${key != null ? '\nRequested key: $key' : ''} final result = key != null ? related!.firstWhere( - (n) => n.value is R && n.keyNotifier == key, + (n) => n.notifier is R && n.keyNotifier == key, orElse: () => throw StateError(''' ❌ Related State Not Found ━━━━━━━━━━━━━━━━━━━━━ Looking for: $R with key: $key Parent type: $T -Available types: ${related!.map((r) => '${r.value.runtimeType}(${r.keyNotifier})').join(', ')} +Available types: ${related!.map((r) => '${r.notifier.runtimeType}(${r.keyNotifier})').join(', ')} ━━━━━━━━━━━━━━━━━━━━━ '''), ) : related!.firstWhere( - (n) => n.value is R, + (n) => n.notifier is R, orElse: () => throw StateError(''' ❌ Related State Not Found ━━━━━━━━━━━━━━━━━━━━━ Looking for: $R Parent type: $T -Available types: ${related!.map((r) => '${r.value.runtimeType}(${r.keyNotifier})').join(', ')} +Available types: ${related!.map((r) => '${r.notifier.runtimeType}(${r.keyNotifier})').join(', ')} ━━━━━━━━━━━━━━━━━━━━━ '''), ); - return result.value as R; + return result.notifier as R; } /// Utility methods @@ -414,5 +414,6 @@ Available types: ${related!.map((r) => '${r.value.runtimeType}(${r.keyNotifier}) } @override - String toString() => '${describeIdentity(this)}($value)'; + String toString() => '${describeIdentity(this)}($notifier)'; + } diff --git a/lib/src/viewmodel/async_viewmodel_impl.dart b/lib/src/viewmodel/async_viewmodel_impl.dart deleted file mode 100644 index 0029194..0000000 --- a/lib/src/viewmodel/async_viewmodel_impl.dart +++ /dev/null @@ -1,74 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:reactive_notifier/src/handler/async_state.dart'; -import 'package:reactive_notifier/src/implements/notifier_impl.dart'; - -/// Base ViewModel implementation for handling asynchronous operations with state management. -/// -/// Provides a standardized way to handle loading, success, and error states for async data. -import 'package:flutter/foundation.dart'; - -/// Base ViewModel implementation for handling asynchronous operations with state management. -abstract class AsyncViewModelImpl extends NotifierImpl> { - AsyncViewModelImpl({ - bool loadOnInit = true, - T? initialData, - }) : super(initialData != null ? AsyncState.success(initialData) : AsyncState.initial()) { - if (loadOnInit) { - _initializeAsync(); - } - } - - /// Internal initialization method that properly handles async initialization - Future _initializeAsync() async { - try { - await reload(); - } catch (error, stackTrace) { - setError(error, stackTrace); - } - } - - /// Public method to reload data - @protected - Future reload() async { - if (value.isLoading) return; - - updateState(AsyncState.loading()); - try { - final result = await loadData(); - updateState(AsyncState.success(result)); - } catch (error, stackTrace) { - setError(error, stackTrace); - } - } - - /// Override this method to provide the async data loading logic - @protected - Future loadData(); - - /// Update data directly - @protected - void updateData(T data) { - updateState(AsyncState.success(data)); - } - - /// Set error state - @protected - void setError(Object error, [StackTrace? stackTrace]) { - updateState(AsyncState.error(error, stackTrace)); - } - - /// Check if any operation is in progress - bool get isLoading => value.isLoading; - - /// Get current error if any - Object? get error => value.error; - - /// Get current stack trace if there's an error - StackTrace? get stackTrace => value.stackTrace; - - /// Check if the state contains valid data - bool get hasData => value.isSuccess; - - /// Get the current data (may be null if not in success state) - T? get data => value.isSuccess ? value.data : null; -} diff --git a/lib/src/viewmodel/viewmodel_impl.dart b/lib/src/viewmodel/viewmodel_impl.dart index 5796180..473c61d 100644 --- a/lib/src/viewmodel/viewmodel_impl.dart +++ b/lib/src/viewmodel/viewmodel_impl.dart @@ -26,6 +26,7 @@ abstract class ViewModelImpl extends StateNotifierImpl { } } + @protected void init(); bool _initialized = false; @@ -93,4 +94,9 @@ abstract class ViewModelStateImpl extends StateNotifierImpl { StateTracker.trackStateChange(_id); } } + + + @override + T get notifier => this.notifier; + } diff --git a/pubspec.yaml b/pubspec.yaml index fda4404..24afc02 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -21,7 +21,7 @@ dev_dependencies: alchemist: ^0.7.0 flutter: - + uses-material-design: true # To add assets to your package, add an assets section, like this: # assets: # - images/a_dot_burr.jpeg diff --git a/test/circular_references_test.dart b/test/circular_references_test.dart index 787726f..3ae338c 100644 --- a/test/circular_references_test.dart +++ b/test/circular_references_test.dart @@ -323,10 +323,10 @@ void main() { reason: 'Should create all instances without nesting', ); - expect(firstNotifier2.value, equals('Level -1')); - expect(firstNotifier.value, equals('Level 0')); - expect(secondNotifier.value, equals('Level 1')); - expect(thirdNotifier.value, equals('Level 2')); + expect(firstNotifier2.notifier, equals('Level -1')); + expect(firstNotifier.notifier, equals('Level 0')); + expect(secondNotifier.notifier, equals('Level 1')); + expect(thirdNotifier.notifier, equals('Level 2')); }); }); } diff --git a/test/golden_reactive_builder.test.dart b/test/golden_reactive_builder.test.dart index c90c9f4..68e7210 100644 --- a/test/golden_reactive_builder.test.dart +++ b/test/golden_reactive_builder.test.dart @@ -19,7 +19,7 @@ void main() { name: 'Change value 0', child: ReactiveBuilder( valueListenable: state, - builder: (context, value, child) { + builder: (value, child) { return Column( children: [ Text("Widget que se recontruye $value"), @@ -46,7 +46,7 @@ void main() { name: 'Change value 200', child: ReactiveBuilder( valueListenable: state, - builder: (context, value, child) { + builder: (value, child) { if (value == 0) { state.updateState(200); } @@ -69,7 +69,7 @@ void main() { home: Scaffold( body: ReactiveBuilder( valueListenable: valueNotifier, - builder: (context, value, noRebuildable) { + builder: ( value, noRebuildable) { rebuildCount++; // Contador para verificar reconstrucciones log("Widget que se reconstruye: $value"); log("Widget que se reconstruye: $value"); diff --git a/test/reactive_notifier_test.dart b/test/reactive_notifier_test.dart index a0c33bc..f43dd51 100644 --- a/test/reactive_notifier_test.dart +++ b/test/reactive_notifier_test.dart @@ -14,22 +14,22 @@ void main() { group('Initialization and Basic Functionality', () { test('should initialize with default value', () { final state = ReactiveNotifier(() => 0); - expect(state.value, 0); + expect(state.notifier, 0); expect(ReactiveNotifier.instanceCount, 1); }); test('should create separate instances for each call', () { final state1 = ReactiveNotifier(() => 0); final state2 = ReactiveNotifier(() => 1); - expect(state1.value, 0); - expect(state2.value, 1); + expect(state1.notifier, 0); + expect(state2.notifier, 1); expect(ReactiveNotifier.instanceCount, 2); }); test('should update value with setState', () { final state = ReactiveNotifier(() => 0); state.updateState(10); - expect(state.value, 10); + expect(state.notifier, 10); }); }); @@ -39,7 +39,7 @@ void main() { int? notifiedValue; notify.addListener(() { - notifiedValue = notify.value; + notifiedValue = notify.notifier; }); notify.updateState(5); @@ -53,10 +53,10 @@ void main() { int? listener2Value; notify.addListener(() { - listener1Value = notify.value; + listener1Value = notify.notifier; }); notify.addListener(() { - listener2Value = notify.value; + listener2Value = notify.notifier; }); notify.updateState(10); @@ -70,7 +70,7 @@ void main() { int? listenerValue; void listener() { - listenerValue = notify.value; + listenerValue = notify.notifier; } notify.addListener(listener); @@ -121,16 +121,16 @@ void main() { final isEvenNotifier = ReactiveNotifier(() => true); countNotifier.addListener(() { - isEvenNotifier.updateState(countNotifier.value % 2 == 0); + isEvenNotifier.updateState(countNotifier.notifier % 2 == 0); }); countNotifier.updateState(1); - expect(countNotifier.value, 1); - expect(isEvenNotifier.value, false); + expect(countNotifier.notifier, 1); + expect(isEvenNotifier.notifier, false); countNotifier.updateState(2); - expect(countNotifier.value, 2); - expect(isEvenNotifier.value, true); + expect(countNotifier.notifier, 2); + expect(isEvenNotifier.notifier, true); }); test('should handle cascading updates', () { @@ -140,15 +140,15 @@ void main() { temperatureCelsius.addListener(() { temperatureFahrenheit - .updateState(temperatureCelsius.value * 9 / 5 + 32); + .updateState(temperatureCelsius.notifier * 9 / 5 + 32); }); temperatureFahrenheit.addListener(() { - if (temperatureFahrenheit.value < 32) { + if (temperatureFahrenheit.notifier < 32) { weatherDescription.updateState('Freezing'); - } else if (temperatureFahrenheit.value < 65) { + } else if (temperatureFahrenheit.notifier < 65) { weatherDescription.updateState('Cold'); - } else if (temperatureFahrenheit.value < 80) { + } else if (temperatureFahrenheit.notifier < 80) { weatherDescription.updateState('Comfortable'); } else { weatherDescription.updateState('Hot'); @@ -156,14 +156,14 @@ void main() { }); temperatureCelsius.updateState(25); // 77Β°F - expect(temperatureCelsius.value, 25); - expect(temperatureFahrenheit.value, closeTo(77, 0.1)); - expect(weatherDescription.value, 'Comfortable'); + expect(temperatureCelsius.notifier, 25); + expect(temperatureFahrenheit.notifier, closeTo(77, 0.1)); + expect(weatherDescription.notifier, 'Comfortable'); temperatureCelsius.updateState(35); // 95Β°F - expect(temperatureCelsius.value, 35); - expect(temperatureFahrenheit.value, closeTo(95, 0.1)); - expect(weatherDescription.value, 'Hot'); + expect(temperatureCelsius.notifier, 35); + expect(temperatureFahrenheit.notifier, closeTo(95, 0.1)); + expect(weatherDescription.notifier, 'Hot'); }); test('should handle circular dependencies without infinite updates', () { @@ -177,21 +177,21 @@ void main() { notifierA.addListener(() { updateCountA++; - notifierB.updateState(notifierA.value + 1); + notifierB.updateState(notifierA.notifier + 1); }); notifierB.addListener(() { updateCountB++; - notifierA.updateState(notifierB.value + 1); + notifierA.updateState(notifierB.notifier + 1); }); notifierA.updateState(1); expect(updateCountA, equals(1), reason: 'notifierA should update once'); expect(updateCountB, equals(1), reason: 'notifierB should update once'); - expect(notifierA.value, equals(1), + expect(notifierA.notifier, equals(1), reason: 'notifierA state should remain 1'); - expect(notifierB.value, equals(2), + expect(notifierB.notifier, equals(2), reason: 'notifierB state should be updated to 2'); ReactiveNotifier.cleanup(); @@ -250,14 +250,14 @@ void main() { final complexState = ReactiveNotifier>( () => {'count': 0, 'name': 'Test'}); complexState.updateState({'count': 1, 'name': 'Updated'}); - expect(complexState.value, {'count': 1, 'name': 'Updated'}); + expect(complexState.notifier, {'count': 1, 'name': 'Updated'}); }); test('should handle null states', () { final nullableState = ReactiveNotifier(() => null); - expect(nullableState.value, isNull); + expect(nullableState.notifier, isNull); nullableState.updateState(5); - expect(nullableState.value, 5); + expect(nullableState.notifier, 5); }); test('should handle state transitions', () { @@ -281,54 +281,54 @@ void main() { } updateStateAsync(); - expect(asyncState.value, 'initial'); + expect(asyncState.notifier, 'initial'); await Future.delayed(const Duration(milliseconds: 150)); - expect(asyncState.value, 'updated'); + expect(asyncState.notifier, 'updated'); }); test('should manage concurrent async updates', () async { final concurrentState = ReactiveNotifier(() => 0); Future incrementAsync() async { await Future.delayed(const Duration(milliseconds: 50)); - concurrentState.updateState(concurrentState.value + 1); + concurrentState.updateState(concurrentState.notifier + 1); } await Future.wait( [incrementAsync(), incrementAsync(), incrementAsync()]); - expect(concurrentState.value, 3); + expect(concurrentState.notifier, 3); }); }); group('Computed States', () { test('should handle computed states', () { final baseState = ReactiveNotifier(() => 1); - final computedState = ReactiveNotifier(() => baseState.value * 2); + final computedState = ReactiveNotifier(() => baseState.notifier * 2); baseState - .addListener(() => computedState.updateState(baseState.value * 2)); + .addListener(() => computedState.updateState(baseState.notifier * 2)); baseState.updateState(5); - expect(computedState.value, 10); + expect(computedState.notifier, 10); }); test('should efficiently update multiple dependent states', () { final rootState = ReactiveNotifier(() => 0); - final computed1 = ReactiveNotifier(() => rootState.value + 1); - final computed2 = ReactiveNotifier(() => rootState.value * 2); + final computed1 = ReactiveNotifier(() => rootState.notifier + 1); + final computed2 = ReactiveNotifier(() => rootState.notifier * 2); final computed3 = - ReactiveNotifier(() => computed1.value + computed2.value); + ReactiveNotifier(() => computed1.notifier + computed2.notifier); rootState.addListener(() { - computed1.updateState(rootState.value + 1); - computed2.updateState(rootState.value * 2); + computed1.updateState(rootState.notifier + 1); + computed2.updateState(rootState.notifier * 2); }); computed1.addListener( - () => computed3.updateState(computed1.value + computed2.value)); + () => computed3.updateState(computed1.notifier + computed2.notifier)); computed2.addListener( - () => computed3.updateState(computed1.value + computed2.value)); + () => computed3.updateState(computed1.notifier + computed2.notifier)); rootState.updateState(5); - expect(computed1.value, 6); - expect(computed2.value, 10); - expect(computed3.value, 16); + expect(computed1.notifier, 6); + expect(computed2.notifier, 10); + expect(computed3.notifier, 16); }); }); @@ -336,7 +336,7 @@ void main() { test('should maintain state history', () { final historicalState = ReactiveNotifier(() => 0); final history = []; - historicalState.addListener(() => history.add(historicalState.value)); + historicalState.addListener(() => history.add(historicalState.notifier)); historicalState.updateState(1); historicalState.updateState(2); historicalState.updateState(3); @@ -346,11 +346,11 @@ void main() { test('should support undo operations', () { final undoableState = ReactiveNotifier(() => 0); final history = [0]; - undoableState.addListener(() => history.add(undoableState.value)); + undoableState.addListener(() => history.add(undoableState.notifier)); undoableState.updateState(1); undoableState.updateState(2); undoableState.updateState(history[history.length - 2]); // Undo - expect(undoableState.value, 1); + expect(undoableState.notifier, 1); }); }); @@ -359,8 +359,8 @@ void main() { final customState = ReactiveNotifier(() => CustomObject(1, 'initial')); customState.updateState(CustomObject(2, 'updated')); - expect(customState.value.id, 2); - expect(customState.value.name, 'updated'); + expect(customState.notifier.id, 2); + expect(customState.notifier.name, 'updated'); }); }); @@ -385,7 +385,7 @@ void main() { // Actualizar el estado en el isolate principal isolateState.updateState(updatedState as int); - expect(isolateState.value, 42); + expect(isolateState.notifier, 42); }); }); @@ -406,9 +406,9 @@ void main() { test('should support dependency injection', () { const injectedDependency = 'Injected Value'; final dependentState = ReactiveNotifier(() => 'Initial'); - expect(dependentState.value, 'Initial'); + expect(dependentState.notifier, 'Initial'); dependentState.updateState('Updated with $injectedDependency'); - expect(dependentState.value, 'Updated with Injected Value'); + expect(dependentState.notifier, 'Updated with Injected Value'); }); }); }); @@ -436,7 +436,7 @@ void main() { state.updateState(42); //42; expect(notifications, 1); - expect(state.value, 42); + expect(state.notifier, 42); }); test('does not notify if value is the same', () { @@ -479,21 +479,21 @@ void main() { ReactiveNotifier(() => 'combined', related: [stateA, stateB]); stateA.addListener(() => updates.add('A')); - expect(stateA.value, 'A'); + expect(stateA.notifier, 'A'); expect(combined.from(stateA.keyNotifier), 'A'); stateB.addListener(() => updates.add('B')); - expect(stateB.value, 'B'); + expect(stateB.notifier, 'B'); expect(combined.from(stateB.keyNotifier), 'B'); combined.addListener(() => updates.add('combined')); stateA.updateState('A2'); - expect(stateA.value, 'A2'); + expect(stateA.notifier, 'A2'); expect(combined.from(stateA.keyNotifier), 'A2'); stateB.updateState('B2'); - expect(stateB.value, 'B2'); + expect(stateB.notifier, 'B2'); expect(combined.from(stateB.keyNotifier), 'B2'); expect(updates.length, 4); From c674770f8b2c14424e8b7140b41588742a4aebcd Mon Sep 17 00:00:00 2001 From: Jhonacode Date: Fri, 20 Dec 2024 01:20:38 +1300 Subject: [PATCH 2/4] Update readme, name on ReacgiveBuilder for notifier, remove context, etc. breaking chnages --- CHANGELOG.md | 22 +- README.md | 621 ++++++------------ example/reactive_notifier_example.dart | 6 +- example/service/connection_service.dart | 2 +- .../viewmodel/connection_state_viewmodel.dart | 4 +- lib/src/builder/reactive_async_builder.dart | 33 +- lib/src/builder/reactive_builder.dart | 24 +- lib/src/builder/reactive_stream_builder.dart | 42 +- lib/src/implements/notifier_impl.dart | 7 +- pubspec.yaml | 2 +- test/golden_reactive_builder.test.dart | 6 +- 11 files changed, 286 insertions(+), 483 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 99b17fc..291c2d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,23 @@ +# 2.4.0 + +### Breaking Changes 🚨 + +- Introducing `transformState` function for model editing, allowing state modifications at any nesting level. This function supports implementations like `copyWith`, enabling selective value updates in your models. + +- Simplified state management: unified `notifier` and VM into a single approach using `ReactiveBuilder`, `ReactiveAsync`, and `ReactiveStream`. Access functions directly through notifier reference (e.g., `instance.notifier.replaceData(...)`). Access `ReactiveAsync` data via `notifier.data`. + +- Removed `ValueNotifier` value dependency, eliminating nested state update issues (previously `instance.value.value`, now `instance.data`). + +- Protected internal builder functions for improved encapsulation. + +- Maintained compatibility with `ListenableBuilder` for `ReactiveNotifier`. + +- Removed `context` dependency from builder as `ReactiveNotifier` doesn't require it. + +### Best Practices +- Recommend using mixins to store related Notifiers, avoiding global variables and maintaining proper context encapsulation. + + # 2.3.1 * Update documentation. * Protected value for NotifierImpl. @@ -12,7 +32,7 @@ * For ViewModels/Complex States: ```dart ReactiveBuilder( - valueListenable: stateConnection.value, + notifier: stateConnection.value, builder: (context, state, keep) => YourWidget() ) ``` diff --git a/README.md b/README.md index c039b28..9ee67cc 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # ReactiveNotifier -A flexible, elegant, and secure tool for state management in Flutter. Designed to easily integrate with architectural patterns like MVVM, it guarantees full independence from BuildContext and is suitable for projects of any scale. +A flexible, elegant, and secure tool for state management in Flutter. Designed with fine-grained state control in mind, it easily integrates with architectural patterns like MVVM, guarantees full independence from BuildContext, and is suitable for projects of any scale. ![reactive_notifier](https://github.com/user-attachments/assets/ca97c7e6-a254-4b19-b58d-fd07206ff6ee) @@ -34,291 +34,138 @@ Add this to your package's `pubspec.yaml` file: ```yaml dependencies: - reactive_notifier: ^2.3.1 + reactive_notifier: ^2.4.0 ``` ## Quick Start -### **Basic Usage with ReactiveNotifier and `ReactiveBuilder.notifier`** +### **Usage with ReactiveNotifier -#### **Example: Handling a Simple State** - -This example demonstrates how to manage a basic state that stores a `String`. It shows how to declare the state, create a widget to display it, and update its value: +Learn how to implement ReactiveNotifier across different use cases - from basic data types (`String`, `bool`) to complex classes (`ViewModels`). These examples showcase global state management patterns that maintain accessibility across your application. +#### With Classes, Viewmodel, etc. ```dart -import 'package:flutter/material.dart'; -import 'package:reactive_notifier/reactive_notifier.dart'; -// Declare a simple state -final messageState = ReactiveNotifier(() => "Hello, world!"); +/// It is recommended to use mixin to save your notifiers, from a static variable. +/// +mixin ConnectionService{ + static final ReactiveNotifier instance = ReactiveNotifier(() => ConnectionManager()); +} + +class ConnectionStateWidget extends StatelessWidget { + const ConnectionStateWidget({super.key}); -class SimpleExample extends StatelessWidget { @override Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text("ReactiveNotifier Example")), - body: Center( - child: ReactiveBuilder.notifier( - notifier: messageState, - builder: (context, value, keep) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, + + /// It is recommended to set the data type in the builder. + return ReactiveBuilder( + + notifier: ConnectionService.instance, + + builder: ( service, keep) { + + /// Notifier is used to access your model's data. + final state = service.notifier; + + return Card( + elevation: 4, + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisSize: MainAxisSize.min, + spacing: 10, children: [ + CircleAvatar( + radius: 30, + backgroundColor: state.color.withValues(alpha: 255 * 0.2), + child: Icon( + state.icon, + color: state.color, + size: 35, + ), + ), + Text( - value, // Display the current state - style: TextStyle(fontSize: 20), + state.message, + style: Theme.of(context).textTheme.titleMedium, ), - SizedBox(height: 20), - keep( - ElevatedButton( - onPressed: () => messageState.updateState("New message!"), - child: Text("Update Message"), + + if (state.isError || state == ConnectionState.disconnected) + keep( + ElevatedButton.icon( + + /// If you don't use notifier, access the functions of your Viewmodel that contains your model. + onPressed: () => service.manualReconnect(), + + icon: const Icon(Icons.refresh), + label: const Text('Retry Connection'), + ), ), - ), + if (state.isSyncing) const LinearProgressIndicator(), ], - ); - }, - ), - ), + ), + ), + ); + }, ); } } ``` ---- - -### **Example Description** - -1. **State Declaration:** - - `ReactiveNotifier` is used to manage a state of type `String`. - - You can use other primitive types such as `int`, `double`, `bool`, `enum`, etc. - -2. **ReactiveBuilder.notifier:** - - Observes the state and automatically updates the UI when its value changes. - - Accepts three parameters in the `builder` method: - - `context`: The widget's context. - - `value`: The current value of the state. - - `keep`: Prevents uncontrolled widget rebuilds. -3. **State Update:** - - The `updateState` method is used to change the state value. - - Whenever the state is updated, the UI dependent on it automatically rebuilds. - -## **1. Model Class Definition** - -We'll create a `MyClass` model with some properties such as `String` and `int`. +#### With simple values. ```dart -class MyClass { - final String name; - final int value; - - MyClass({required this.name, required this.value}); - - // Method to create an empty instance - MyClass.empty() : name = '', value = 0; - // Method to clone and update values - MyClass copyWith({String? name, int? value}) { - return MyClass( - name: name ?? this.name, - value: value ?? this.value, - ); - } +mixin ConnectionService{ + static final ReactiveNotifier instance = ReactiveNotifier(() => "N/A"); } -``` - - -## **2. Create and Update State with `ReactiveNotifier`** - -Now, we'll use `ReactiveNotifier` to manage the state of `MyClass`. We'll start with an empty state and later update it using the `updateState` method. - -```dart -final myReactive = ReactiveNotifier(() => MyClass.empty()); -``` - -Here, `myReactive` is a `ReactiveNotifier` managing the state of `MyClass`. - ---- -## **3. Display and Update State with `ReactiveBuilder.notifier`** - -We'll use `ReactiveBuilder.notifier` to display the current state of `MyClass` and update it when needed. - -```dart import 'package:flutter/material.dart'; import 'package:reactive_notifier/reactive_notifier.dart'; -class MyApp extends StatelessWidget { +// Declare a simple state +class ConnectionStateWidget extends StatelessWidget { + const ConnectionStateWidget({super.key}); + @override Widget build(BuildContext context) { - return MaterialApp( - home: Scaffold( - appBar: AppBar(title: Text("Direct ReactiveNotifier")), - body: Center( - child: ReactiveBuilder.notifier( - notifier: myReactive, - builder: (context, state, keep) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text("Name: ${state.name}"), - Text("Value: ${state.value}"), - SizedBox(height: 20), - ElevatedButton( - onPressed: () { - // Update the state with new values - myReactive.updateState(state.copyWith(name: "New Name", value: 42)); - }, - child: Text("Update State"), - ), - ], - ); - }, - ), - ), - ), + return ReactiveBuilder( + notifier: ConnectionService.instance, + builder: ( state, keep) => Text(state), ); } } -``` - -### **Advantages of This Approach** -- **Simplicity**: This example is straightforward and easy to understand, making it ideal for handling simple states without requiring a `ViewModel` or repository. -- **Reactivity**: With `ReactiveNotifier` and `ReactiveBuilder.notifier`, the UI automatically updates whenever the state changes. -- **Immutability**: `MyClass` follows the immutability pattern, simplifying state management and preventing unexpected side effects. +class OtherWidget extends StatelessWidget { + const OtherWidget({super.key}); -## **Shopping Cart with `ViewModelStateImpl`** - -### **1. Defining the Model** - -The `CartModel` class represents the shopping cart's state. It contains the data and includes a `copyWith` method for creating a new instance with updated values. - -```dart -class CartModel { - final List items; - final double total; - - CartModel({this.items = const [], this.total = 0.0}); - - // Method to clone the model with updated values - CartModel copyWith({List? items, double? total}) { - return CartModel( - items: items ?? this.items, - total: total ?? this.total, + @override + Widget build(BuildContext context) { + return Column( + children: [ + OutlinedButton(onPressed: (){ + ConnectionService.instance.updateState("New value"); + }, child: Text('Edit String')) + ], ); } } -``` -### **2. ViewModel for Managing Cart Logic** +class OtherViewModel{ -The `ViewModel` contains the logic for modifying the cart's state. Instead of using `state.copyWith`, use `value.copyWith` to access the current state and update it. + void otherFunction(){ + ///Come code..... -```dart -class CartViewModel extends ViewModelStateImpl { - CartViewModel() : super(CartModel()); - - // Function to add a product to the cart and update the total - void addProduct(String item, double price) { - final updatedItems = List.from(value.items)..add(item); - final updatedTotal = value.total + price; - updateState(value.copyWith(items: updatedItems, total: updatedTotal)); - } - - // Function to empty the cart - void clearCart() { - updateState(CartModel()); + ConnectionService.instance.updateState("Value from other viewmodel"); } } -``` - ---- - -### **3. Creating the ViewModel Instance** - -Create the `ViewModel` instance that will manage the cart's state. - -```dart -final cartViewModel = ReactiveNotifier(() => CartViewModel()); -``` - ---- - -### **4. Widget to Display Cart State** -Finally, create a widget that observes the cart's state and updates the UI whenever necessary. - -```dart -import 'package:flutter/material.dart'; -import 'package:reactive_notifier/reactive_notifier.dart'; - -class CartScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text("Shopping Cart")), - body: Center( - child: ReactiveBuilder( - valueListenable: cartViewModel.value, - builder: (context, viewModel, keep) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - Text("Products in Cart:"), - ...viewModel.items.map((item) => Text(item)).toList(), - SizedBox(height: 20), - Text("Total: \$${viewModel.total.toStringAsFixed(2)}"), - SizedBox(height: 20), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - keep( - ElevatedButton( - onPressed: () { - // Add a new product - cartViewModel.value.addProduct("Product A", 19.99); - }, - child: Text("Add Product A"), - ), - ), - SizedBox(width: 10), - keep( - ElevatedButton( - onPressed: () { - // Clear the cart - cartViewModel.value.clearCart(); - }, - child: Text("Clear Cart"), - ), - ), - ], - ), - ], - ); - }, - ), - ), - ); - } -} +/// This example above applies to any Reactive Notifier, no matter how complex, it can be used from anywhere without the need for a reference or handler. ``` - -### **Explanation** - -1. **Model (`CartModel`)**: - - `CartModel` is a class that holds the shopping cart data (`items` and `total`). - - The `copyWith` method allows creating a new instance of the model with modified values while maintaining immutability. - -2. **ViewModel (`CartViewModel`)**: - - `CartViewModel` extends `ViewModelStateImpl` and handles business logic. - - Instead of `state.copyWith`, `value.copyWith` is used to access and update the current state. - - The methods `addProduct` and `clearCart` update the state using `updateState`. - -3. **UI with `ReactiveBuilder`**: - - `ReactiveBuilder` observes the `ViewModel` state and rebuilds the UI when the state changes. - - The `keep` function is used to prevent unnecessary button rebuilds, improving performance. +Both simple and complex values can be modified from anywhere in the application without modifying the structure of your widget. +You also don't need to instantiate variables in your widget build, you just call the mixin directly where you want to use it, this helps with less coupling, being able to replace all functions from the mixin and not fight with extensive migrations. --- @@ -337,44 +184,16 @@ import 'package:reactive_notifier/reactive_notifier.dart'; class CartRepository extends RepositoryImpl { // We simulate the loading of a shopping cart - Future fetchData() async { await Future.delayed(Duration(seconds: 2)); return CartModel( - items: ['Producto A', 'Producto B'], + items: ['Product A', 'Product B'], total: 39.98, ); } + + /// More functions ..... - // Method to add a product to the cart - Future agregarProducto(CartModel carrito, String item, double price) async { - await Future.delayed(Duration(seconds: 1)); - carrito.items.add(item); - carrito.total += price; - } -} -``` - ---- - -## **2. Cart Model (`CartModel`)** - -The model remains the same: - -```dart -class CartModel { - final List items; - final double total; - - CartModel({this.items = const [], this.total = 0.0}); - - // Method to clone the model with new values - CartModel copyWith({List? items, double? total}) { - return CartModel( - items: items ?? this.items, - total: total ?? this.total, - ); - } } ``` @@ -391,26 +210,38 @@ class CartViewModel extends ViewModelImpl { CartViewModel(this.repository) : super(CartModel()); // Function to load the cart from the repository - Future cargarCarrito() async { + Future loadShoppingCart() async { try { // We get the cart from the repository - final carrito = await repository.fetchData(); - setState(carrito); // We update the status with the cart loaded + final shoppingCart = await repository.fetchData(); + updateState(carrito); // We update the status with the cart loaded } catch (e) { // Error handling - print("Error al cargar el carrito: $e"); + print("Error: $e"); } } // Function to add a product to the cart - Future agregarProducto(String item, double price) async { + Future addProduct(String item, double price) async { try { - await repository.agregarProducto(value, item, price); + + await repository.addProduct(value, item, price); + // We update the status after adding the product - updateState(value.copyWith(items: value.items, total: value.total)); + updateState(notifier.copyWith(items: value.items, total: value.total)); + + // Or + transformState((state) => state.copyWith(items: value.items, total: value.total)); + + // Or + updateState(yourModelWithData); + } catch (e) { + // Error handling - print("Error al agregar el producto: $e"); + print("Error $e"); + + } } } @@ -435,100 +266,69 @@ final cartViewModel = ReactiveNotifier((){ Finally, we are going to display the cart status in the UI using `ReactiveBuilder`, which will automatically update when the status changes. ```dart -class CartScreen extends StatelessWidget { - @override - Widget build(BuildContext context) { - return Scaffold( - appBar: AppBar(title: Text("Carrito de Compras")), - body: Center( - child: ReactiveBuilder( - valueListenable: cartViewModel.value, - builder: (context, viewModel, keep) { - return Column( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - if (viewModel.items.isEmpty) - keep(Text("Loading cart...")), - if (viewModel.items.isNotEmpty) ...[ - keep(Text("Products in cart:")), - ...viewModel.items.map((item) => Text(item)).toList(), - keep(const SizedBox(height: 20)), - Text("Total: \$${viewModel.total.toStringAsFixed(2)}"), - keep(const SizedBox(height: 20)), - Row( - mainAxisAlignment: MainAxisAlignment.center, - children: [ - keep( - ElevatedButton( - onPressed: () { - // Add a new product - cartViewModel.value.agregarProducto("Producto C", 29.99); - }, - child: Text("Agregar Producto C"), - ), - ), - keep(const SizedBox(width: 10)), - keep( - ElevatedButton( - onPressed: () { - // Empty cart - cartViewModel.value.setState(CartModel()); - }, - child: Text("Vaciar Carrito"), - ), - ), - ], - ), - ], - ], - ); - }, - ), - ), +ReactiveBuilder( + notifier: cartViewModel, + builder: ( viewModel, keep) { + return Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + if (viewModel.data.isEmpty) + keep(Text("Loading cart...")), + if (viewModel.data.isNotEmpty) ...[ + keep(Text("Products in cart:")), + ...viewModel.data.map((item) => Text(item)).toList(), + keep(const SizedBox(height: 20)), + Text("Total: \$${viewModel.total.toStringAsFixed(2)}"), + keep(const SizedBox(height: 20)), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + keep( + ElevatedButton( + onPressed: () { + // Add a new product + cartViewModel.notifier.agregarProducto("Producto C", 29.99); + }, + child: Text("Agregar Producto C"), + ), + ), + keep(const SizedBox(width: 10)), + keep( + ElevatedButton( + onPressed: () { + // Empty cart + cartViewModel.notifier.myCleaningCarFunction(); + + }, + child: Text("Vaciar Carrito"), + ), + ), + ], + ), + ], + ], ); - } -} - + }, +) ``` -### **Explanation of the Example** - -1. **Repository (`CartRepository`)**: -- The repository extends `RepositoryImpl`, allowing you to interact with the cart data model. -- The `fetchData` function simulates fetching the cart data, and `addProduct` simulates adding products to the cart. - -2. **Model (`CartModel`)**: -- `CartModel` contains the cart data (items and total). -- The `copyWith` method is used to create a new instance with modified values, maintaining immutability. - -3. **`ViewModelImpl` (`CartViewModel`)**: -- `CartViewModel` extends `ViewModelImpl`, allowing you to handle the cart business logic. -- The `loadCart` and `addProduct` methods interact with the repository and update the state of the cart. - -4. **UI with `ReactiveBuilder`**: -- `ReactiveBuilder` observes the state of the `ViewModel` and updates the UI automatically when the state changes. -- `keep` is used to avoid unnecessary button rebuilds. - - ---- - +# ⚑️ **Essential: `ReactiveNotifier` Core Concepts** ## **Documentation for `related` in `ReactiveNotifier`** The `related` attribute in `ReactiveNotifier` allows you to efficiently manage interdependent states. They can be used in different ways depending on the structure and complexity of the state you need to handle. -### **Types of `related` Usage** - -1. **Direct Relationship between Simple Notifiers** -- This is the case where you have multiple independent `ReactiveNotifier`s (of simple type like `int`, `String`, `bool`, etc.) and you want any change in one of these notifiers to trigger an update in a `ReactiveBuilder` that is watched by a combined `ReactiveNotifier`. +### **Using `related` in ReactiveNotifier** -2. **Relationship between a Main `ReactiveNotifier` and Other Complementary Notifiers** -- In this approach, a main `ReactiveNotifier` handles a complex class (e.g. `UserInfo`), and other notifiers complement this state, such as `Settings`. The companion states are managed separately, but are related to the main state via `related`. Changes to any of the related notifiers cause the `ReactiveBuilder` to be updated. +**Establishing Relationships Between Notifiers** +- `ReactiveNotifier` can manage any type of data (simple or complex). Through the `related` property, you can establish connections between different notifiers, where changes in any related notifier will trigger updates in the `ReactiveBuilder` that's watching them. +**Example Scenario: Managing Connected States** +- A primary notifier might handle a complex `UserInfo` class, while other notifiers manage related states like `Settings` or `Preferences`. Using `related`, any changes in these interconnected states will trigger the appropriate UI updates through `ReactiveBuilder`. --- -### **1. Direct Relationship between Simple Notifiers** +### **Direct Relationship between Simple Notifiers** In this approach, you have several simple `ReactiveNotifier`s, and you use them together to notify state changes when any of these notifiers changes. The `ReactiveNotifier`s are related to each other using the `related` attribute, and you see a combined `ReactiveBuilder`. @@ -550,17 +350,15 @@ final combinedNotifier = ReactiveNotifier( - **Explanation**: Here, `combinedNotifier` is a `ReactiveNotifier` that updates when any of the three notifiers (`timeHoursNotifier`, `routeNotifier`, `statusNotifier`) changes. This is useful when you have several simple states and you want them all to be connected to trigger an update in the UI together. -#### **Using with `ReactiveBuilder`** - ```dart -ReactiveBuilder.notifier( +ReactiveBuilder( notifier: combinedNotifier, - builder: (context, _, keep) { + builder: (state, keep) { return Column( children: [ - Text("Horas: ${timeHoursNotifier.value}"), - Text("Ruta: ${routeNotifier.value}"), - Text("Estado: ${statusNotifier.value ? 'Activo' : 'Inactivo'}"), + Text("Hours: ${timeHoursNotifier.value}"), + Text("Route: ${routeNotifier.value}"), + Text("State: ${statusNotifier.value ? 'Active' : 'Inactive'}"), ], ); }, @@ -572,7 +370,7 @@ ReactiveBuilder.notifier( --- -### **2. Relationship between a Main `ReactiveNotifier` and Other Complementary Notifiers** +### ** Relationship between a Main `ReactiveNotifier` and Other Complementary Notifiers** In this approach, you have a main `ReactiveNotifier` that handles a more complex class, such as a `UserInfo` object, and other complementary `ReactiveNotifier`s are related through `related`. These complementary notifiers do not need to be declared inside the main object class, but are integrated with it through the `related` attribute. @@ -613,17 +411,15 @@ final userStateNotifier = ReactiveNotifier( - **Explanation**: In this example, `userStateNotifier` is the main `ReactiveNotifier` that handles the state of `UserInfo`. `settingsNotifier` and `notificationsEnabledNotifier` are companion notifiers that handle user settings such as dark mode and enabling notifications. While they are not declared within `UserInfo`, they are related to it via `related`. -#### **Using with `ReactiveBuilder`** - ```dart ReactiveBuilder( - valueListenable: userStateNotifier.value, - builder: (context, userInfo, keep) { + notifier: userStateNotifier, + builder: ( userInfo, keep) { return Column( children: [ - Text("Usuario: ${userInfo.name}, Edad: ${userInfo.age}"), - Text("ConfiguraciΓ³n: ${settingsNotifier.value}"), - Text("Notificaciones: ${notificationsEnabledNotifier.value ? 'Habilitadas' : 'Deshabilitadas'}"), + Text("User: ${userInfo.name}, Age: ${userInfo.age}"), + Text("Configuration: ${settingsNotifier.notifier}"), + Text("Notifications: ${notificationsEnabledNotifier.notifier ? 'Active' : 'Inactive'}"), ], ); }, @@ -636,9 +432,9 @@ ReactiveBuilder( #### **Usage with `ReactiveBuilder.notifier`** ```dart -ReactiveBuilder.notifier( +ReactiveBuilder( notifier: userStateNotifier, - builder: (context, userInfo, keep) { + builder: (userInfo, keep) { return Column( children: [ ElevatedButton( @@ -652,16 +448,12 @@ ReactiveBuilder.notifier( }, ); ``` - -- **Explanation**: - We use `ReactiveBuilder.notifier` to directly observe and update the `userStateNotifier`. When the user's name or any other value changes, it is automatically updated in the UI. - --- ### **Advantages of Using `related` in `ReactiveNotifier`** 1. **Flexibility**: - You can relate simple and complex notifiers without the need to involve additional classes. This is useful for handling states that depend on multiple values ​​without overcomplicating the structure. + You can relate simple and complex notifiers without the need to involve additional classes. This is useful for handling states that depend on multiple values without overcomplicating the structure. 2. **Optimization**: When related notifiers change, the UI is automatically updated without the need to manually manage dependencies. This streamlines the workflow and improves the performance of the application. @@ -713,12 +505,12 @@ class AppDashboard extends StatelessWidget { @override Widget build(BuildContext context) { return ReactiveBuilder( - valueListenable: appState, - builder: (context, state, keep) { + notifier: appState, + builder: (state, keep) { - final user = userState.value; - final cart = cartState.value; - final settings = settingsState.value; + final user = userState.notifier.data; + final cart = cartState.notifier.data; + final settings = settingsState.notifier.data; return Column( children: [ @@ -735,8 +527,8 @@ class AppDashboard extends StatelessWidget { ``` - **Explanation**: -- Here, we directly access the values ​​of `userState`, `cartState`, and `settingsState` using `.value`. -- **Pros**: It's a quick and straightforward way to access the values ​​if you don't need to perform any extra logic on them. +- Here, we directly access the values of `userState`, `cartState`, and `settingsState` using `.notifier.data`. +- **Pros**: It's a quick and straightforward way to access the values if you don't need to perform any extra logic on them. - **Cons**: If you need to access a specific value of a related `ReactiveNotifier` and it's not directly in the `builder`, you might need something more organized, like using `keyNotifier` or the `from()` method. --- @@ -752,8 +544,8 @@ class AppDashboard extends StatelessWidget { @override Widget build(BuildContext context) { return ReactiveBuilder( - valueListenable: appState, - builder: (context, state, keep) { + notifier: appState, + builder: (state, keep) { final user = appState.from(); final cart = appState.from(); @@ -782,7 +574,7 @@ class AppDashboard extends StatelessWidget { ### **3. Using `keyNotifier` to Access Specific Notifiers** -The `keyNotifier` is useful when you want to access a related state that has a unique key within the `related` relationship. This is especially useful when you have multiple notifiers of the same type (for example, multiple `cartState`s) and you need to distinguish between them. +The `keyNotifier` is useful when you want to access a related state that has a unique key within the `related` relationship. This is especially useful when you have multiple notifiers of the same type (for example, multiple `cartState's`) and you need to distinguish between them. #### **Using `keyNotifier`** @@ -791,12 +583,12 @@ class AppDashboard extends StatelessWidget { @override Widget build(BuildContext context) { return ReactiveBuilder( - valueListenable: appState, - builder: (context, state, keep) { + notifier: appState, + builder: (state, keep) { - final user = appState.from(userState.keyNotifier); - final cart = appState.from(cartState.keyNotifier); - final settings = appState.from(settingsState.keyNotifier); + final user = appState.from(userState.keyNotifier); /// Or appState.from(userState.keyNotifier) + final cart = appState.from(cartState.keyNotifier); /// .... + final settings = appState.from(settingsState.keyNotifier); /// .... return Column( children: [ @@ -819,21 +611,6 @@ class AppDashboard extends StatelessWidget { --- -### **Summary of Ways to Access Related States** - -1. **Direct Access to `ReactiveNotifier`**: -- **Simplest way**: `userState.value or final user = userState.value;` -- **Ideal for simple, straightforward states**. - -2. **Using `from()`**: -- **Explicit access to a related state**: `final user = appState.from();` -- **Ideal for handling more complex relationships between notifiers and extracting values ​​from a specific state**. - -3. **Using `keyNotifier`**: -- **Access to a related state with a unique identifier**: `final cart = appState.from(cartState.keyNotifier);` -- **Ideal for handling notifiers of the same type and differentiating between them**. - - ### What to Avoid ```dart @@ -873,11 +650,11 @@ class ProductsScreen extends StatelessWidget { @override Widget build(BuildContext context) { return ReactiveAsyncBuilder>( - viewModel: productViewModel, - buildSuccess: (products) => ProductGrid(products), - buildLoading: () => const LoadingSpinner(), - buildError: (error, stack) => ErrorWidget(error), - buildInitial: () => const InitialView(), + notifier: productViewModel, + onSuccess: (products) => ProductGrid(products), + onLoading: () => const LoadingSpinner(), + onError: (error, stack) => ErrorWidget(error), + onInitial: () => const InitialView(), ); } } @@ -896,12 +673,12 @@ class ChatScreen extends StatelessWidget { @override Widget build(BuildContext context) { return ReactiveStreamBuilder( - streamNotifier: messagesStream, - buildData: (message) => MessageBubble(message), - buildLoading: () => const LoadingIndicator(), - buildError: (error) => ErrorMessage(error), - buildEmpty: () => const NoMessages(), - buildDone: () => const StreamComplete(), + notifier: messagesStream, + onData: (message) => MessageBubble(message), + onLoading: () => const LoadingIndicator(), + onError: (error) => ErrorMessage(error), + onEmpty: () => const NoMessages(), + onDone: () => const StreamComplete(), ); } } diff --git a/example/reactive_notifier_example.dart b/example/reactive_notifier_example.dart index f12dd31..dbe81a8 100644 --- a/example/reactive_notifier_example.dart +++ b/example/reactive_notifier_example.dart @@ -4,17 +4,19 @@ import 'package:reactive_notifier/reactive_notifier.dart'; import 'service/connection_service.dart'; import 'viewmodel/connection_state_viewmodel.dart'; + class ConnectionStateWidget extends StatelessWidget { const ConnectionStateWidget({super.key}); @override Widget build(BuildContext context) { - return ReactiveBuilder( - valueListenable: ConnectionService.instance, + return ReactiveBuilder( + notifier: ConnectionService.instance, builder: ( service, keep) { final state = service.notifier; + return Card( elevation: 4, child: Padding( diff --git a/example/service/connection_service.dart b/example/service/connection_service.dart index cf19442..b6adf8d 100644 --- a/example/service/connection_service.dart +++ b/example/service/connection_service.dart @@ -3,5 +3,5 @@ import 'package:reactive_notifier/reactive_notifier.dart'; import '../viewmodel/connection_state_viewmodel.dart'; mixin ConnectionService{ - static final ReactiveNotifier instance = ReactiveNotifier(() => ConnectionManager()); + static final ReactiveNotifier instance = ReactiveNotifier(() => ConnectionManagerVM()); } \ No newline at end of file diff --git a/example/viewmodel/connection_state_viewmodel.dart b/example/viewmodel/connection_state_viewmodel.dart index a791fb3..6548b96 100644 --- a/example/viewmodel/connection_state_viewmodel.dart +++ b/example/viewmodel/connection_state_viewmodel.dart @@ -4,8 +4,8 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:reactive_notifier/reactive_notifier.dart'; -class ConnectionManager extends ViewModelStateImpl { - ConnectionManager() : super(ConnectionState.offline); +class ConnectionManagerVM extends ViewModelStateImpl { + ConnectionManagerVM() : super(ConnectionState.offline); Timer? _reconnectTimer; bool _isReconnecting = false; diff --git a/lib/src/builder/reactive_async_builder.dart b/lib/src/builder/reactive_async_builder.dart index 9ec0fda..2e864d7 100644 --- a/lib/src/builder/reactive_async_builder.dart +++ b/lib/src/builder/reactive_async_builder.dart @@ -3,33 +3,33 @@ import 'package:flutter/material.dart'; import 'package:reactive_notifier/src/handler/async_state.dart'; class ReactiveAsyncBuilder extends StatelessWidget { - final AsyncViewModelImpl viewModel; - final Widget Function(T data) buildSuccess; - final Widget Function()? buildLoading; - final Widget Function(Object? error, StackTrace? stackTrace)? buildError; - final Widget Function()? buildInitial; + final AsyncViewModelImpl notifier; + final Widget Function(T data) onSuccess; + final Widget Function()? onLoading; + final Widget Function(Object? error, StackTrace? stackTrace)? onError; + final Widget Function()? onInitial; const ReactiveAsyncBuilder({ super.key, - required this.viewModel, - required this.buildSuccess, - this.buildLoading, - this.buildError, - this.buildInitial, + required this.notifier, + required this.onSuccess, + this.onLoading, + this.onError, + this.onInitial, }); @override Widget build(BuildContext context) { return AnimatedBuilder( - animation: viewModel, + animation: notifier, builder: (context, _) { - return viewModel.when( - initial: () => buildInitial?.call() ?? const SizedBox.shrink(), + return notifier.when( + initial: () => onInitial?.call() ?? const SizedBox.shrink(), loading: () => - buildLoading?.call() ?? + onLoading?.call() ?? const Center(child: CircularProgressIndicator.adaptive()), - success: (data) => buildSuccess(data), - error: (error, stackTrace) => buildError != null ? buildError!(error, stackTrace) : Center(child: Text('Error: $error')), + success: (data) => onSuccess(data), + error: (error, stackTrace) => onError != null ? onError!(error, stackTrace) : Center(child: Text('Error: $error')), ); }, ); @@ -42,6 +42,7 @@ class ReactiveAsyncBuilder extends StatelessWidget { /// Provides a standardized way to handle loading, success, and error states for async data. /// Base ViewModel implementation for handling asynchronous operations with state management. +@protected abstract class AsyncViewModelImpl extends ChangeNotifier { late AsyncState _state; diff --git a/lib/src/builder/reactive_builder.dart b/lib/src/builder/reactive_builder.dart index c0f5731..e342958 100644 --- a/lib/src/builder/reactive_builder.dart +++ b/lib/src/builder/reactive_builder.dart @@ -1,12 +1,10 @@ import 'dart:async'; -import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart'; import 'package:reactive_notifier/src/implements/notifier_impl.dart'; -import 'package:reactive_notifier/src/reactive_notifier.dart'; class ReactiveBuilder extends StatefulWidget { - final NotifierImpl valueListenable; + final NotifierImpl notifier; final Widget Function( T state, Widget Function(Widget child) keep, @@ -14,7 +12,7 @@ class ReactiveBuilder extends StatefulWidget { const ReactiveBuilder({ super.key, - required this.valueListenable, + required this.notifier, required this.builder, }); @@ -31,23 +29,23 @@ class _ReactiveBuilderState extends State> { @override void initState() { super.initState(); - value = widget.valueListenable.notifier; - widget.valueListenable.addListener(_valueChanged); + value = widget.notifier.notifier; + widget.notifier.addListener(_valueChanged); } @override void didUpdateWidget(ReactiveBuilder oldWidget) { super.didUpdateWidget(oldWidget); - if (oldWidget.valueListenable != widget.valueListenable) { - oldWidget.valueListenable.removeListener(_valueChanged); - value = widget.valueListenable.notifier; - widget.valueListenable.addListener(_valueChanged); + if (oldWidget.notifier != widget.notifier) { + oldWidget.notifier.removeListener(_valueChanged); + value = widget.notifier.notifier; + widget.notifier.addListener(_valueChanged); } } @override void dispose() { - widget.valueListenable.removeListener(_valueChanged); + widget.notifier.removeListener(_valueChanged); debounceTimer?.cancel(); super.dispose(); } @@ -60,12 +58,12 @@ class _ReactiveBuilderState extends State> { if (!isTesting) { debounceTimer = Timer(const Duration(milliseconds: 100), () { setState(() { - value = widget.valueListenable.notifier; + value = widget.notifier.notifier; }); }); } else { setState(() { - value = widget.valueListenable.notifier; + value = widget.notifier.notifier; }); } } diff --git a/lib/src/builder/reactive_stream_builder.dart b/lib/src/builder/reactive_stream_builder.dart index 130224d..4b91a4a 100644 --- a/lib/src/builder/reactive_stream_builder.dart +++ b/lib/src/builder/reactive_stream_builder.dart @@ -5,21 +5,21 @@ import 'package:reactive_notifier/src/handler/stream_state.dart'; import 'package:reactive_notifier/src/reactive_notifier.dart'; class ReactiveStreamBuilder extends StatefulWidget { - final ReactiveNotifier> streamNotifier; - final Widget Function(T data) buildData; - final Widget Function()? buildLoading; - final Widget Function(Object error)? buildError; - final Widget Function()? buildEmpty; - final Widget Function()? buildDone; + final ReactiveNotifier> notifier; + final Widget Function(T data) onData; + final Widget Function()? onLoading; + final Widget Function(Object error)? onError; + final Widget Function()? onEmpty; + final Widget Function()? onDone; const ReactiveStreamBuilder({ super.key, - required this.streamNotifier, - required this.buildData, - this.buildLoading, - this.buildError, - this.buildEmpty, - this.buildDone, + required this.notifier, + required this.onData, + this.onLoading, + this.onError, + this.onEmpty, + this.onDone, }); @override @@ -34,20 +34,20 @@ class _ReactiveStreamBuilderState extends State> { @override void initState() { super.initState(); - widget.streamNotifier.addListener(_onStreamChanged); - _subscribe(widget.streamNotifier.notifier); + widget.notifier.addListener(_onStreamChanged); + _subscribe(widget.notifier.notifier); } @override void dispose() { - widget.streamNotifier.removeListener(_onStreamChanged); + widget.notifier.removeListener(_onStreamChanged); _unsubscribe(); super.dispose(); } void _onStreamChanged() { _unsubscribe(); - _subscribe(widget.streamNotifier.notifier); + _subscribe(widget.notifier.notifier); } void _subscribe(Stream stream) { @@ -68,15 +68,15 @@ class _ReactiveStreamBuilderState extends State> { @override Widget build(BuildContext context) { return _state.when( - initial: () => widget.buildEmpty?.call() ?? const SizedBox.shrink(), + initial: () => widget.onEmpty?.call() ?? const SizedBox.shrink(), loading: () => - widget.buildLoading?.call() ?? + widget.onLoading?.call() ?? const Center(child: CircularProgressIndicator.adaptive()), - data: (data) => widget.buildData(data), + data: (data) => widget.onData(data), error: (error) => - widget.buildError?.call(error) ?? + widget.onError?.call(error) ?? Center(child: Text('Error: $error')), - done: () => widget.buildDone?.call() ?? const SizedBox.shrink(), + done: () => widget.onDone?.call() ?? const SizedBox.shrink(), ); } } diff --git a/lib/src/implements/notifier_impl.dart b/lib/src/implements/notifier_impl.dart index 1cfab34..e27d663 100644 --- a/lib/src/implements/notifier_impl.dart +++ b/lib/src/implements/notifier_impl.dart @@ -1,8 +1,8 @@ import 'package:flutter/foundation.dart'; -@protected /// value return. +@protected abstract class NotifierImpl extends ChangeNotifier { T _notifier; NotifierImpl(this._notifier) { @@ -35,6 +35,11 @@ abstract class NotifierImpl extends ChangeNotifier { _notifier = newState; } + void transformState( T Function(T data) data){ + _notifier = data(_notifier); + notifyListeners(); + } + @protected @override String toString() => '${describeIdentity(this)}($_notifier)'; diff --git a/pubspec.yaml b/pubspec.yaml index 24afc02..497b9f6 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,7 +1,7 @@ name: reactive_notifier description: A Dart library for managing reactive state efficiently, supporting multiples related state. -version: 2.3.1 +version: 2.4.0 homepage: https://github.com/JhonaCodes/reactive_notifier.git environment: diff --git a/test/golden_reactive_builder.test.dart b/test/golden_reactive_builder.test.dart index 68e7210..ab8ca81 100644 --- a/test/golden_reactive_builder.test.dart +++ b/test/golden_reactive_builder.test.dart @@ -18,7 +18,7 @@ void main() { GoldenTestScenario( name: 'Change value 0', child: ReactiveBuilder( - valueListenable: state, + notifier: state, builder: (value, child) { return Column( children: [ @@ -45,7 +45,7 @@ void main() { GoldenTestScenario( name: 'Change value 200', child: ReactiveBuilder( - valueListenable: state, + notifier: state, builder: (value, child) { if (value == 0) { state.updateState(200); @@ -68,7 +68,7 @@ void main() { MaterialApp( home: Scaffold( body: ReactiveBuilder( - valueListenable: valueNotifier, + notifier: valueNotifier, builder: ( value, noRebuildable) { rebuildCount++; // Contador para verificar reconstrucciones log("Widget que se reconstruye: $value"); From 729f7e413fe529ca55cbf7cd7d76b7304401f334 Mon Sep 17 00:00:00 2001 From: Jhonacode Date: Fri, 20 Dec 2024 01:21:47 +1300 Subject: [PATCH 3/4] Dart format update --- example/reactive_notifier_example.dart | 5 +---- example/service/connection_service.dart | 7 +++--- .../viewmodel/connection_state_viewmodel.dart | 6 ++--- lib/src/builder/reactive_async_builder.dart | 21 +++++++----------- lib/src/builder/reactive_builder.dart | 6 +---- lib/src/builder/reactive_stream_builder.dart | 3 +-- lib/src/implements/notifier_impl.dart | 13 ++++------- lib/src/reactive_notifier.dart | 4 ++-- lib/src/viewmodel/viewmodel_impl.dart | 2 -- test/golden_reactive_builder.test.dart | 2 +- test/reactive_notifier_test.dart | 22 ++++++++++--------- 11 files changed, 36 insertions(+), 55 deletions(-) diff --git a/example/reactive_notifier_example.dart b/example/reactive_notifier_example.dart index dbe81a8..dac1c1a 100644 --- a/example/reactive_notifier_example.dart +++ b/example/reactive_notifier_example.dart @@ -4,7 +4,6 @@ import 'package:reactive_notifier/reactive_notifier.dart'; import 'service/connection_service.dart'; import 'viewmodel/connection_state_viewmodel.dart'; - class ConnectionStateWidget extends StatelessWidget { const ConnectionStateWidget({super.key}); @@ -12,11 +11,9 @@ class ConnectionStateWidget extends StatelessWidget { Widget build(BuildContext context) { return ReactiveBuilder( notifier: ConnectionService.instance, - builder: ( service, keep) { - + builder: (service, keep) { final state = service.notifier; - return Card( elevation: 4, child: Padding( diff --git a/example/service/connection_service.dart b/example/service/connection_service.dart index b6adf8d..47214f7 100644 --- a/example/service/connection_service.dart +++ b/example/service/connection_service.dart @@ -2,6 +2,7 @@ import 'package:reactive_notifier/reactive_notifier.dart'; import '../viewmodel/connection_state_viewmodel.dart'; -mixin ConnectionService{ - static final ReactiveNotifier instance = ReactiveNotifier(() => ConnectionManagerVM()); -} \ No newline at end of file +mixin ConnectionService { + static final ReactiveNotifier instance = + ReactiveNotifier(() => ConnectionManagerVM()); +} diff --git a/example/viewmodel/connection_state_viewmodel.dart b/example/viewmodel/connection_state_viewmodel.dart index 6548b96..ad0e007 100644 --- a/example/viewmodel/connection_state_viewmodel.dart +++ b/example/viewmodel/connection_state_viewmodel.dart @@ -57,8 +57,6 @@ class ConnectionManagerVM extends ViewModelStateImpl { _reconnectTimer?.cancel(); super.dispose(); } - - } enum ConnectionState { @@ -105,7 +103,7 @@ extension ConnectionStateX on ConnectionState { ConnectionState.uploading || ConnectionState.syncing || ConnectionState.connecting => - Colors.blue, + Colors.blue, ConnectionState.waiting || ConnectionState.pendingSync => Colors.orange, _ => Colors.red, }; @@ -126,4 +124,4 @@ extension ConnectionStateX on ConnectionState { ConnectionState.pendingSync => 'Pending sync', }; } -} \ No newline at end of file +} diff --git a/lib/src/builder/reactive_async_builder.dart b/lib/src/builder/reactive_async_builder.dart index 2e864d7..38d42d9 100644 --- a/lib/src/builder/reactive_async_builder.dart +++ b/lib/src/builder/reactive_async_builder.dart @@ -29,14 +29,15 @@ class ReactiveAsyncBuilder extends StatelessWidget { onLoading?.call() ?? const Center(child: CircularProgressIndicator.adaptive()), success: (data) => onSuccess(data), - error: (error, stackTrace) => onError != null ? onError!(error, stackTrace) : Center(child: Text('Error: $error')), + error: (error, stackTrace) => onError != null + ? onError!(error, stackTrace) + : Center(child: Text('Error: $error')), ); }, ); } } - /// Base ViewModel implementation for handling asynchronous operations with state management. /// /// Provides a standardized way to handle loading, success, and error states for async data. @@ -44,12 +45,10 @@ class ReactiveAsyncBuilder extends StatelessWidget { /// Base ViewModel implementation for handling asynchronous operations with state management. @protected abstract class AsyncViewModelImpl extends ChangeNotifier { - late AsyncState _state; late bool loadOnInit; - AsyncViewModelImpl(this._state,{ this.loadOnInit = true }) :super() { - + AsyncViewModelImpl(this._state, {this.loadOnInit = true}) : super() { if (kFlutterMemoryAllocationsEnabled) { ChangeNotifier.maybeDispatchObjectCreation(this); } @@ -57,7 +56,6 @@ abstract class AsyncViewModelImpl extends ChangeNotifier { if (loadOnInit) { _initializeAsync(); } - } /// Internal initialization method that properly handles async initialization @@ -97,7 +95,7 @@ abstract class AsyncViewModelImpl extends ChangeNotifier { } @protected - void loadingState(){ + void loadingState() { _state = AsyncState.loading(); notifyListeners(); } @@ -111,12 +109,10 @@ abstract class AsyncViewModelImpl extends ChangeNotifier { } @protected - void cleanState(){ + void cleanState() { _state = AsyncState.initial(); } - - /// Check if any operation is in progress bool get isLoading => _state.isLoading; @@ -138,7 +134,7 @@ abstract class AsyncViewModelImpl extends ChangeNotifier { required R Function() loading, required R Function(T data) success, required R Function(Object? err, StackTrace? stackTrace) error, - }){ + }) { return _state.when( initial: initial, loading: loading, @@ -146,5 +142,4 @@ abstract class AsyncViewModelImpl extends ChangeNotifier { error: error, ); } - -} \ No newline at end of file +} diff --git a/lib/src/builder/reactive_builder.dart b/lib/src/builder/reactive_builder.dart index e342958..bfa2c11 100644 --- a/lib/src/builder/reactive_builder.dart +++ b/lib/src/builder/reactive_builder.dart @@ -16,7 +16,6 @@ class ReactiveBuilder extends StatefulWidget { required this.builder, }); - @override State> createState() => _ReactiveBuilderState(); } @@ -78,7 +77,7 @@ class _ReactiveBuilderState extends State> { @override Widget build(BuildContext context) { - return widget.builder( value, _noRebuild); + return widget.builder(value, _noRebuild); } } @@ -105,6 +104,3 @@ class _NoRebuildWrapperState extends State<_NoRebuildWrapper> { } bool get isTesting => const bool.fromEnvironment('dart.vm.product') == true; - - - diff --git a/lib/src/builder/reactive_stream_builder.dart b/lib/src/builder/reactive_stream_builder.dart index 4b91a4a..cd8f087 100644 --- a/lib/src/builder/reactive_stream_builder.dart +++ b/lib/src/builder/reactive_stream_builder.dart @@ -74,8 +74,7 @@ class _ReactiveStreamBuilderState extends State> { const Center(child: CircularProgressIndicator.adaptive()), data: (data) => widget.onData(data), error: (error) => - widget.onError?.call(error) ?? - Center(child: Text('Error: $error')), + widget.onError?.call(error) ?? Center(child: Text('Error: $error')), done: () => widget.onDone?.call() ?? const SizedBox.shrink(), ); } diff --git a/lib/src/implements/notifier_impl.dart b/lib/src/implements/notifier_impl.dart index e27d663..c31e09b 100644 --- a/lib/src/implements/notifier_impl.dart +++ b/lib/src/implements/notifier_impl.dart @@ -1,6 +1,5 @@ import 'package:flutter/foundation.dart'; - /// value return. @protected abstract class NotifierImpl extends ChangeNotifier { @@ -11,7 +10,6 @@ abstract class NotifierImpl extends ChangeNotifier { } } - T get notifier => _notifier; /// [updateState] @@ -35,7 +33,7 @@ abstract class NotifierImpl extends ChangeNotifier { _notifier = newState; } - void transformState( T Function(T data) data){ + void transformState(T Function(T data) data) { _notifier = data(_notifier); notifyListeners(); } @@ -44,7 +42,6 @@ abstract class NotifierImpl extends ChangeNotifier { @override String toString() => '${describeIdentity(this)}($_notifier)'; - @immutable @override bool get hasListeners => super.hasListeners; @@ -65,7 +62,6 @@ abstract class StateNotifierImpl extends ChangeNotifier { /// [updateState] /// Updates the state and notifies listeners if the value has changed. - void updateState(T newState) { if (_notifier.hashCode == newState.hashCode) { return; @@ -75,10 +71,9 @@ abstract class StateNotifierImpl extends ChangeNotifier { notifyListeners(); } - - void transformState( T Function(T data) data){ + void transformState(T Function(T data) data) { final dataNotifier = data(_notifier); - if(dataNotifier.hashCode == _notifier.hashCode){ + if (dataNotifier.hashCode == _notifier.hashCode) { return; } _notifier = data(_notifier); @@ -119,4 +114,4 @@ abstract class StateNotifierImpl extends ChangeNotifier { @protected @override bool get hasListeners => super.hasListeners; -} \ No newline at end of file +} diff --git a/lib/src/reactive_notifier.dart b/lib/src/reactive_notifier.dart index 85eaeb8..8498da5 100644 --- a/lib/src/reactive_notifier.dart +++ b/lib/src/reactive_notifier.dart @@ -115,7 +115,8 @@ Location: $trace _checkNotificationOverflow(); assert(() { - log('πŸ“ Updating state for $T: $notifier -> ${newState.runtimeType}', level: 10); + log('πŸ“ Updating state for $T: $notifier -> ${newState.runtimeType}', + level: 10); return true; }()); @@ -415,5 +416,4 @@ Available types: ${related!.map((r) => '${r.notifier.runtimeType}(${r.keyNotifie @override String toString() => '${describeIdentity(this)}($notifier)'; - } diff --git a/lib/src/viewmodel/viewmodel_impl.dart b/lib/src/viewmodel/viewmodel_impl.dart index 473c61d..bfb5b1c 100644 --- a/lib/src/viewmodel/viewmodel_impl.dart +++ b/lib/src/viewmodel/viewmodel_impl.dart @@ -95,8 +95,6 @@ abstract class ViewModelStateImpl extends StateNotifierImpl { } } - @override T get notifier => this.notifier; - } diff --git a/test/golden_reactive_builder.test.dart b/test/golden_reactive_builder.test.dart index ab8ca81..1013dd6 100644 --- a/test/golden_reactive_builder.test.dart +++ b/test/golden_reactive_builder.test.dart @@ -69,7 +69,7 @@ void main() { home: Scaffold( body: ReactiveBuilder( notifier: valueNotifier, - builder: ( value, noRebuildable) { + builder: (value, noRebuildable) { rebuildCount++; // Contador para verificar reconstrucciones log("Widget que se reconstruye: $value"); log("Widget que se reconstruye: $value"); diff --git a/test/reactive_notifier_test.dart b/test/reactive_notifier_test.dart index f43dd51..f3beedc 100644 --- a/test/reactive_notifier_test.dart +++ b/test/reactive_notifier_test.dart @@ -302,9 +302,10 @@ void main() { group('Computed States', () { test('should handle computed states', () { final baseState = ReactiveNotifier(() => 1); - final computedState = ReactiveNotifier(() => baseState.notifier * 2); - baseState - .addListener(() => computedState.updateState(baseState.notifier * 2)); + final computedState = + ReactiveNotifier(() => baseState.notifier * 2); + baseState.addListener( + () => computedState.updateState(baseState.notifier * 2)); baseState.updateState(5); expect(computedState.notifier, 10); }); @@ -313,17 +314,17 @@ void main() { final rootState = ReactiveNotifier(() => 0); final computed1 = ReactiveNotifier(() => rootState.notifier + 1); final computed2 = ReactiveNotifier(() => rootState.notifier * 2); - final computed3 = - ReactiveNotifier(() => computed1.notifier + computed2.notifier); + final computed3 = ReactiveNotifier( + () => computed1.notifier + computed2.notifier); rootState.addListener(() { computed1.updateState(rootState.notifier + 1); computed2.updateState(rootState.notifier * 2); }); - computed1.addListener( - () => computed3.updateState(computed1.notifier + computed2.notifier)); - computed2.addListener( - () => computed3.updateState(computed1.notifier + computed2.notifier)); + computed1.addListener(() => + computed3.updateState(computed1.notifier + computed2.notifier)); + computed2.addListener(() => + computed3.updateState(computed1.notifier + computed2.notifier)); rootState.updateState(5); expect(computed1.notifier, 6); @@ -336,7 +337,8 @@ void main() { test('should maintain state history', () { final historicalState = ReactiveNotifier(() => 0); final history = []; - historicalState.addListener(() => history.add(historicalState.notifier)); + historicalState + .addListener(() => history.add(historicalState.notifier)); historicalState.updateState(1); historicalState.updateState(2); historicalState.updateState(3); From eebe1271c5cca30454e3ce8b24002567462e8b81 Mon Sep 17 00:00:00 2001 From: Jhonacode Date: Fri, 20 Dec 2024 01:24:05 +1300 Subject: [PATCH 4/4] Update flutter workflow --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7c03df1..7ab7171 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,7 @@ jobs: - name: Setup Flutter uses: subosito/flutter-action@v2 with: - flutter-version: '3.24.5' + flutter-version: '3.27.1' channel: 'stable' - name: Install dependencies