5

I seem to lose application state whenever I perform a hot reload.

I am using a BloC provider to store application state. This is passed at the App level in the main.dart and consumed on a child page. On the initial load of the view, the value is shown. I can navigate around the application and the state persists. However, when I perform a hot reload, I lose the values and seemingly the state.

How can I fix this issue so that state is preserved on Hot Reload?

Bloc Provider

abstract class BlocBase {
  void dispose();
}

class BlocProvider<T extends BlocBase> extends StatefulWidget {
  BlocProvider({
    Key key,
    @required this.child,
    @required this.bloc,
  }): super(key: key);

  final T bloc;
  final Widget child;

  @override
  _BlocProviderState<T> createState() => _BlocProviderState<T>();

  static T of<T extends BlocBase>(BuildContext context){
    final type = _typeOf<BlocProvider<T>>();
    BlocProvider<T> provider = context.ancestorWidgetOfExactType(type);
    return provider.bloc;
  }

  static Type _typeOf<T>() => T;
}

class _BlocProviderState<T> extends State<BlocProvider<BlocBase>>{
  @override
  void dispose(){
    widget.bloc.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context){
    return widget.child;
  }
}
class MyApp extends StatelessWidget {
  // This widget is the root of your application.
  @override
  Widget build(BuildContext context) {
    return BlocProvider<ApplicationStateBloc>(
      bloc: ApplicationStateBloc(),
      child: MaterialApp(
        title: 'Handshake',
        theme: ThemeData(
          primarySwatch: Colors.blue,
        ),
        home: LoadingPage(),
      )
    );
  }
}
class ProfileSettings extends StatefulWidget {
  @override
  _ProfileSettingsState createState() => _ProfileSettingsState();
}

class _ProfileSettingsState extends State<ProfileSettings>{
  ApplicationStateBloc _applicationStateBloc;

  @override
  void initState() {
    super.initState();
    _applicationStateBloc = BlocProvider.of<ApplicationStateBloc>(context);
  }

  @override
  void dispose() {
    _applicationStateBloc?.dispose();
    super.dispose();
  }

  Widget emailField() {
    return StreamBuilder<UserAccount>(
      stream: _applicationStateBloc.getUserAccount,
      builder: (context, snapshot){
        if (snapshot.hasData) {
          return Text(snapshot.data.displayName, style: TextStyle(color: Color(0xFF151515), fontSize: 16.0),);
        }
        return Text('');
      },
    );
  }

  @override
  Widget build(BuildContext context) {

    return BlocProvider<ApplicationStateBloc>(
      bloc: _applicationStateBloc,
      child: Scaffold(
        backgroundColor: Colors.white,
        body: SafeArea(
          child: Column(
            children: <Widget>[
              emailField(),
              .... // rest of code
class ApplicationStateBloc extends BlocBase {

  var userAccountController = BehaviorSubject<UserAccount>();
  Function(UserAccount) get updateUserAccount => userAccountController.sink.add;
  Stream<UserAccount> get getUserAccount => userAccountController.stream;

  @override
  dispose() {
    userAccountController.close();
  }

}
KennyP
  • 53
  • 1
  • 5
  • I think you should move `_applicationStateBloc = BlocProvider.of(context);` to `didChangeDependencies` instead of the `initState` method – Shababb Karim Apr 17 '19 at 09:15
  • Thank you for your suggestion, it did not work. @override void didChangeDependencies() { super.didChangeDependencies(); _applicationStateBloc = BlocProvider.of(context); } – KennyP Apr 17 '19 at 13:27

2 Answers2

1

I was facing the same problem. Inherited widgets make it hard disposing bloc's resources. Stateful widget, on the other hand, allows disposing, but in the implementation you're using it doesn't persist the bloc in the state causing state loss on widgets rebuild.

After some experimenting I came up with an approach that combines the two:

class BlocHolder<T extends BlocBase> extends StatefulWidget {
  final Widget child;
  final T Function() createBloc;

  BlocHolder({
    @required this.child,
    @required this.createBloc
  });

  @override
  _BlocHolderState createState() => _BlocHolderState();
}

class _BlocHolderState<T extends BlocBase> extends State<BlocHolder> {
  T _bloc;

  Function hello;

  @override
  void initState() {
    super.initState();
    _bloc = widget.createBloc();
  }

  @override
  Widget build(BuildContext context) {
    return BlocProvider(
      child: widget.child,
      bloc: _bloc,
    );
  }

  @override
  void dispose() {
    _bloc.dispose();
    super.dispose();
  }
}

Bloc holder creates bloc in createState() and persists it. It also disposes bloc's resources in dispose().

class BlocProvider<T extends BlocBase> extends InheritedWidget {
  final T bloc;

  const BlocProvider({
    Key key,
    @required Widget child,
    @required T bloc,
  })
      : assert(child != null),
        bloc = bloc,
        super(key: key, child: child);

  static T of<T extends BlocBase>(BuildContext context) {
    final provider = context.inheritFromWidgetOfExactType(BlocProvider) as BlocProvider;
    return provider.bloc;
  }

  @override
  bool updateShouldNotify(BlocProvider old) => false;
}

BlocProvider, as the name suggests, is only responsible for providing the bloc to nested widgets.

All the blocs extend BlocBase class

abstract class BlocBase {
  void dispose();
}

Here's a usage example:

class RouteHome extends MaterialPageRoute<ScreenHome> {
 RouteHome({List<ModelCategory> categories, int position}): super(builder: 
    (BuildContext ctx) => BlocHolder(
        createBloc: () => BlocMain(ApiMain()),
        child: ScreenHome(),
      ));
    }
AlexKost
  • 2,792
  • 4
  • 22
  • 42
0

You are losing the state because your bloc is being retrieved in the _ProfileSettingsState's initState() thus, it won't change even when you hot-reload because that method is only called only once when the widget is built.

Either move it to the build() method, just before returning the BlocProvider

@override
Widget build(BuildContext context) {
  _applicationStateBloc = BlocProvider.of<ApplicationStateBloc>(context);

  return BlocProvider<ApplicationStateBloc>(
    bloc: _applicationStateBloc,
    child: Scaffold(
      backgroundColor: Colors.white,
     ....

or to the didUpdateWidget method which is called anytime the widget state is rebuild.

Have in mind that if you are using a non-broadcast stream in your bloc you may get an exception if you try to listen to a stream that is already being listened to.

Miguel Ruivo
  • 16,035
  • 7
  • 57
  • 87
  • Thanks for your suggestions, I've tried your suggestion and it still does not seem to work. The block uses BehaviorSubject. I've done the following -> @override void didChangeDependencies() { super.didChangeDependencies(); _applicationStateBloc = BlocProvider.of(context); } – KennyP Apr 17 '19 at 13:24
  • The problem with the `BehaviorSubject` is that it won't trigger any event unless the `value` reference itself changes. If you have for example, a `BehaviorSubject>` and if the list that you're working on changes a value, you add as value another reference (eg. `List.from(oldList)`). Make sure that this is not your issue and that you might be misleading with something else. – Miguel Ruivo Apr 17 '19 at 14:33
  • rxDart API indicates that subscribers will automatically get the last value added to the stream "A special StreamController that captures the latest item that has been added to the controller, and emits that as the first item to any new listener. This subject allows sending data, error and done events to the listener. The latest item that has been added to the subject will be sent to any new listeners of the subject. After that, any new events will be appropriately sent to the listeners. It is possible to provide a seed value that will be emitted if no items have been added to the subject." – KennyP Apr 18 '19 at 00:59
  • Exactly. That’s what I’m talking about. The reference that you give to the behavior subject acts as a seed but if the reference is unchanged, no event will be triggered thus the changes might not update. – Miguel Ruivo Apr 19 '19 at 01:00