Skip to content

Commit

Permalink
Merge pull request #3 from JhonaCodes/reactive_builder_viewmodel
Browse files Browse the repository at this point in the history
Reactive view model
  • Loading branch information
JhonaCodes authored Feb 3, 2025
2 parents f7481d4 + f1417d2 commit 33be565
Show file tree
Hide file tree
Showing 9 changed files with 359 additions and 62 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
# 2.5.0
- Implement `ReactiveViewModelBuilder` for complex state management.

# 2.4.2
- Some dart format.

Expand Down
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Add this to your package's `pubspec.yaml` file:

```yaml
dependencies:
reactive_notifier: ^2.4.2
reactive_notifier: ^2.5.0
```
## Quick Start
Expand Down Expand Up @@ -270,7 +270,7 @@ class CartViewModel extends ViewModelImpl<CartModel> {
Here we create the repository instance and the `ViewModelImpl`:

```dart
final cartViewModel = ReactiveNotifier<CartViewModel>((){
final cartViewModelNotifier = ReactiveNotifier<CartViewModel>((){
final cartRepository = CartRepository();
return CartViewModel(cartRepository);
});
Expand All @@ -283,17 +283,17 @@ final cartViewModel = ReactiveNotifier<CartViewModel>((){
Finally, we are going to display the cart status in the UI using `ReactiveBuilder`, which will automatically update when the status changes.

```dart
ReactiveBuilder<CartViewModel>(
notifier: cartViewModel,
builder: ( viewModel, keep) {
ReactiveViewModelBuilder<CartModel>(
notifier: cartViewModelNotifier.notifier,
builder: ( carModel, keep) {
return Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
if (viewModel.data.isEmpty)
if (carModel.isEmpty)
keep(Text("Loading cart...")),
if (viewModel.data.isNotEmpty) ...[
if (carModel.isNotEmpty) ...[
keep(Text("Products in cart:")),
...viewModel.data.map((item) => Text(item)).toList(),
...carModel.map((item) => Text(item)).toList(),
keep(const SizedBox(height: 20)),
Text("Total: \$${viewModel.total.toStringAsFixed(2)}"),
keep(const SizedBox(height: 20)),
Expand All @@ -304,7 +304,7 @@ ReactiveBuilder<CartViewModel>(
ElevatedButton(
onPressed: () {
// Add a new product
cartViewModel.notifier.agregarProducto("Producto C", 29.99);
cartViewModelNotifier.notifier.agregarProducto("Producto C", 29.99);
},
child: Text("Agregar Producto C"),
),
Expand All @@ -314,7 +314,7 @@ ReactiveBuilder<CartViewModel>(
ElevatedButton(
onPressed: () {
// Empty cart
cartViewModel.notifier.myCleaningCarFunction();
cartViewModelNotifier.notifier.myCleaningCarFunction();
},
child: Text("Vaciar Carrito"),
Expand Down
18 changes: 9 additions & 9 deletions lib/reactive_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,25 @@
library reactive_notifier;

/// Export the base [ReactiveNotifier] class which provides basic state management functionality.
export 'package:reactive_notifier/src/reactive_notifier.dart';
/// Export [ReactiveAsyncBuilder] and [ReactiveStreamBuilder]
export 'package:reactive_notifier/src/builder/reactive_async_builder.dart';

/// Export the [ReactiveBuilder] widget which listens to a [ReactiveNotifier] and rebuilds
/// itself whenever the value changes.
export 'package:reactive_notifier/src/builder/reactive_builder.dart';
export 'package:reactive_notifier/src/builder/reactive_stream_builder.dart';

/// Export the [AsyncState]
export 'package:reactive_notifier/src/handler/async_state.dart';

/// Export [ReactiveAsyncBuilder] and [ReactiveStreamBuilder]
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/viewmodel_impl.dart';

/// Export RepositoryImpl
export 'package:reactive_notifier/src/implements/repository_impl.dart';

/// Export ServiceImpl
export 'package:reactive_notifier/src/implements/service_impl.dart';

/// Export the base [ReactiveNotifier] class which provides basic state management functionality.
export 'package:reactive_notifier/src/reactive_notifier.dart';

/// Export ViewModelImpl
export 'package:reactive_notifier/src/viewmodel/viewmodel_impl.dart';
142 changes: 142 additions & 0 deletions lib/src/builder/reactive_builder.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'package:flutter/material.dart';
import 'package:reactive_notifier/src/implements/notifier_impl.dart';

/// Reactive Builder for simple state or direct model state.
class ReactiveBuilder<T> extends StatefulWidget {
final NotifierImpl<T> notifier;
final Widget Function(
Expand Down Expand Up @@ -103,4 +104,145 @@ class _NoRebuildWrapperState extends State<_NoRebuildWrapper> {
Widget build(BuildContext context) => child;
}

/// [ReactiveViewModelBuilder]
/// ReactiveViewModelBuilder is a specialized widget for handling ViewModel states
/// It's designed to work specifically with StateNotifierImpl implementations
/// and provides efficient state management and rebuilding mechanisms
///
class ReactiveViewModelBuilder<T> extends StatefulWidget {
/// [StateNotifierImpl]
/// The notifier should be a StateNotifierImpl that manages the ViewModel's data
/// T represents the data type being managed, not the ViewModel class itself
///
final StateNotifierImpl<T> notifier;

/// Builder function that creates the widget tree
/// Takes two parameters:
/// - state: Current state of type T
/// - keep: Function to prevent unnecessary rebuilds of child widgets
///
final Widget Function(
T state,
Widget Function(Widget child) keep,
) builder;

const ReactiveViewModelBuilder({
super.key,
required this.notifier,
required this.builder,
});

@override
State<ReactiveViewModelBuilder<T>> createState() =>
_ReactiveBuilderStateViewModel<T>();
}

/// State class for ReactiveViewModelBuilder
/// Handles state management and widget rebuilding
class _ReactiveBuilderStateViewModel<T>
extends State<ReactiveViewModelBuilder<T>> {
/// Current value of the state
late T value;

/// Cache for widgets that shouldn't rebuild
final Map<String, _NoRebuildWrapperViewModel> _noRebuildWidgets = {};

/// Timer for debouncing updates
Timer? debounceTimer;

@override
void initState() {
super.initState();
// Initialize with current data from notifier
value = widget.notifier.data;
// Subscribe to changes
widget.notifier.addListener(_valueChanged);
}

@override
void didUpdateWidget(ReactiveViewModelBuilder<T> oldWidget) {
super.didUpdateWidget(oldWidget);
// Handle notifier changes by updating subscriptions
if (oldWidget.notifier != widget.notifier) {
oldWidget.notifier.removeListener(_valueChanged);
value = widget.notifier.data;
widget.notifier.addListener(_valueChanged);
}
}

@override
void dispose() {
// Cleanup subscriptions and timer
widget.notifier.removeListener(_valueChanged);
debounceTimer?.cancel();
super.dispose();
}

/// Handles state changes from the notifier
/// Implements debouncing to prevent too frequent updates
void _valueChanged() {
// Cancel existing debounce timer
debounceTimer?.cancel();

// Debounce updates with 100ms delay
if (!isTesting) {
debounceTimer = Timer(const Duration(milliseconds: 100), () {
setState(() {
value = widget.notifier.data;
});
});
} else {
// Immediate update during testing
setState(() {
value = widget.notifier.data;
});
}
}

/// Creates or retrieves a cached widget that shouldn't rebuild
Widget _noRebuild(Widget keep) {
final key = keep.hashCode.toString();
if (!_noRebuildWidgets.containsKey(key)) {
_noRebuildWidgets[key] = _NoRebuildWrapperViewModel(builder: keep);
}
return _noRebuildWidgets[key]!;
}

@override
Widget build(BuildContext context) {
return widget.builder(value, _noRebuild);
}
}

/// Widget wrapper that prevents rebuilds of its children
/// Used by the _noRebuild function to optimize performance
class _NoRebuildWrapperViewModel extends StatefulWidget {
/// The widget to be wrapped and prevented from rebuilding
final Widget builder;

const _NoRebuildWrapperViewModel({required this.builder});

@override
_NoRebuildWrapperStateViewModel createState() =>
_NoRebuildWrapperStateViewModel();
}

/// State for _NoRebuildWrapperViewModel
/// Maintains a single instance of the child widget
class _NoRebuildWrapperStateViewModel
extends State<_NoRebuildWrapperViewModel> {
/// Cached instance of the child widget
late Widget child;

@override
void initState() {
super.initState();
// Store the initial widget
child = widget.builder;
}

@override
Widget build(BuildContext context) => child;
}

bool get isTesting => const bool.fromEnvironment('dart.vm.product') == true;
10 changes: 0 additions & 10 deletions lib/src/implements/notifier_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -98,16 +98,6 @@ abstract class StateNotifierImpl<T> extends ChangeNotifier {
@override
String toString() => '${describeIdentity(this)}($data)';

@protected
@override
void addListener(VoidCallback listener) {
super.addListener(listener);
}

@protected
@override
void removeListener(VoidCallback listener) => super.removeListener(listener);

@protected
@override
void dispose() => super.dispose();
Expand Down
4 changes: 3 additions & 1 deletion lib/src/reactive_notifier.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import 'dart:developer';
import 'implements/notifier_impl.dart';

import 'package:flutter/foundation.dart';

import 'implements/notifier_impl.dart';

/// A reactive state management solution that supports:
/// - Singleton instances with key-based identity
/// - Related states management
Expand Down
2 changes: 1 addition & 1 deletion lib/src/viewmodel/viewmodel_impl.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,9 @@ import 'dart:developer';

import 'package:flutter/foundation.dart';
import 'package:reactive_notifier/src/tracker/state_tracker.dart';
import '../implements/repository_impl.dart';

import '../implements/notifier_impl.dart';
import '../implements/repository_impl.dart';

/// [ViewModelImpl]
/// Base ViewModel implementation with repository integration for domain logic and data handling.
Expand Down
32 changes: 1 addition & 31 deletions pubspec.yaml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: reactive_notifier
description: A Dart library for managing reactive state efficiently, supporting multiples related state.

version: 2.4.2
version: 2.5.0
homepage: https://github.com/JhonaCodes/reactive_notifier.git

environment:
Expand All @@ -22,33 +22,3 @@ dev_dependencies:

flutter:
uses-material-design: true
# To add assets to your package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images

# To add custom fonts to your package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package
Loading

0 comments on commit 33be565

Please sign in to comment.