brianegan / flutter_redux

A library that connects Widgets to a Redux Store
MIT License
1.65k stars 219 forks source link

null safety #206

Closed lukepighetti closed 3 years ago

lukepighetti commented 3 years ago

Is there a roadmap for null safety in flutter_redux?

brianegan commented 3 years ago

Heya -- the Dart team has been managing the migration of the Redux package. That work's just completed, so now I need to make some updates to this library.

I can't give a specific date, but I'll try to get to it in the next couple of weeks!

lukepighetti commented 3 years ago

Thanks for the update, much appreciated!

sunderee commented 3 years ago

I've created my own personal implementation of flutter_redux with null safety support, so maybe this will be useful when migrating.

Here are the modified typedef callbacks

typedef ViewModelBuilder<ViewModel> = Widget Function(
  BuildContext context,
  ViewModel vm,
);

typedef StoreConverter<S, ViewModel> = ViewModel Function(
  Store<S?>? store,
);

typedef OnInitCallback<S> = void Function(
  Store<S?>? store,
);

typedef OnDisposeCallback<S> = void Function(
  Store<S?>? store,
);

typedef IgnoreChangeTest<S> = bool Function(S state);

typedef OnWillChangeCallback<ViewModel> = void Function(
  ViewModel previousViewModel,
  ViewModel newViewModel,
);

typedef OnDidChangeCallback<ViewModel> = void Function(ViewModel viewModel);

typedef OnInitialBuildCallback<ViewModel> = void Function(ViewModel viewModel);

and this is the "main" file

class StoreProvider<S> extends InheritedWidget {
  final Store<S?>? _store;

  const StoreProvider({
    Key? key,
    required Store<S?>? store,
    required Widget child,
  })   : assert(store != null),
        _store = store,
        super(key: key, child: child);

  static Store<S?>? of<S>(BuildContext context, {bool listen = true}) {
    final provider = (listen
        ? context.dependOnInheritedWidgetOfExactType<StoreProvider<S>>()
        : context
            .getElementForInheritedWidgetOfExactType<StoreProvider<S>>()
            ?.widget) as StoreProvider<S>;

    return provider._store;
  }

  @override
  bool updateShouldNotify(StoreProvider<S> oldWidget) =>
      _store != oldWidget._store;
}

class StoreConnector<S, ViewModel> extends StatelessWidget {
  final ViewModelBuilder<ViewModel?> builder;
  final StoreConverter<S?, ViewModel?> converter;

  final bool distinct;

  final OnInitCallback<S?>? onInit;
  final OnDisposeCallback<S?>? onDispose;
  final bool rebuildOnChange;

  final IgnoreChangeTest<S?>? ignoreChange;
  final OnWillChangeCallback<ViewModel?>? onWillChange;
  final OnDidChangeCallback<ViewModel?>? onDidChange;
  final OnInitialBuildCallback<ViewModel?>? onInitialBuild;

  const StoreConnector({
    Key? key,
    required this.builder,
    required this.converter,
    this.distinct = false,
    this.onInit,
    this.onDispose,
    this.rebuildOnChange = true,
    this.ignoreChange,
    this.onWillChange,
    this.onDidChange,
    this.onInitialBuild,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return _StoreStreamListener<S, ViewModel>(
      store: StoreProvider.of<S>(context),
      builder: builder,
      converter: converter,
      distinct: distinct,
      onInit: onInit,
      onDispose: onDispose,
      rebuildOnChange: rebuildOnChange,
      ignoreChange: ignoreChange,
      onWillChange: onWillChange,
      onDidChange: onDidChange,
      onInitialBuild: onInitialBuild,
    );
  }
}

class StoreBuilder<S> extends StatelessWidget {
  static Store<S?> _identity<S>(Store<S?> store) => store;
  final ViewModelBuilder<Store<S?>?> builder;

  final bool rebuildOnChange;

  final OnInitCallback<S?>? onInit;
  final OnDisposeCallback<S?>? onDispose;
  final OnWillChangeCallback<Store<S?>?>? onWillChange;
  final OnDidChangeCallback<Store<S?>?>? onDidChange;
  final OnInitialBuildCallback<Store<S?>?>? onInitialBuild;

  const StoreBuilder({
    Key? key,
    required this.builder,
    this.onInit,
    this.onDispose,
    this.rebuildOnChange = true,
    this.onWillChange,
    this.onDidChange,
    this.onInitialBuild,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return StoreConnector<S, Store<S>>(
      builder: builder,
      converter: _identity as Store<S>? Function(Store<S?>?),
      rebuildOnChange: rebuildOnChange,
      onInit: onInit,
      onDispose: onDispose,
      onWillChange: onWillChange,
      onDidChange: onDidChange,
      onInitialBuild: onInitialBuild,
    );
  }
}

class _StoreStreamListener<S, ViewModel> extends StatefulWidget {
  final ViewModelBuilder<ViewModel?> builder;
  final StoreConverter<S?, ViewModel?> converter;
  final Store<S?>? store;
  final bool rebuildOnChange;
  final bool distinct;
  final OnInitCallback<S?>? onInit;
  final OnDisposeCallback<S?>? onDispose;
  final IgnoreChangeTest<S?>? ignoreChange;
  final OnWillChangeCallback<ViewModel?>? onWillChange;
  final OnDidChangeCallback<ViewModel?>? onDidChange;
  final OnInitialBuildCallback<ViewModel?>? onInitialBuild;

  const _StoreStreamListener({
    Key? key,
    required this.builder,
    required this.store,
    required this.converter,
    this.distinct = false,
    this.onInit,
    this.onDispose,
    this.rebuildOnChange = true,
    this.ignoreChange,
    this.onWillChange,
    this.onDidChange,
    this.onInitialBuild,
  }) : super(key: key);

  @override
  State<StatefulWidget> createState() {
    return _StoreStreamListenerState<S, ViewModel>();
  }
}

class _StoreStreamListenerState<S, ViewModel>
    extends State<_StoreStreamListener<S, ViewModel>> {
  Stream<ViewModel>? _stream;
  ViewModel? _latestValue;
  ConverterError? _latestError;
  S? _lastConvertedState;

  @override
  void initState() {
    if (widget.onInit != null) {
      widget.onInit!(widget.store);
    }

    _computeLatestValue();

    if (widget.onInitialBuild != null) {
      WidgetsBinding.instance?.addPostFrameCallback((_) {
        widget.onInitialBuild!(_latestValue);
      });
    }

    _createStream();

    super.initState();
  }

  @override
  void dispose() {
    if (widget.onDispose != null) {
      widget.onDispose!(widget.store);
    }

    super.dispose();
  }

  @override
  void didUpdateWidget(_StoreStreamListener<S, ViewModel> oldWidget) {
    _computeLatestValue();

    if (widget.store != oldWidget.store) {
      _createStream();
    }

    super.didUpdateWidget(oldWidget);
  }

  void _computeLatestValue() {
    try {
      _latestError = null;
      _latestValue = widget.converter(widget.store);
    } catch (e, s) {
      _latestValue = null;
      _latestError = ConverterError(e, s);
    }
  }

  @override
  Widget build(BuildContext context) {
    return widget.rebuildOnChange
        ? StreamBuilder<ViewModel>(
            stream: _stream,
            builder: (context, snapshot) {
              if (_latestError != null)
                throw _latestError ??
                    ConverterError(
                      'Unknown error',
                      StackTrace.empty,
                    );

              return widget.builder(
                context,
                _latestValue,
              );
            },
          )
        : _latestError != null
            ? throw _latestError ??
                ConverterError(
                  'Unknown error',
                  StackTrace.empty,
                )
            : widget.builder(context, _latestValue);
  }

  void _createStream() {
    _stream = widget.store?.onChange
        .where((state) {
          final ifStateChanged = !identical(
            _lastConvertedState,
            widget.store?.state,
          );
          _lastConvertedState = widget.store?.state;
          return ifStateChanged;
        })
        .where((state) {
          if (widget.ignoreChange != null) {
            return !widget.ignoreChange!(widget.store?.state);
          }

          return true;
        })
        .map((state) => widget.converter(widget.store))
        .where((viewModel) {
          if (widget.distinct) {
            return viewModel != _latestValue;
          }

          return true;
        })
        .transform(
          StreamTransformer.fromHandlers(
            handleData: (viewModel, sink) {
              _latestError = null;

              if (widget.onWillChange != null) {
                widget.onWillChange!(_latestValue, viewModel);
              }

              _latestValue = viewModel;

              if (widget.onDidChange != null) {
                WidgetsBinding.instance?.addPostFrameCallback((_) {
                  widget.onDidChange!(_latestValue);
                });
              }

              if (viewModel != null) {
                sink.add(viewModel);
              }
            },
            handleError: (error, stackTrace, sink) {
              _latestValue = null;
              _latestError = ConverterError(error, stackTrace);
              sink.addError(error, stackTrace);
            },
          ),
        );
  }
}

class ConverterError extends Error {
  final Object error;
  final StackTrace stackTrace;

  ConverterError(this.error, this.stackTrace);

  @override
  String toString() => '$runtimeType: $error';
}

Kind regards, and much success in the future! You've done an amazing job with this library already!

brianegan commented 3 years ago

Thanks @sunderee, that'll certainly help me get started. Really appreciate the code and kind words!

Oleh-Sv commented 3 years ago

@brianegan Thanks for your library

PR for null safety #208

CarGuo commented 3 years ago

Looking forward to release

brianegan commented 3 years ago

Publish 0.8.0 which supports null safety