diff --git a/example/lib/combine_example/combine_example.dart b/example/lib/combine_example/combine_example.dart index 30a9af4..7d596b4 100644 --- a/example/lib/combine_example/combine_example.dart +++ b/example/lib/combine_example/combine_example.dart @@ -1,63 +1,54 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:ref/ref.dart'; -final _ref = CombineRef([ - ref(0), - ref('I go second'), - futureRef( - () async { - await Future.delayed( - const Duration( - seconds: 3, - ), - ); - return 'I go third'; - }, - ), - CombineRef( - [ - futureRef(() async { - return 'I go fourth'; - }), - transformRef( - Ref('HEHE'), - debounceTransformer( - const Duration( - milliseconds: 500, - ), - ), - ) - ], - (ref) { - return ref.join(','); - }, - ), -], (ref) { - return 'Combined ref: ${ref.join(',')}'; -}); +final _countRef = ref(0); +final _prefixRef = ref('Result'); + +// computedRef tự động phát hiện dependency vào _countRef và _prefixRef +final _labelRef = computedRef( + () => '${_prefixRef.state}: ${_countRef.state}', +); + +class ComputedExample extends StatelessWidget { + const ComputedExample({super.key}); -class CombineExample extends StatelessWidget { - const CombineExample({ - super.key, - }); @override Widget build(BuildContext context) { return Scaffold( + appBar: AppBar(title: const Text('Computed Example')), body: Column( mainAxisAlignment: MainAxisAlignment.center, children: [ ReactiveWidget( - ref: _ref, - builder: (context, value) => Text(value), + ref: _labelRef, + builder: (context, value) => Text( + value, + style: const TextStyle(fontSize: 24), + ), + ), + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + ElevatedButton( + onPressed: () => _countRef.update((v) => v - 1), + child: const Text('-'), + ), + const SizedBox(width: 16), + ElevatedButton( + onPressed: () => _countRef.update((v) => v + 1), + child: const Text('+'), + ), + ], ), + const SizedBox(height: 12), ElevatedButton( - onPressed: () {}, - child: const Text( - 'Update state', - ), - ) + onPressed: () { + _prefixRef.state = + _prefixRef.state == 'Result' ? 'Count' : 'Result'; + }, + child: const Text('Toggle Prefix'), + ), ], ), ); diff --git a/example/lib/improved_example/improved_example.dart b/example/lib/improved_example/improved_example.dart index c83852b..5717708 100644 --- a/example/lib/improved_example/improved_example.dart +++ b/example/lib/improved_example/improved_example.dart @@ -28,15 +28,13 @@ class User { String toString() => 'User(name: $name, age: $age)'; } -// Tạo các ref +// Tạo các ref nguồn final nameRef = ref('John'); final ageRef = ref(30); -// Sử dụng combine2Refs để kết hợp 2 ref -final userRef = combine2Refs( - nameRef, - ageRef, - (name, age) => User(name: name, age: age), +// computedRef tự động phát hiện dependency vào nameRef và ageRef +final userRef = computedRef( + () => User(name: nameRef.state, age: ageRef.state), ); // Sử dụng select để chỉ lắng nghe một phần của state @@ -67,8 +65,6 @@ class _ImprovedExampleState extends State { // Đăng ký effect và lưu hàm dispose _disposeEffect = userRef.effect((user) { print('User changed: $user'); - // Có thể thực hiện các side effect khác ở đây - // Ví dụ: lưu vào local storage, gọi API, v.v. }); } @@ -76,7 +72,7 @@ class _ImprovedExampleState extends State { void dispose() { nameController.dispose(); ageController.dispose(); - _disposeEffect(); // Hủy effect khi widget bị dispose + _disposeEffect(); super.dispose(); } @@ -91,19 +87,18 @@ class _ImprovedExampleState extends State { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - // Sử dụng RefProvider để cung cấp ref cho widget tree RefProvider( ref: userRef, child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ const Text('User Info (using RefConsumer):'), - // Sử dụng RefConsumer để tiêu thụ ref RefConsumer( builder: (context, user) { return Text( 'Name: ${user.name}, Age: ${user.age}', - style: TextStyle(fontWeight: FontWeight.bold), + style: + const TextStyle(fontWeight: FontWeight.bold), ); }, ), @@ -112,7 +107,6 @@ class _ImprovedExampleState extends State { ), ), const Text('Name (using ReactiveWidget with select):'), - // Sử dụng ReactiveWidget với select ReactiveWidget( ref: userNameRef, builder: (context, name) { @@ -154,9 +148,9 @@ class _ImprovedExampleState extends State { const SizedBox(height: 20), ElevatedButton( onPressed: () { - // Cập nhật trực tiếp userRef - userRef.state = User(name: 'Alice', age: 25); - // Cập nhật controllers để phản ánh giá trị mới + // Cập nhật các ref nguồn — userRef sẽ tự động recompute + nameRef.state = 'Alice'; + ageRef.state = 25; nameController.text = 'Alice'; ageController.text = '25'; }, diff --git a/lib/src/base/base.dart b/lib/src/base/base.dart index dd9b569..93a3049 100644 --- a/lib/src/base/base.dart +++ b/lib/src/base/base.dart @@ -1,7 +1,7 @@ export 'future_ref.dart'; export 'ref.dart'; export 'base_ref.dart'; -export 'combine_ref.dart'; +export 'computed_ref.dart'; export 'transform_ref.dart'; export 'family_ref.dart'; export 'readonly_ref.dart'; diff --git a/lib/src/base/base_ref.dart b/lib/src/base/base_ref.dart index 5cb9cf3..e2ddc32 100644 --- a/lib/src/base/base_ref.dart +++ b/lib/src/base/base_ref.dart @@ -1,5 +1,6 @@ import 'package:flutter/foundation.dart'; import 'package:ref/ref.dart'; +import 'package:ref/src/base/tracking_context.dart'; import 'package:ref/src/listener_manager/listener_manager.dart'; import 'package:ref/src/observer/global_observer_manager.dart'; import 'package:ref/src/observer/ref_observer_mixin.dart'; @@ -62,7 +63,13 @@ abstract class BaseRef with RefObserverMixin { bool shouldUpdate(T oldState, T newState) => oldState != newState; /// Lấy state hiện tại - T get state => _state; + /// + /// Nếu đang trong quá trình tracking (ComputedRef đang chạy compute), + /// ref này sẽ tự động được đăng ký là dependency. + T get state { + RefTrackingContext.onAccess?.call(this); + return _state; + } /// Cập nhật state và thông báo cho các listener /// diff --git a/lib/src/base/combine_ref.dart b/lib/src/base/combine_ref.dart deleted file mode 100644 index acea05f..0000000 --- a/lib/src/base/combine_ref.dart +++ /dev/null @@ -1,106 +0,0 @@ -import 'package:flutter/foundation.dart'; -import 'package:ref/src/base/base_ref.dart'; - -/// Một class để kết hợp nhiều ref thành một ref mới -class CombineRef extends BaseRef { - final List _refs; - final T Function(List states) _combiner; - final Map _scopeIds = {}; - - CombineRef( - this._refs, - this._combiner, - ) : super( - _combiner( - _refs.map((e) => e.state).toList(), - ), - ) { - // Đăng ký listener cho mỗi ref - for (final ref in _refs) { - final scopeId = Object().hashCode.toString(); - _scopeIds[ref] = scopeId; - - ref.addListener( - (value) { - // Tính toán state mới khi bất kỳ ref nào thay đổi - final newState = _combiner( - _refs.map((e) => e.state).toList(), - ); - - // Chỉ cập nhật nếu state thực sự thay đổi - if (shouldUpdate(state, newState)) { - state = newState; - } - }, - name: scopeId, - ); - } - } - - @override - bool shouldUpdate(T oldState, T newState) { - // Sử dụng toán tử == để so sánh state - return oldState != newState; - } - - @override - void dispose() { - // Hủy đăng ký listener cho mỗi ref - for (final entry in _scopeIds.entries) { - entry.key.removeListener(entry.value); - } - _scopeIds.clear(); - super.dispose(); - } -} - -/// Tạo một CombineRef từ danh sách các ref -/// -/// Ví dụ: -/// ```dart -/// final nameRef = ref('John'); -/// final ageRef = ref(30); -/// -/// final personRef = combineRef( -/// [nameRef, ageRef], -/// (states) => {'name': states[0], 'age': states[1]}, -/// ); -/// ``` -CombineRef combineRef( - List refs, - T Function(List states) combiner, -) { - return CombineRef( - refs, - combiner, - ); -} - -/// Tạo một CombineRef từ 2 ref -CombineRef combine2Refs( - BaseRef refA, - BaseRef refB, - R Function(A a, B b) combiner, -) { - return CombineRef( - [refA, refB], - (states) => combiner(states[0] as A, states[1] as B), - ); -} - -/// Tạo một CombineRef từ 3 ref -CombineRef combine3Refs( - BaseRef refA, - BaseRef refB, - BaseRef refC, - R Function(A a, B b, C c) combiner, -) { - return CombineRef( - [refA, refB, refC], - (states) => combiner( - states[0] as A, - states[1] as B, - states[2] as C, - ), - ); -} diff --git a/lib/src/base/computed_ref.dart b/lib/src/base/computed_ref.dart new file mode 100644 index 0000000..7d7e6e9 --- /dev/null +++ b/lib/src/base/computed_ref.dart @@ -0,0 +1,126 @@ +import 'package:ref/src/base/base_ref.dart'; +import 'package:ref/src/base/tracking_context.dart'; + +/// Một ref tự động tính toán giá trị từ các ref khác (dependency). +/// +/// [ComputedRef] theo dõi tất cả các ref được truy cập trong hàm [compute]. +/// Khi bất kỳ dependency nào thay đổi, [ComputedRef] sẽ tự động tính lại giá trị. +/// +/// Không cần khai báo dependency thủ công — chúng được phát hiện tự động +/// thông qua [RefTrackingContext] khi hàm compute chạy. +/// +/// Ví dụ: +/// ```dart +/// final firstName = ref('John'); +/// final lastName = ref('Doe'); +/// +/// // Tự động phát hiện dependency vào firstName và lastName +/// final fullName = computedRef(() => '${firstName.state} ${lastName.state}'); +/// +/// firstName.state = 'Jane'; +/// print(fullName.state); // 'Jane Doe' +/// ``` +/// +/// Hỗ trợ conditional dependency — nếu hàm compute có nhánh điều kiện, +/// chỉ những ref thực sự được truy cập mới được đăng ký: +/// ```dart +/// final showAge = ref(true); +/// final age = ref(25); +/// final name = ref('Alice'); +/// +/// final label = computedRef(() { +/// if (showAge.state) return '${name.state}, age ${age.state}'; +/// return name.state; +/// }); +/// ``` +class ComputedRef extends BaseRef { + final T Function() _compute; + + /// Lưu các dependency và scopeId tương ứng để removeListener khi cần + final Map _deps = {}; + + /// Guard tránh recompute đệ quy khi addListener gọi callback ngay lập tức + bool _isTracking = false; + + ComputedRef(this._compute) : super(_compute()) { + _track(); + } + + /// Chạy hàm compute với tracking bật để phát hiện và đăng ký dependencies. + void _track() { + _isTracking = true; + + // Xóa các dependency cũ + for (final entry in _deps.entries) { + entry.key.removeListener(entry.value); + } + _deps.clear(); + + // Bật tracking và chạy compute + final previous = RefTrackingContext.onAccess; + RefTrackingContext.onAccess = _onDepAccessed; + _compute(); // kết quả bỏ qua — chỉ dùng để phát hiện deps + RefTrackingContext.onAccess = previous; + + _isTracking = false; + } + + /// Được gọi khi một BaseRef có state được đọc trong quá trình tracking. + void _onDepAccessed(Object dep) { + if (dep is! BaseRef || _deps.containsKey(dep)) return; + final scopeId = Object().hashCode.toString(); + _deps[dep] = scopeId; + dep.addListener( + // _isTracking guard ngăn callback ngay lập tức (do addListener notify ngay) + (_) { if (!_isTracking) _recompute(); }, + name: scopeId, + ); + } + + /// Tính lại giá trị và cập nhật dependency khi có dep thay đổi. + void _recompute() { + _isTracking = true; + + // Xóa dependency cũ + for (final entry in _deps.entries) { + entry.key.removeListener(entry.value); + } + _deps.clear(); + + // Re-track với hàm compute (xử lý conditional dependency) + final previous = RefTrackingContext.onAccess; + RefTrackingContext.onAccess = _onDepAccessed; + final result = _compute(); + RefTrackingContext.onAccess = previous; + + _isTracking = false; + + // Cập nhật state sau khi đã xong tracking (tránh recursive) + super.state = result; + } + + @override + void dispose() { + for (final entry in _deps.entries) { + entry.key.removeListener(entry.value); + } + _deps.clear(); + super.dispose(); + } +} + +/// Tạo một [ComputedRef] từ hàm compute. +/// +/// Hàm [compute] sẽ được chạy ngay lập tức và mỗi khi có dependency thay đổi. +/// Các dependency được phát hiện tự động — bất kỳ ref nào có `.state` được +/// đọc bên trong hàm compute đều trở thành dependency. +/// +/// Ví dụ: +/// ```dart +/// final count = ref(0); +/// final doubled = computedRef(() => count.state * 2); +/// +/// count.state = 5; +/// print(doubled.state); // 10 +/// ``` +ComputedRef computedRef(T Function() compute) => ComputedRef(compute); diff --git a/lib/src/base/tracking_context.dart b/lib/src/base/tracking_context.dart new file mode 100644 index 0000000..7590225 --- /dev/null +++ b/lib/src/base/tracking_context.dart @@ -0,0 +1,11 @@ +/// Context nội bộ dùng bởi [ComputedRef] để tự động phát hiện dependency. +/// +/// Khi [ComputedRef] chạy hàm compute, nó set [onAccess] để nhận thông báo +/// mỗi khi một [BaseRef] nào đó được đọc state, từ đó xác định dependency. +class RefTrackingContext { + RefTrackingContext._(); + + /// Callback được gọi khi một BaseRef có state được đọc trong quá trình tracking. + /// Null khi không có ComputedRef nào đang chạy. + static void Function(Object dep)? onAccess; +} diff --git a/test/combine_ref_test.dart b/test/combine_ref_test.dart deleted file mode 100644 index 5f6a35e..0000000 --- a/test/combine_ref_test.dart +++ /dev/null @@ -1,44 +0,0 @@ -import 'package:ref/ref.dart'; -import 'package:test/test.dart'; - -void main() { - group('CombineRef', () { - test('should combine multiple refs into one', () { - final ref1 = Ref(1); - final ref2 = Ref(2); - - final combinedRef = CombineRef( - [ref1, ref2], - (states) => states.reduce((a, b) => a + b), // Tổng các giá trị - ); - - expect(combinedRef.state, equals(3)); // Ban đầu: 1 + 2 = 3 - - ref1.state = 5; // Thay đổi ref1 - expect(combinedRef.state, equals(7)); // 5 + 2 = 7 - }); - - test('should notify changes when one of the refs changes', () async { - final ref1 = Ref(1); - final ref2 = Ref(2); - - final combinedRef = CombineRef( - [ref1, ref2], - (states) => states.reduce((a, b) => a + b), - ); - - expect(combinedRef.state, equals(3)); - - ref1.state = 5; // Thay đổi ref1 - expect(combinedRef.state, equals(7)); - - ref2.state = 10; // Thay đổi ref2 - expect(combinedRef.state, equals(15)); - - await Future.delayed(Duration(milliseconds: 100)); - - // expect( - // changes, equals([3, 7, 15])); // Các giá trị lần lượt: 1+2, 5+2, 5+10 - }); - }); -} diff --git a/test/computed_ref_test.dart b/test/computed_ref_test.dart new file mode 100644 index 0000000..2d06377 --- /dev/null +++ b/test/computed_ref_test.dart @@ -0,0 +1,119 @@ +import 'package:ref/ref.dart'; +import 'package:test/test.dart'; + +void main() { + group('ComputedRef', () { + test('tính toán giá trị ban đầu từ dependency', () { + final a = Ref(1); + final b = Ref(2); + + final sum = ComputedRef(() => a.state + b.state); + + expect(sum.state, equals(3)); + }); + + test('tự động cập nhật khi dependency thay đổi', () { + final a = Ref(1); + + final doubled = ComputedRef(() => a.state * 2); + expect(doubled.state, equals(2)); + + a.state = 5; + expect(doubled.state, equals(10)); + }); + + test('theo dõi nhiều dependency', () { + final a = Ref(1); + final b = Ref(2); + + final sum = ComputedRef(() => a.state + b.state); + + a.state = 3; + expect(sum.state, equals(5)); + + b.state = 4; + expect(sum.state, equals(7)); + }); + + test('thông báo listener khi tính lại giá trị', () { + final a = Ref(0); + final doubled = ComputedRef(() => a.state * 2); + + final values = []; + doubled.addListener(values.add, name: 'test'); + + a.state = 1; + a.state = 2; + + // listener nhận giá trị ban đầu + 2 lần cập nhật + expect(values, equals([0, 2, 4])); + }); + + test('factory function computedRef hoạt động đúng', () { + final count = ref(5); + final label = computedRef(() => 'Count: ${count.state}'); + + expect(label.state, equals('Count: 5')); + + count.state = 10; + expect(label.state, equals('Count: 10')); + }); + + test('hỗ trợ conditional dependency', () { + final flag = Ref(true); + final a = Ref(1); + final b = Ref(100); + + final result = ComputedRef(() => flag.state ? a.state : b.state); + expect(result.state, equals(1)); + + // Thay đổi b khi flag=true → b không phải dependency → không recompute + b.state = 200; + expect(result.state, equals(1)); + + // Thay đổi flag → b trở thành dependency + flag.state = false; + expect(result.state, equals(200)); + + // Bây giờ b là dependency + b.state = 300; + expect(result.state, equals(300)); + }); + + test('chuỗi computed (computed phụ thuộc computed)', () { + final base = Ref(2); + final doubled = ComputedRef(() => base.state * 2); + final quadrupled = ComputedRef(() => doubled.state * 2); + + expect(quadrupled.state, equals(8)); + + base.state = 3; + expect(doubled.state, equals(6)); + expect(quadrupled.state, equals(12)); + }); + + test('dispose dọn dẹp dependency', () { + final a = Ref(1); + final computed = ComputedRef(() => a.state); + + computed.dispose(); + + // Sau dispose, thay đổi a không gây lỗi và computed giữ giá trị cũ + a.state = 99; + expect(computed.state, equals(1)); + }); + + test('không thông báo listener khi giá trị không đổi', () { + final a = Ref(5); + final same = ComputedRef(() => a.state); + + final values = []; + same.addListener(values.add, name: 'test'); + final countBefore = values.length; + + a.state = 5; // giá trị giống → shouldUpdate = false → không notify listener + expect(values.length, equals(countBefore)); + expect(same.state, equals(5)); + }); + }); +}