Open derolf opened 3 years ago
We needed the same so created this:
import 'package:flutter/material.dart';
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:state_notifier/state_notifier.dart';
/// Hook provides this object with both the notifier and state as properties
class StateNotifierState<State, Notifier extends StateNotifier<State>> {
final Notifier notifier;
final State state;
StateNotifierState(this.notifier, this.state);
}
/// Manages a [StateNotifier] automatically disposed.
///
/// Change [keys] to reinitialize.
///
/// See also:
/// * [StateNotifier], the managed object.
StateNotifierState<State, Notifier> useStateNotifier<State, Notifier extends StateNotifier<State>>(
Notifier Function() stateNotifierBuilder,
{List<Object>? keys}) =>
use(_StateNotifierHook<State, Notifier>(stateNotifierBuilder, keys: keys));
/// Hook object for a StateNotifier
class _StateNotifierHook<State, Notifier extends StateNotifier<State>>
extends Hook<StateNotifierState<State, Notifier>> {
final Notifier Function() stateNotifierBuilder;
const _StateNotifierHook(this.stateNotifierBuilder, {List<Object>? keys}) : super(keys: keys);
@override
_StateNotifierHookState<State, Notifier> createState() => _StateNotifierHookState<State, Notifier>();
}
/// Hook object state that is provided as the current state
// Don't implement "didUpdateHook", key changes will init a new state and dispose the old state
class _StateNotifierHookState<State, Notifier extends StateNotifier<State>>
extends HookState<StateNotifierState<State, Notifier>, _StateNotifierHook<State, Notifier>> {
late final Notifier _notifier;
late final Function() _stopListening;
late StateNotifierState<State, Notifier> _stateNotifierState;
@override
void initHook() {
super.initHook();
_notifier = hook.stateNotifierBuilder();
_stopListening = _notifier.addListener((State s) {
setState(() => _stateNotifierState = StateNotifierState(_notifier, s));
});
}
@override
StateNotifierState<State, Notifier> build(BuildContext context) => _stateNotifierState;
@override
void dispose() {
_stopListening();
}
@override
String get debugLabel => 'useStateNotifier<$State,$Notifier>';
}
The above hook will manage objects that extends StateNotifier
. For state we use freezed
objects.
class DateTimeEntryNotifier extends StateNotifier<DateTimeEntryState> { ... }
@freezed class DateTimeEntryState ...
To create the hook provide a builder for the StateNotifier
. One annoying aspect is that you need to specify the types since it seems that Dart can't extract the state type from the builder function return type.
final dateTimeEntry = useStateNotifier<DateTimeEntryState, DateTimeEntryNotifier>(
() => DateTimeEntryNotifier(initialInstant));
dateTimeEntry.state.time // <-- access state values
dateTimeEntry.notifier.setTime(...) // <-- use notifier functions
We haven't had time to create the tests and documentation needed to formally submit it.
We needed something like this as well and here is our implementation:
import 'package:flutter/widgets.dart' show BuildContext;
import 'package:flutter_hooks/flutter_hooks.dart';
import 'package:state_notifier/state_notifier.dart';
/// Subscribes to a [StateNotifier] and marks the widget as needing build
/// whenever the state is updated.
///
/// See also:
/// * [StateNotifier]
T useStateNotifier<T>(StateNotifier<T> notifier) {
return use(_StateNotifierHook(notifier));
}
class _StateNotifierHook<T> extends Hook<T> {
const _StateNotifierHook(this.notifier);
final StateNotifier<T> notifier;
@override
_StateNotifierStateHook<T> createState() =>
// ignore: invalid_use_of_protected_member
_StateNotifierStateHook<T>(notifier.state);
}
class _StateNotifierStateHook<T> extends HookState<T, _StateNotifierHook<T>> {
_StateNotifierStateHook(T initialState) : _state = initialState;
T _state;
void Function()? _removeListener;
@override
void initHook() {
super.initHook();
_removeListener = hook.notifier.addListener(_listener);
}
@override
void didUpdateHook(_StateNotifierHook<T> oldHook) {
super.didUpdateHook(oldHook);
if (hook.notifier != oldHook.notifier) {
_removeListener?.call();
_removeListener = hook.notifier.addListener(_listener);
}
}
@override
T build(BuildContext context) {
return _state;
}
void _listener(T state) {
setState(() {
_state = state;
});
}
@override
void dispose() {
_removeListener?.call();
super.dispose();
}
}
Example usage in a build method for a HookWidget
:
final selection = useStateNotifier(selectionNotifier);
@rich-j you may be able to simplify some of the require generic parameters and the function signature. As you can see in the sample code above, type inference allows you to omit a lot of the <generic, type> annotations. Also, it might be more idiomatic to accept the StateNotifier directly rather than a builder.
(Take a look at the implementation of useValueListenable
in flutter_hooks
for a good reference.)
Looking at your implementation, I suppose we should add a debugLabel
and an optional keys
parameter to our implementation.
@venkatd our initial implementation was similar to yours and didn't use a builder. We found that we (always?) needed that implementation paired with useMemoised
which is a builder. Also we replaced useReducer
hooks which provides a Store
object that provides both state
and dispatch
as a matched pair - so we chose to return a StateNotifierState
object that provides the state
and notifier
as matched pairs. We could name our implementation useStateNotifierBuilder
since it's not idiomatic but it's practical and removed the source of a few issues we had with the idiomatic implementation.
As you are probably aware, Dart's type inference (and variance) are not as capable as many other languages (e.g. Scala) so we document places where it failed. In our builder implementation Dart didn't infer the state type from the builder function so we just noted that it needs help (this was last year and could be different now since Dart with NNBD was released).
@rich-j thanks for sharing your motivation behind the decisions. Could come in handy for those who need to create a state notifier in that way. In our case, the majority of StateNotifier
objects came injected as dependencies so they were already instantiated. In the few cases where they weren't we were able to add an extra line to useStateNotifier(...)
For an implementation that gets integrated into the library (such as flutter_hooks_state_notifier
?), it may make sense to match the API of useValueListenable
since StateNotifier is an implementation of ValueNotifier
but without the Flutter dependency.
I have published a package that does just that. https://pub.dev/packages/hooks_state_notifier.
Can we have a
useStateNotifier
hook to be used withflutter_hooks
.