diff --git a/CHANGELOG.md b/CHANGELOG.md index 49ebb9e..78543ce 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,50 @@ +## [1.1.0] - 05/01/2019 + +There is a lot of change in this version. +This is not a major release because there are no change in the design +or new functionality, excluding hot reload support (but yes, +something will break updating to the new version). + +This is a better implementation how make more clear the goal +to avoid the use of classes and mixin and prefer instead +hook functions and composition. + +The change came also from the necessity to make a better division of +responsibility. + +* removed `Hook.dispose`. + + Logic moved to a new `_DisposableController`, + how is private, because we don't want developers use class extension or mixin. + Sorry if someone was using `Hook.dispose` compose using `useEffect` instead. + +* replaced `HookState` with `StateController` + + It's a better name, and describe better the returned value of `useState`. + +* deprecated `HookState.set` (now `StateController.set`), + use `StateController.value = newValue instead` +* `useState` now return `StateController` instead of 'HookState' +* `useMemo` now implements the dispose lifecycle +* `useEffect` --like all the hooks functions-- use `useMemo` +* **added hot reload support** + + When the hock type change, because an hook function is added, + removed, or change type, + the hook will be disposed and reset to null. + There will be no break hot reloading the app. + But will be other side effects. + + We decide to not make hooks shift to the next position, + because we prefer to have the same behavior in the case you add, + remove, or change an hook function call. + +* Added Hot Reload test +* Added Hot Reload and Changelog section ro [README](README.md) + +We are also thinking to make `use` private. +Everything can be done using `useMemo` now. + ## [1.0.1] - 26/12/2018 * flutter packages get error fix diff --git a/README.md b/README.md index 0bb478a..dbbe71e 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,9 @@ Like for [React](https://reactjs.org/docs/hooks-intro.html#motivation), Hooks try to be a simple method to share stateful logic between `Component`. +The goal of thi library is to devoid class extensions and mixin. +Of course flutter is not designed for functional Component and Hooks. + ## Getting Started You should ensure that you add the flhooks @@ -20,7 +23,7 @@ as a dependency in your flutter project. ```yaml dependencies: - flhooks: "^1.0.1" + flhooks: "^1.1.0" ``` You should then run `flutter packages upgrade` @@ -98,18 +101,19 @@ It's the same as passing `() => fn` to `useMemo`. ### useState -`useState` return an `HookState`, -`HookState.value` is `initial` value passed to `useState`, -or the last passed to `HookState.set`. +`useState` return a `StateController`, +`HookState.value` is the `initial` value passed to `useState`, +or the last set using `state.value = newValue`. -Will trigger the rebuild of the `StatefulBuilder`. +`state.value = newValue` will trigger +the rebuild of the `StatefulBuilder`. ```dart final name = useState(''); // ... get the value Text(name.value); -//... update the value - onChange: (newValue) => name.set(newValue); +//... update the value and rebuild the component + onChange: (newValue) => name.value = newValue; ``` ### useEffect @@ -140,7 +144,7 @@ V useAsync(Future Function() asyncFn, V initial, List store) { var active = true; asyncFn().then((result) { if (active) { - state.set(result); + state.value = result; } }); return () { @@ -153,6 +157,38 @@ V useAsync(Future Function() asyncFn, V initial, List store) { Now you can use `useAsync` like any other hooks function. +## Hot Reload + +Hot reload is basically supported. + +When the hock type change, because an hook function is added, +removed, or change type, +the hook will be disposed and reset to null. + +However after an add or a remove, all hooks after the one how change, +can be disposed or had a reset. + +__Pay attention, will be no break hot reloading the app, +but will be other side effects.__ + +We decide to not make hooks shift to the next position, +because we prefer to have the same behavior in the case you add, +remove, or change an hook function call. + +Feel free to open a issue or fork the repository +to suggest a new implementation. + ## Example -More example in the [example](example) directory. \ No newline at end of file +More example in the [example](example) directory. + +## Changelog +Current version is __1.1.0__, +read the [changelog](CHANGELOG.md) for more info. + +## Next on flhooks + +New hooks will be added in future like `useFuture` (or `useAsync`) and `useStream`, +there will be no need to use `FutureBuilder` and `StreamBuilder` anymore. + +We are actually testing some `useIf` conditional implementation of hooks. diff --git a/example/todo_app/pubspec.yaml b/example/todo_app/pubspec.yaml index e552186..5e7e635 100644 --- a/example/todo_app/pubspec.yaml +++ b/example/todo_app/pubspec.yaml @@ -15,7 +15,7 @@ environment: dependencies: flutter: sdk: flutter - flhooks: ^1.0.0 + flhooks: ^1.0.1 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. diff --git a/lib/flhooks.dart b/lib/flhooks.dart index 95ce266..7a47f0f 100644 --- a/lib/flhooks.dart +++ b/lib/flhooks.dart @@ -3,20 +3,18 @@ library flhooks; import 'package:flutter/widgets.dart'; /// [Hook] is the same as a property in the [State] of a [StatefulWidget]. -/// [value], [store] and [dispose] will be saved in the current [State]. +/// [controller], [store] and [dispose] will be saved in the current [State]. /// /// [Hook] can only be created and modified by an [HookTransformer] function, /// consumed by the [use] function. class Hook { const Hook({ - this.value, + this.controller, this.store, - this.dispose, }); - final V value; + final V controller; final S store; - final Function dispose; } /// Define the type of an hook transformer function. @@ -26,44 +24,61 @@ class Hook { /// [HookTransformer] can only be consumed by the [use] function. typedef HookTransformer = Hook Function(Hook); -StateSetter _currentSetState; -List _currentHooks; -int _currentIndex; +class _DisposableController { + _DisposableController(this.onDispose); -void Function(StateSetter, List, int) _setHooksContext = ( - setState, - hooks, - index, -) { - _currentSetState = setState; - _currentHooks = hooks; - _currentIndex = index; -}; + final Function onDispose; + void dispose() { + if (this.onDispose != null) { + this.onDispose(); + } + } +} + +void _dispose(Hook hook) { + if (hook != null && hook.controller is _DisposableController) { + hook.controller.dispose(); + } +} + +class _HookContext { + _HookContext({ + this.setState, + this.hooks, + }); + + final StateSetter setState; + final List hooks; + int index = 0; +} + +_HookContext _currentHookContext; /// Define the type of a builder function how can use Hooks. typedef HookWidgetBuilder = Widget Function(BuildContext); class _HookBuilderState extends State { - _HookBuilderState(); + _HookBuilderState() { + _hooks = []; + } - final List hooks = []; + List _hooks; @override Widget build(BuildContext context) { - _setHooksContext(setState, hooks, 0); + _currentHookContext = _HookContext( + hooks: _hooks, + setState: setState, + ); final result = widget.builder(context); - _setHooksContext(null, null, null); + _currentHookContext = null; return result; } @override void dispose() { - hooks.forEach((hook) { - if (hook.dispose != null) { - hook.dispose(); - } - }); + _hooks.forEach(_dispose); super.dispose(); } } @@ -79,8 +94,8 @@ class _HookBuilderState extends State { /// // define a state of type double /// final example = useState(0.0); /// final onChanged = useCallback((double newValue) { -/// // call example.set for update the value in state -/// example.set(newValue); +/// // set example.value for update the value in state +/// example.value = newValue; /// }, [example]); /// return Material( /// child: Center( @@ -113,7 +128,7 @@ class HookBuilder extends StatefulWidget { } /// [use] consume the [transformer], -/// return the [Hook.value] of the [Hook] generated by the [transformer], +/// return the [Hook.controller] of the [Hook] generated by the [transformer], /// store the [Hook] in the current hooks context. /// /// All Hooks function will start with use, and call [use] directly or indirectly. @@ -130,23 +145,39 @@ class HookBuilder extends StatefulWidget { /// final asyncHook = () => AsyncTransformer(...)(); /// ``` V use(HookTransformer transformer) { - assert(_currentIndex != null, - 'the current index of the hook context cannot be null'); - assert(_currentHooks != null, - 'the current hooks of the hook context cannot be null'); - assert(_currentSetState != null, - 'the current setState of the hook contect cannot be null'); + assert( + _currentHookContext != null, 'the current hooks context cannot be null'); + assert(_currentHookContext.index != null, + 'the current index of the hook context cannot be null'); + assert(_currentHookContext.hooks != null, + 'the current hooks of the hook context cannot be null'); + assert(_currentHookContext.setState != null, + 'the current setState of the hook contect cannot be null'); + final _currentHooks = _currentHookContext.hooks; + final _currentIndex = _currentHookContext.index; if (_currentHooks.length <= _currentIndex) { _currentHooks.length = _currentIndex + 1; } - final currentHook = _currentHooks[_currentIndex]; + var currentHook = _currentHooks[_currentIndex]; + // check type change for hot reload and eventually dispose + if (currentHook != null) { + if (currentHook is! Hook) { + debugPrint( + 'Hook Type change detected, the hook will be disposed and resetted'); + _dispose(currentHook); + currentHook = null; + } + } final hook = transformer(currentHook); assert(hook != null, 'a transformer cannot return null value'); _currentHooks[_currentIndex] = hook; - _currentIndex += 1; - return hook.value; + _currentHookContext.index += 1; + return hook.controller; } +bool _storeEquals(List one, List two) => + one == two || one.every((o) => two.any((t) => t == o)); + /// Return the memoized value of [fn]. /// /// [fn] will be recalled only if [store] change. @@ -156,13 +187,13 @@ V use(HookTransformer transformer) { V useMemo(V Function() fn, List store) { return use((current) { if (current != null) { - final oldStore = current.store; - if (store.every((e) => oldStore.any((o) => o == e))) { + if (_storeEquals(store, current.store)) { return current; } + _dispose(current); } return Hook( - value: fn(), + controller: fn(), store: store, ); }); @@ -181,22 +212,7 @@ V useMemo(V Function() fn, List store) { /// /// [useEffect] is useful for async or stream subscription. void useEffect(Function Function() fn, List store) { - use((current) { - if (current != null) { - final oldStore = current.store; - if (store.every((e) => oldStore.any((o) => o == e))) { - return current; - } - if (current.dispose != null) { - current.dispose(); - } - } - return Hook( - value: null, - store: store, - dispose: fn(), - ); - }); + useMemo(() => _DisposableController(fn()), store); } /// Return the first reference to [fn]. @@ -206,39 +222,46 @@ void useEffect(Function Function() fn, List store) { /// final onClick = useCallback(() => ..., [input1, input2]); /// ``` /// It's the same as passing `() => fn` to [useMemo]. -final Function Function(Function fn, List store) useCallback = - (Function fn, List store) => useMemo(() => fn, store); - -/// Is an hook state value and setter. -class HookState { - HookState({ - @required this.value, - this.set, - }); +Function useCallback(Function fn, List store) => useMemo(() => fn, store); + +/// Is an hook state controller.. +class StateController { + StateController({ + @required V value, + @required this.setState, + }) : _value = value; + + V _value; + + V get value => _value; - V value; - void Function(V) set; + set value(V newValue) => setState(() { + _value = newValue; + }); + + final StateSetter setState; + + @Deprecated( + 'Use `state.value = newValue` instead. Will be removed in future release.') + void set(V newValue) => value = newValue; } -/// Return an [HookState], -/// [HookState.value] is [initial] or the last passed to [HookState.set]. -/// Will trigger the rebuild of the [StatefulBuilder]. +/// Return an [StateController] with +/// [StateController.value] as [initial], or the latest set.. +/// `state.value = newValue` Will trigger the rebuild of the [StatefulBuilder]. /// /// ```dart /// final name = useState(''); /// // ... get the value /// Text(name.value); -/// //... update the value -/// onChange: (newValue) => name.set(newValue); +/// //... update the value and rebuild the component +/// onChange: (newValue) => name.value = newValue; /// ``` -HookState useState(V initial) { - final setState = _currentSetState; - return useMemo(() { - final state = HookState( - value: initial, - ); - state.set = (value) => setState(() => state.value = value); - return state; - }, []); +StateController useState(V initial) { + return useMemo( + () => StateController( + value: initial, + setState: _currentHookContext.setState, + ), + []); } - diff --git a/pubspec.yaml b/pubspec.yaml index 68df0bb..54decc2 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -1,6 +1,8 @@ name: flhooks -description: Stateful function Component in flutter. React like Hooks implementation for Flutter. -version: 1.0.1 +description: Stateful functional Component in flutter and + React like Hooks implementation for Flutter. + Simple to use like calling a function. +version: 1.1.0 author: Alfredo Salzillo homepage: https://github.com/alfredosalzillo/flhooks diff --git a/test/flhooks_test.dart b/test/flhooks_test.dart index 9e08c1e..6305f61 100644 --- a/test/flhooks_test.dart +++ b/test/flhooks_test.dart @@ -5,7 +5,7 @@ import 'package:flutter_test/flutter_test.dart'; void main() { test('hooks function cannot be called outside HookBuilder', () async { expect(() { - final value = useMemo(() => 1, []); + useEffect(() => null, []); }, throwsAssertionError); }); testWidgets( @@ -20,7 +20,7 @@ void main() { builder: (BuildContext context) { final test = useState(0.0); final onChanged = useCallback((double newValue) { - test.set(newValue); + test.value = newValue; }, [test]); return MaterialApp( home: Material( @@ -69,6 +69,7 @@ void main() { child: Slider( key: sliderKey, value: value, + onChanged: (newValue) => null, ), ), ), @@ -78,4 +79,52 @@ void main() { ); expect(value, equals(1)); }); + testWidgets('hot reload doesn\'t break', + (WidgetTester tester) async { + // You can use keys to locate the widget you need to test + var sliderKey = UniqueKey(); + var value = 0.0; + // Tells the tester to build a UI based on the widget tree passed to it + await tester.pumpWidget( + HookBuilder( + builder: (BuildContext context) { + useEffect(() { + value = 1; + }, []); + return MaterialApp( + home: Material( + child: Center( + child: Slider( + key: sliderKey, + value: value, + onChanged: (newValue) => null, + ), + ), + ), + ); + }, + ), + ); + await tester.pumpWidget( + HookBuilder( + builder: (BuildContext context) { + final test = useState(1.0); + useEffect(() { + value = 1; + }, []); + return MaterialApp( + home: Material( + child: Center( + child: Slider( + key: sliderKey, + value: value, + onChanged: (newValue) => test.value = newValue, + ), + ), + ), + ); + }, + ), + ); + }); }