flutter bloc library percentage progress bar - flutter

How can a bloc show percentage progress bar
//For example, there is a regular bloc
#override
Stream<JobState> mapEventToState(JobEvent event) async* {
if (event is HardJobEvent) {
yield* _mapHardJobToState();
}
}
Stream<UpdateState> _mapHardJobToState() async* {
try {
//It is necessary to display a progress bar for this method.
await doSomeHardJob();
} catch (e) {
print(e);
}
}
doSomeHardJob() async* {
for( var i = 1 ; i < 1000; i++ ) {
//This yield does not work. Doesn't display any errors
//State not transfer
yield HardJob(nowCounter: i);
}
}

I use cubit instead of bloc. but the technique should be similar.
I have a broadcast stream controller in the payload generating function. In the event dispatcher (this should be your bloc) I listen to it and emit loading states with a double value. the bloc builder in the widgets can react to it. check put my little implementation:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart';
/// Model
class Data {
final DateTime date;
Data(this.date);
}
/// Repo
class DataRepository {
/// here comes the trick:
final StreamController<double> progress = StreamController.broadcast(); // make it broadcast to allow multiple subcribtions
Future<List<Data>> generateData() async {
/// here comes your time taking work
progress.sink.add(0.0); // set progress to 0
List<Data> payload =
[]; // this will be the data you want to transport in the loadED event
await Future.forEach(List.generate(10, (i) => i), (int i) async {
/// here comes the progess; dont send it too late otherwise
/// the loadED state will be followed by a loadING state and
/// you will see a never ending spinner
progress.sink.add(i / 10);
/// this would be like eg a http call
payload.add(Data(DateTime.now()));
await Future.delayed(
const Duration(seconds: 1)); // simulate the time consuming action
});
return payload;
}
}
/// State
#immutable
abstract class DataState {}
class DataInitial extends DataState {
DataInitial();
}
class DataLoading extends DataState {
/// this state will emit the actual progress value to the spinner
final double progress;
DataLoading(this.progress);
/// boilerplate code to tell state events apart from each other even though they are of the same type
#override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is DataLoading && other.progress == progress;
}
#override
int get hashCode => progress.hashCode;
}
class DataLoaded extends DataState {
/// this state will transport the payload data
final List<Data> data;
DataLoaded(this.data);
/// same as aboth
#override
bool operator ==(Object other) {
if (identical(this, other)) return true;
return other is DataLoaded && other.data == data;
}
#override
int get hashCode => data.hashCode;
}
/// Cubit
class DataCubit extends Cubit<DataState> {
/// Cubit works like a simple Bloc
final DataRepository dataRepository;
/// the repo will do the actual work
DataCubit({required this.dataRepository}) : super(DataInitial());
Future<void> generateData() async {
/// this will bring the progress value to the loading spinner widget.
/// each time a new progress is made a new DataLoadING state will be emitted
dataRepository.progress.stream
.listen((progress) => emit(DataLoading(progress)));
/// this await is sincere; it will take aaages; really
final payload = await dataRepository.generateData();
/// finally the payload will be sent to the widgets
emit(DataLoaded(payload));
}
}
late DataRepository dataRepository;
void main() {
/// init the repo that will do the heavy lifting like eg a db or http request
dataRepository = DataRepository();
runApp(const MyApp());
}
class MyApp extends StatelessWidget {
const MyApp({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
/// makes the cubit (like a baby bloc) available to all child widgets of the app
return BlocProvider<DataCubit>(
create: (context) => DataCubit(dataRepository: dataRepository),
child: const MaterialApp(
title: 'Flutter Progress Demo',
home: MyHomePage(),
),
);
}
}
class MyHomePage extends StatelessWidget {
const MyHomePage({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Progress Example'),
),
body: Center(
child: BlocBuilder<DataCubit, DataState>(
builder: (context, state) {
if (state is DataLoading) {
return CircularProgressIndicator.adaptive(
value: state.progress,
);
} else if (state is DataLoaded) {
List<Data> longAnticipatedData = state.data;
return ListView.builder(
itemCount: longAnticipatedData.length,
itemBuilder: (context, i) => ListTile(
title:
Text(longAnticipatedData[i].date.toIso8601String()),
));
} else {
/// initial state
return const Center(
child: Text('press the FAB'),
);
}
},
),
),
floatingActionButton: FloatingActionButton(
/// generate data; will take a good while
onPressed: () => context.read<DataCubit>().generateData(),
child: const Icon(Icons.start),
),
);
}
}

Related

Flutter awesome notifications how to fix StateError (Bad state: Stream has already been listened to.)

I am getting this error when I have signed out from my flutter app and trying to log in again:
StateError (Bad state: Stream has already been listened to.)
The code that gives me this error is on my first page:
#override
void initState() {
AwesomeNotifications().actionStream.listen((notification) async {
if (notification.channelKey == 'scheduled_channel') {
var payload = notification.payload['payload'];
var value = await FirebaseFirestore.instance
.collection(widget.user.uid)
.doc(payload)
.get();
navigatorKey.currentState.push(PageRouteBuilder(
pageBuilder: (_, __, ___) => DetailPage(
user: widget.user,
i: 0,
docname: payload,
color: value.data()['color'].toString(),
createdDate: int.parse((value.data()['date'].toString())),
documentId: value.data()['documentId'].toString(),)));
}
});
super.initState();
}
And on another page that contains the sign out code.
await FirebaseAuth.instance.signOut();
if (!mounted) return;
Navigator.pushNamedAndRemoveUntil(context,
"/login", (Route<dynamic> route) => false);
What can I do to solve this? Is it possible to stop listen to actionstream when I log out? Or should I do it in another way?
Streams over all are single use, they replace the callback hell that that ui is, at first a single use streams can seem useless but that may be for a lack of foresight. Over all (at lest for me) flutter provides all the necessary widgets to not get messy with streams, you can find them in the Implementers section of ChangeNotifier and all of those implement others like TextEditingController.
With that, an ideal (again, at least for me) is to treat widgets as clusters where streams just tie them in a use case, for example, the widget StreamBuilder is designed to build on demand so it only needs something that pumps changes to make a "live object" like in a clock, a periodic function adds a new value to the stream and the widget just needs to listen and update.
To fix your problem you can make .actionStream fit the case you are using it or change a bit how are you using it (having a monkey patch is not good but you decide if it is worth it).
This example is not exactly a "this is what is wrong, fix it", it is more to showcase a use of how pushNamedAndRemoveUntil and StreamSubscription can get implemented. I also used a InheritedWidget just because is so useful in this cases. One thing you should check a bit more is that the variable count does not stop incrementing when route_a is not in focus, the stream is independent and it will be alive as long as the widget is, which in your case, rebuilding the listening widget is the error.
import 'dart:async';
import 'package:flutter/material.dart';
void main() => runApp(App());
const String route_a = '/route_a';
const String route_b = '/route_b';
const String route_c = '/route_c';
class App extends StatelessWidget {
Stream<int> gen_nums() async* {
while (true) {
await Future.delayed(Duration(seconds: 1));
yield 1;
}
}
#override
Widget build(BuildContext ctx) {
return ReachableData(
child: MaterialApp(
initialRoute: route_a,
routes: <String, WidgetBuilder>{
route_a: (_) => Something(stream: gen_nums()),
route_b: (_) => FillerRoute(),
route_c: (_) => SetMount(),
},
),
);
}
}
class ReachableData extends InheritedWidget {
final data = ReachableDataState();
ReachableData({super.key, required super.child});
static ReachableData of(BuildContext ctx) {
final result = ctx.dependOnInheritedWidgetOfExactType<ReachableData>();
assert(result != null, 'Context error');
return result!;
}
#override
bool updateShouldNotify(ReachableData old) => false;
}
class ReachableDataState {
String? mount;
}
// route a
class Something extends StatefulWidget {
// If this widget needs to be disposed then use the other
// constructor and this call in the routes:
// Something(subscription: gen_nums().listen(null)),
// final StreamSubscription<int> subscription;
// Something({required this.subscription, super.key});
final Stream<int> stream;
Something({required this.stream, super.key});
#override
State<Something> createState() => _Something();
}
class _Something extends State<Something> {
int count = 0;
void increment_by(int i) => setState(
() => count += i,
);
#override
void initState() {
super.initState();
widget.stream.listen(increment_by);
// To avoid any funny errors you should set the subscription
// on pause or the callback to null on dispose
// widget.subscription.onData(increment_by);
}
#override
Widget build(BuildContext ctx) {
var mount = ReachableData.of(ctx).data.mount ?? 'No mount';
return Scaffold(
body: InkWell(
child: Text('[$count] Push Other / $mount'),
onTap: () {
ReachableData.of(ctx).data.mount = null;
Navigator.of(ctx).pushNamed(route_b);
},
),
);
}
}
// route b
class FillerRoute extends StatelessWidget {
const FillerRoute({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Go next'),
// Option 1: go to the next route
// onTap: () => Navigator.of(ctx).pushNamed(route_c),
// Option 2: go to the next route and extend the pop
onTap: () => Navigator.of(ctx)
.pushNamedAndRemoveUntil(route_c, ModalRoute.withName(route_a)),
),
);
}
}
// route c
class SetMount extends StatelessWidget {
const SetMount({super.key});
#override
Widget build(BuildContext ctx) {
return Scaffold(
body: InkWell(
child: Text('Set Mount'),
onTap: () {
ReachableData.of(ctx).data.mount = 'Mounted';
// Option 1: pop untill reaches the correct route
// Navigator.of(ctx).popUntil(ModalRoute.withName(route_a));
// Option 2: a regular pop
Navigator.of(ctx).pop();
},
),
);
}
}

Async Redux: How to use Events as one-to-many

This is a case.
I want to use one TimerWidget for 1+ forms.
And i don`t want to save its state in the Store.
So I created it as an Event, and realized like this.
/// Action
class TimeIsOnAction extends AppAction {
TimeIsOnAction(this.timerCounter);
final int timerCounter;
#override
Future<AppState?> reduce() async {
return state.copyWith(timerCounter: Event(timerCounter));
}
}
/// Widget
class TimerWidget extends StatelessWidget {
const TimerWidget({Key? key, required this.timerCounter}) : super(key: key);
final Event<int> timerCounter;
#override
Widget build(BuildContext context) {
final timer = timerCounter.state ?? 0;
// !!!! Consume or Not ???
timerCounter.consume();
return Center(child: Text('$timer'));
}
}
//////////////////////////////////////////////////////////////////////////////
/// Connector
class TimerWidgetConnector extends StatelessWidget {
const TimerWidgetConnector({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return StoreConnector<AppState, _Vm>(
vm: () => _Factory(),
builder: (context, vm) {
return TimerWidget(
timerCounter: vm.timerCounter,
);
},
);
}
}
///
class _Factory extends AppVmFactory {
#override
_Vm fromStore() {
return _Vm(
timerCounter: state.timerCounter,
);
}
}
///
class _Vm extends Vm {
final Event<int> timerCounter;
_Vm({
required this.timerCounter,
}) : super(equals: [timerCounter]);
}
/// Persisting
#override
Future<void> persistDifference(
{AppState? lastPersistedState, required AppState newState}) async {
if (lastPersistedState == null || lastPersistedState != newState) {
return _safeWrapperS(() async {
final json = newState.toJson();
final s = jsonEncode(json);
_saveString(_appStateKey, s);
return;
});
}
}
/// Applying 1
children: [
const Center(child: TimerWidgetConnector()),
Center(child: Text('$isDarkMode')),
/// Applying 2
10.verticalSpace,
const Center(child: TimerWidgetConnector()),
10.verticalSpace,
But! If i consume event in TimerWidget.build after applying - it works only on one Form
If i don't consume - its state automatically persisted with every event changing.
Is there recipe for that case?

Riverpod: List provider is not rebuilding

Flutter riverpod is not notifying the Consumer on the state change when the StateNotifier's type is List, while the same implementation works just fine for other types.
here, I provided a minimal reproducable example:
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return ProviderScope(
child: MaterialApp(
home: MyHomePage(),
),
);
}
}
class CounterState extends StateNotifier<List<int>> {
static final provider = StateProvider(
(ref) => CounterState(),
);
int get last {
print('last');
return state.last;
}
int get length {
print('len');
return state.length;
}
// the body of this will be provided below
add(int p) {}
CounterState() : super(<int>[0]);
}
class MyHomePage extends ConsumerWidget {
#override
Widget build(BuildContext context, watch) {
void _incrementCounter() {
final _count = Random.secure().nextInt(100);
context.read(CounterState.provider.notifier).state.add(_count);
}
var count = watch(CounterState.provider.notifier).state.length;
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text(
'You have pushed the button this many times: $count',
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: Icon(Icons.add),
),
);
}
}
as for the add method, I tried implementing it in a lot of ways, but neither works.
here is what I tried:
1: just add it straight away:
add(int p) {
state.add(p);
}
2: I also tried the solution suggested in this answer:
add(int p) {
state = [...state, p];
}
3: I tried to destroy the list entirely, and reassign it:
add(int p) {
final _state = [];
// copy [state] to [_state]
for (var item in state) {
_state.add(item);
}
// empty the state
state = [];
// add the new element
_state.add(p);
// refill [state] from [_state]
for (var item in _state) {
state.add(item);
}
print(state.length); // it continues until here and prints
}
Firstly, you are not creating the correct provider to listen to a StateNotifier. You need to change this:
static final provider = StateProvider(
(ref) => CounterState(),
);
to this:
static final provider = StateNotifierProvider<CounterState, List<int>>(
(ref) => CounterState(),
);
Please refer to the Riverpod documentation about the different types of providers.
Secondly, you are not actually watching for state changes, but you are just getting the state object from the notifier.
Change this line:
var count = watch(CounterState.provider.notifier).state.length;
to this:
final count = watch(CounterState.provider).length;
also, your increment method is not correct for StateNotifier providers. Please change this:
context.read(CounterState.provider.notifier).state.add(_count);
to this:
context.read(CounterState.provider.notifier).add(_count);
It should rebuild now when the state changes. However, you do need an implementation of your add method that actually changes the state object itself. I would suggest the second variant you mentioned, that is in my opinion the nicest way to do this:
add(int p) {
state = [...state, p];
}
#TmKVU explained well, so I'm skipping that part. You can also follow riverpod document.
here is my example of riverPod:
stateNotifierProvider
stateProvider
Your widget
import 'dart:math';
import 'package:stack_overflow/exports.dart';
class CounterState extends StateNotifier<List<int>> {
static final provider = StateNotifierProvider(
(ref) => CounterState(),
);
int get last {
print('last');
return state.last;
}
int get length {
print('len');
return state.length;
}
// the body of this will be provided below
add(int p) {}
CounterState() : super(<int>[0]);
}
class MyHomePageSSSS extends ConsumerWidget {
#override
Widget build(BuildContext context, watch) {
void _incrementCounter() {
final _count = Random.secure().nextInt(100);
context.read(CounterState.provider.notifier).state =
context.read(CounterState.provider.notifier).state..add(_count);
}
final countprovider = watch(CounterState.provider);
return Scaffold(
appBar: AppBar(),
body: Center(
child: Text(
'You have pushed the button this many times: ${countprovider.length}',
),
),
floatingActionButton: FloatingActionButton(
onPressed: _incrementCounter,
child: Icon(Icons.add),
),
);
}
}

How to navigate one page to another without changing state in Flutter using flutter_bloc

I am facing one issue in which when I am going from one page to another page using flitter BLoC, my first page rebuild before reaching to second. I am able to restrict the rebuild of the page using buildWhen in BlocBuilder, but the problem is when I come back to the first page again by back press then the page can not show the previous state widgets. I don't know how to manage navigation between pages without rebuild the page again, I am using flutter_bloc 6.1.1 below is my code.
FirstPage
class FirstPage extends StatefulWidget {
final MyData dataObj;
FirstPage({this.dataObj});
#override
_MyFirstPageState createState() => _MyFirstPageState();
}
class _MyFirstPageState extends State<FirstPage> {
FirstPageBloc _bloc = FirstPageBloc();
String _userAddress='';
#override
void initState() {
super.initState();
_bloc.add(UserInfoEvent(dataObj:widget.dataObj));
}
#override
Widget build(BuildContext context) {
return Scaffold(
resizeToAvoidBottomPadding: false,
appBar: AppBar(
title: Text(StringConstants.APP_TITLE_HEADING),
),
body: BlocListener<FirstPageBloc, FirstPageState>(
cubit: _bloc,
listenWhen: (previousState, state) {
// return true/false to determine whether or not
// to call listener with state
return true;
},
listener: (context, state) async{
if (state is LoadingState) {
print('Loading ...');
}
if (state is DataInfoState) {
_userAddress=state.userAddress;
}
if(state is ConfirmationState){
Navigator.push(context, MaterialPageRoute(builder: (context) => SecondPage(dataObj: widget.dataObj)));
}
},
child: BlocBuilder<FirstPageBloc, FirstPageState>(
//bloc: _bloc,
cubit: _bloc,
buildWhen: (previousState, state) {
// return true/false to determine whether or not
// to rebuild the widget with state
if(state is ConfirmationState){
return false;
}
return true;
},
builder: (context, state) {
if (state is LoadingState) {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
CircularProgressIndicator(valueColor:
AlwaysStoppedAnimation<Color>(ColorConstants.Primary),),
Text(StringConstants.PLEASE_WAIT)
],),
);
}
return _mainWidget();
}),
),
);
}
}
BLoC
class FirstPageBloc extends Bloc<FirstPageEvent, FirstPageState>{
FirstPageBloc() : super(InitialState());
#override
Stream<FirstPageState> mapEventToState(FirstPageEvent event) async*{
// TODO: implement mapEventToState
if(event is DataInfoEvent){
yield* _getUserData(event.dataObj);
}
if(event is ConfirmationEvent){
yield* _confirmTaskData(event.dataObj);
}
}
Stream<DelConfirmState> _confirmTaskData(MyData dataObj) async* {
yield LoadingState();
//Performing some SQLite DB operations
yield ConfirmationState();
}
Stream<DelConfirmState> _getUserData(MyData dataObj) async* {
yield LoadingState();
String userAddress='ABDC001, PIN- 0091910, 5th Main USA';
//Fetching User data from SQLite database and passing to UI
yield DataInfoState(userAddress:userAddress);
}
}
State
abstract class FirstPageState extends Equatable {}
///This is our initial state
class InitialState extends FirstPageState {
#override
List<Object> get props => null;
}
//This state will call for loading the progress var
class LoadingState extends FirstPageState {
#override
List<Object> get props => [];
}
//This state will call for loading the progress var
class ErrorState extends FirstPageState {
final String errorMessage;
ErrorState({#required this.errorMessage});
#override
List<Object> get props => [];
}
//This state will retun the userdata
class DataInfoState extends FirstPageState {
final String userAddress;
DataInfoState({#required this.userAddress});
#override
// TODO: implement props
List<Object> get props => [];
}
class TaskConfirmationState extends FirstPageState {
ConfirmationState({});
#override
// TODO: implement props
List<Object> get props => [];
}
Event
abstract class FirstPageEvent extends Equatable {}
class GetUserInfoEvent extends FirstPageEvent {
final MyData dataObj;
GetUserInfoEvent({this.taskObj});
#override
List<Object> get props => [];
}
class ConfirmationEvent extends FirstPageEvent {
final MyData dataObj
ConfirmationEvent({this.dataObj});
#override
List<Object> get props => [];
}
Please advise
Thank You
You need to provide your bloc at a higher level widget, then you need to get it from the context. In this way the state will persist even through navigation.
You can do that wrapping your widget like this:
BlocProvider(
create: (context) => FirstPageBloc(),
child: FirstPage(),
)
and then inside of initState you can get it like this:
_bloc = BlocProvider.of<FirstPageBloc>(context);

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);
});
}
}
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());
}
}