rrousselGit / state_notifier

ValueNotifier, but outside Flutter and with some extra perks
MIT License
311 stars 28 forks source link

useStateNotifier hook #48

Open derolf opened 3 years ago

derolf commented 3 years ago

Can we have a useStateNotifier hook to be used with flutter_hooks.

rich-j commented 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.

venkatd commented 3 years ago

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.

rich-j commented 3 years ago

@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).

venkatd commented 3 years ago

@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.

TarekkMA commented 2 years ago

I have published a package that does just that. https://pub.dev/packages/hooks_state_notifier.