Michael Polt
Michael Polt

Reputation: 651

How to handle navigation using stream from inheritedWidget?

I'm using an inherited Widget to access a Bloc with some long running task (e.g. search). I want to trigger the search on page 1 and continue to the next page when this is finished. Therefore I'm listening on a stream and wait for the result to happen and then navigate to the result page. Now, due to using an inherited widget to access the Bloc I can't access the bloc with context.inheritFromWidgetOfExactType() during initState() and the exception as I read it, recommends doing this in didChangeDependencies().

Doing so this results in some weird behavior as the more often I go back and forth, the more often the stream I access fires which would lead to the second page beeing pushed multiple times. And this increases with each back and forth interaction. I don't understand why the stream why this is happening. Any insights here are welcome. As a workaround I keep a local variable _onSecondPage holding the state to avoid pushing several times to the second Page.

I found now How to call a method from InheritedWidget only once? which helps in my case and I could access the inherited widget through context.ancestorInheritedElementForWidgetOfExactType() and just listen to the stream and navigate to the second page directly from initState(). Then the stream behaves as I would expect, but the question is, does this have any other side effects, so I should rather get it working through listening on the stream in didChangeDependencides() ?

Code examples

My FirstPage widget listening in the didChangeDependencies() on the stream. Working, but I think I miss something. The more often i navigate from first to 2nd page, the second page would be pushed multiple times on the navigation stack if not keeping a local _onSecondPage variable.

  @override
  void didChangeDependencies() {
    super.didChangeDependencies();
    debugPrint("counter: $_counter -Did change dependencies called");
    // This works the first time, after that going back and forth to the second screen is opened several times
    BlocProvider.of(context).bloc.finished.stream.listen((bool isFinished) {
       _handleRouting(isFinished);
    });
  }

  void _handleRouting(bool isFinished) async {
    if (isFinished && !_onSecondPage) {
      _onSecondPage = true;
      debugPrint("counter: $_counter -   finished: $isFinished : ${DateTime.now().toIso8601String()} => NAVIGATE TO OTHER PAGE");
      await Navigator.push(
        context,
        MaterialPageRoute(builder: (context) => SecondRoute()),
      );
      _onSecondPage = false;
    } else {
      debugPrint("counter: $_counter -    finished: $isFinished : ${DateTime.now().toIso8601String()} => not finished, nothing to do now");
    }
  }

  @override
  void dispose() {
    debugPrint("counter: $_counter - disposing my homepage State");
    subscription?.cancel();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text(widget.title),
      ),
      body: Center(
        child: Column(
          mainAxisAlignment: MainAxisAlignment.center,
          children: <Widget>[
            Text(
              'You have pushed the button this many times:',
            ),
            StreamBuilder(
              stream: BlocProvider.of(context).bloc.counter.stream,
              initialData: 0,
              builder: (context, snapshot) {
                _counter = snapshot.data;
                return Text(
                  "${snapshot.data}",
                  style: Theme.of(context).textTheme.display1,
                );
              },
            ),
          ],
        ),
      ),
      floatingActionButton: FloatingActionButton(
        onPressed: _incrementCounter,
        tooltip: 'Increment',
        child: Icon(Icons.add),
      ),
    );
  }
}

A simple Bloc faking some long running work

///Long Work Bloc
class LongWorkBloc {
  final BehaviorSubject<bool> startLongWork = BehaviorSubject<bool>();
  final BehaviorSubject<bool> finished = BehaviorSubject<bool>();

  int _counter = 0;
  final BehaviorSubject<int> counter = BehaviorSubject<int>();


  LongWorkBloc() {
    startLongWork.stream.listen((bool start) {
      if (start) {
        debugPrint("Start long running work");
        Future.delayed(Duration(seconds: 1), () => {}).then((Map<dynamic, dynamic> reslut) {
          _counter++;
          counter.sink.add(_counter);
          finished.sink.add(true);
          finished.sink.add(false);
        });
      }
    });
  }

  dispose() {
    startLongWork?.close();
    finished?.close();
    counter?.close();
  }
}

Better working code

If I however remove the code to access the inherited widget from didChangeDependencies() and listen to the stream in the initState() it seems to be working properly.

Here I get hold of the inherited widget holding the stream through context.ancestorInheritedElementForWidgetOfExactType()

Is this ok to do so? Or what would be a flutter best practice in this case?

  @override
  void initState() {
    super.initState();
    //this works, but I don't know if this is good practice or has any side effects?
    BlocProvider p = context.ancestorInheritedElementForWidgetOfExactType(BlocProvider)?.widget;
    if (p != null) {
      p.bloc.finished.stream.listen((bool isFinished) {
        _handleRouting(isFinished);
      });
    }
  }

Upvotes: 1

Views: 1130

Answers (1)

fstof
fstof

Reputation: 311

Personally, I have not found any reason not to listen to BLoC state streams in initState. As long as you remember to cancel your subscription on dispose

If your BlocProvider is making proper use of InheritedWidget you should not have a problem getting your value inside of initState.

like So

  void initState() {
    super.initState();
    _counterBloc = BlocProvider.of(context);
    _subscription = _counterBloc.stateStream.listen((state) {
      if (state.total > 20) {
        Navigator.push(context,
            MaterialPageRoute(builder: (BuildContext context) {
          return TestPush();
        }));
      }
    });
  }

Here is an example of a nice BlocProvider that should work in any case

import 'package:flutter/widgets.dart';

import 'bloc_base.dart';

class BlocProvider<T extends BlocBase> extends StatefulWidget {
  final T bloc;
  final Widget child;

  BlocProvider({
    Key key,
    @required this.child,
    @required this.bloc,
  }) : super(key: key);

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

  static T of<T extends BlocBase>(BuildContext context) {
    final type = _typeOf<_BlocProviderInherited<T>>();
    _BlocProviderInherited<T> provider =
        context.ancestorInheritedElementForWidgetOfExactType(type)?.widget;
    return provider?.bloc;
  }

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

class _BlocProviderState<T extends BlocBase> extends State<BlocProvider<BlocBase>> {
  @override
  Widget build(BuildContext context) {
    return _BlocProviderInherited<T>(
      bloc: widget.bloc,
      child: widget.child,
    );
  }

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

class _BlocProviderInherited<T> extends InheritedWidget {
  final T bloc;

  _BlocProviderInherited({
    Key key,
    @required Widget child,
    @required this.bloc,
  }) : super(key: key, child: child);

  @override
  bool updateShouldNotify(InheritedWidget oldWidget) => false;
}

... and finally the BLoC

import 'dart:async';

import 'bloc_base.dart';

abstract class CounterEventBase {
  final int amount;
  CounterEventBase({this.amount = 1});
}

class CounterIncrementEvent extends CounterEventBase {
  CounterIncrementEvent({amount = 1}) : super(amount: amount);
}

class CounterDecrementEvent extends CounterEventBase {
  CounterDecrementEvent({amount = 1}) : super(amount: amount);
}

class CounterState {
  final int total;
  CounterState(this.total);
}

class CounterBloc extends BlocBase {
  CounterState _state = CounterState(0);

  // Input Streams/Sinks
  final _eventInController = StreamController<CounterEventBase>();
  Sink<CounterEventBase> get events => _eventInController;
  Stream<CounterEventBase> get _eventStream => _eventInController.stream;

  // Output Streams/Sinks
  final _stateOutController = StreamController<CounterState>.broadcast();
  Sink<CounterState> get _states => _stateOutController;
  Stream<CounterState> get stateStream => _stateOutController.stream;

  // Subscriptions
  final List<StreamSubscription> _subscriptions = [];

  CounterBloc() {
    _subscriptions.add(_eventStream.listen(_handleEvent));
  }

  _handleEvent(CounterEventBase event) async {
    if (event is CounterIncrementEvent) {
      _state = (CounterState(_state.total + event.amount));
    } else if (event is CounterDecrementEvent) {
      _state = (CounterState(_state.total - event.amount));
    }
    _states.add(_state);
  }

  @override
  void dispose() {
    _eventInController.close();
    _stateOutController.close();
    _subscriptions.forEach((StreamSubscription sub) => sub.cancel());
  }
}

Upvotes: 1

Related Questions