Riverpod: Set state of StateNotifierProvider in dispose method - riverpod

I have the following Riverpod StateNotifierProvider:
class DisconnectString extends StateNotifier<String> {
DisconnectString() : super('default string value');
void change(String text) => state = text;
}
final disconnectString = StateNotifierProvider<DisconnectString, String>(
(ref) => DisconnectString());
Depending on the current screen, the disconnect string should be different. Currently I'm able to change the string when entering a new screen in the initState method:
#override
void initState() {
Future.delayed(Duration.zero, () {
ref.read(disconnectString.notifier).change('A new string value');
});
}
However, I'm not able to set it back to the default value in the dispose method.
#override
void dispose() {
ref.read(disconnectString.notifier).change('default string value');
}
I just get "Looking up a deactivated widget's ancestor is unsafe"
I could not find any stackOverflow Riverpod issues regarding this. However, I tried all the solutions in this ticket Calling a method with Provider.of(context) inside of dispose() method cause "Looking up a deactivated widget's ancestor is unsafe.", without success.
For example I tried to store the Reader (ref.read) in a variable and use that in the dispose method, but it didn't work.

Related

How to call A-BLoC function from B-BLoC function?

So, like most, I am new to Flutter, Bloc and Firebase Analytics. There are several ways to sync blocs by making one listen to another, but it quite doesn't match my situation as I'd like to keep track of some "Analytics related state variables".
AnalyticsBloc extends Bloc<AnalyticsEvent, AnalyticsState> {
final FirebaseAnalytics analytics;
final FirebaseAnalyticsObserver observer;
#override
FirebaseAnalyticsState get initialState => FirebaseAnalyticsState.initial();
void setScreen(String currentScreen) {
// sends and stores the currentScreen in Bloc State
dispatch(SetScreenEvent(currentScreen));
}
void sendEvent(String eventName) {
// uses the stored currentScreen in Bloc State
dispatch(SendEventEvent(eventName));
}
...
}
class AppBloc extends Bloc<AppEvent, AppState> {
#override
AppState get initialState => AppState.initial();
void someApplicationEvent() {
// Problem: is there any way to trigger sendEvent from this Bloc?
sendEvent('someAppEventTriggered');
dispatch(ResetAppEvent());
}
}
Problem: is there any way to trigger AnalyticsBloc.sendEvent from AppBloc?
Or should I just design it differently?
Pass blocA instance to blocB constructor and call function of blocA

Flutter stream doesn't listen or detect change

I have problem, i have 3 dart files,
home.dart contain button with onclick:
final cartEmiter = CartEmitter();
cartEmiter.emitCart("add_cart");
cart.dart contain:
class CartEmitter {
StreamController _controller = StreamController.broadcast();
void emitCart(action) {
_controller.add(action);
// print(action);
}
Stream get cartAction => _controller.stream;
}
and in main.dart I have this code to change the cart badge.
StreamSubscription _cartCountSubscribtion;
int _cartCount = 0;
#override
void initState() {
_cartCountSubscribtion = CartEmitter().cartAction.listen((action) {
print(action);
setState(() {
_cartCount++;
});
});
super.initState();
}
#override
void dispose() {
_cartCountSubscribtion.cancel();
super.dispose();
}
But it doesn't work, no error, no output printed.
Is my code wrong or how to listen to change?
You create a new CartEmitter in the initState function, and another one in the onclick code. Those two are not connected in any way, so the event you emit with the emitCart call is emitted on a different CartEmitter than the one you listen to.
You need to share the same CartEmitter instance between the initState and onclick code.
Alternatively, if you know that you will only ever need one CartEmitter, you can make the _controller static, so the same controller (and stream) is shared between all instances of CartEmitter.
In that case, you can make emitCart and cartAction static too, and never create any CartEmitter instance at all.

flutter bloc pattern navigation back results in bad state

I have a problem/question regarding the bloc plattern with flutter.
Currently, i am starting my app like this
class _MyAppState extends State<MyApp> {
#override
Widget build(BuildContext context) {
return BlocProvider(
bloc: MyBloc(),
child: MaterialApp(
title: "MyApp",
home: MyHomePage(),
routes: {
'/homePage': (context) => MyHomePage(),
'/otherPage': (context) => OtherPage(),
'/otherPage2': (context) => OtherPage2(),
...
},
));
So that i can retrieve/access myBloc like
myBloc = BlocProvider.of(context) as MyBloc;
and the data represented by the state like
BlocBuilder<MyBlocEvent, MyObject>(
bloc: myBloc,
builder: (BuildContext context, MyObject myObject) {
....
var t = myObject.data;
....
myBloc.onFirstEvent();
...
};
wherever i need it.
MyBloc is implemented like this:
abstract clas MyBlocEvent {}
class FirstEvent extends MyBlocEvent {}
class SecondEvent extends MyBlocEvent {}
class MyBloc extends Bloc<MyBlocEvent , MyObject>
void onFirstEvent()
{
dispatch(FirstEvent());
}
void onSecondEvent()
{
dispatch(SecondEvent());
}
#override
Stream<MyObject> mapEventToState( MyObject state, MyBlocEvent event) async* {
if (event is FirstEvent) {
state.data = "test1";
}
else if (event is SecondEvent) {
state.otherData = 5;
}
yield state;
}
The problem i now have, is that as soon as i change on of the state values and call
Navigator.pop(context)
to go back in the current stack, i can't change anything is the state anymore because the underlying stream seems to be closed. It fails with the message:
Another exception was thrown: Bad state: Cannot add new events after calling close"
Now this only happens after i call pop. If i only push new screens i can happily change the state data without any problems.
Am i doing something wrong regarding the Navigation here or is there something else i didn't catch regarding flutter or the bloc pattern itself?
Bad state: Cannot add new events after calling close
This error means that you are calling add on a StreamController after having called close:
var controller = StreamController<int>();
controller.close();
controller.add(42); // Bad state: Cannot add new events after calling close
It is likely related to you calling close inside the dispose method the "wrong" widget.
A good rule of thumb is to never dispose/close an object outside of the widget that created it. This ensure that you cannot use an object already disposed of.
Hope this helps in your debugging.
The navigation of the app depends on your widget designs.
I use stateless widgets and render the view using bloc's data.
Whenever i navigate to another page, i would pop the current widget and navigate to the next widget.
The next stateless widget declare the bloc,
then in your subsequent stateless widgets should contain calls like MyBloc.dispatch(event(param1: value1, param2: value2));
In MyBloc, you need to set the factory of your state that contains final values;
#override
Stream<MyObject> mapEventToState( MyObject state, MyBlocEvent event) async* {
if (event is FirstEvent) {
// set it in the state, so this code is omitted
// state.data = "test1";
// add this
yield state.sampleState([], "test1");
}
else if (event is SecondEvent) {
// state.otherData = 5;
yield state.sampleState([], 5);
} else {
yield state.sampleState([], null);
}
The MyObjectState needs to be setup like this,
class MyObjectState {
final List<Bar> bars;
final String Foo;
const MyObjectState(
{this.bars,
this.foo,
});
factory MyObjectState.sampleState(List<Bar> barList, String value1) {
return MyObjectState(bars: barList, foo: message);
}
}
So that the stateless widget can use the bloc like this
MyBloc.currentState.sampleState.foo
You can try run Felix Angelov's flutter project.
Login Flow Example

Connecting a sink of BLoC with another BLoC

I am using the BLoC pattern as described at the Google IO talk.
I have a simple BLoC which is used to display a alert in the UI whenever a string is added to messageSink:
class AlertBloc {
final _message = BehaviorSubject<String>();
AlertBloc() {}
Stream<String> get message => _message.stream;
Sink<String> get messageSink => _message.sink;
void dispose() {
_message.close(); }
}
Elsewhere in the app, I have another BLoC which needs to add a string to messageSink, when a certain condition is met.
I noticed it is not a good idea to provide the whole BLoC from the Google I/O repo for the talk, and they provide advice for connecting a stream from a BLoC to another BLoC sink:
Note that we are not providing [CartBloc] to the
[ProductSquareBloc] directly, although it would be easier to
implement. BLoCs should not depend on other BLoCs (separation of
concerns). They can only communicate with each other using
streams. In this case, the [CartBloc.items] output plugs into the
[ProductSquareBloc.cartItems] input.
My question is how to connect a sink from a BLoC to another BLoC stream?
Here is a simple example for you. Imagine the following two BLoCs:
The first one exposes a Stream and populates it with some values:
class ProducerBLoC {
//Controller is private - you do not want to expose it
final StreamController<int> _productionController = StreamController<int>();
//Instead, you expose a stream
Stream<int> get production => _productionController.stream;
//This method generates some values and puts them to stream
void produceValue() {
_productionController.sink.add(1);
_productionController.sink.add(2);
_productionController.sink.add(3);
}
//Don't forget to close your controllers
void dispose() {
_productionController.close();
}
}
The other one exposes a Sink and processes values that are put into it.
class ConsumerBLoC {
//Controller is private - you do not want to expose it
final StreamController<int> _consumptionController = StreamController<int>();
//Instead, you expose a sink
StreamSink<int> get consumption => _consumptionController.sink;
//In class constructor we start listening to the stream of values
ConsumerBLoC() {
_consumptionController.listen((value) {_consumeValue(value);} );
//or simply: _consumptionController.listen(_consumeValue); //theese are the same
}
//This method generates some values and puts them to stream
void consumeValue(int value) {
//Do something with the value
print('Value processed: $value');
}
//Don't forget to close your controllers
void dispose() {
_consumptionController.close();
}
}
Now, the task is to connect production stream to consumption sink. As you have correctly noticed, you do not want for any of two BLoCs to know anything about existence of the other one. So none of the two should hold references to the other one or even create instances of another one. Instead, you connect them using your Widget class:
//Define some widget to represent main screen of your application
class MainScreen extends StatefulWidget {
#override
State<StatefulWidget> createState() => _MainScreenState();
}
//And define a state for this widget (state does not need to be public)
class _MainScreenState extends State<MainScreen> {
//You define both blocks here
ProducerBLoC _producer = new ProducerBLoC();
ConsumerBLoC _consumer = new ConsumerBLoC();
//Now, either do it in _MainScreenState constructor, or in the initState() method
#override
void initState() {
super.initState();
//Connect production stream with consumption sink
_producer.production.listen((value) => _consumer.consumption.add(value));
//Or, beautifully: _producer.production.pipe(_consumer.consumption);
}
#override
Widget build(BuildContext context) {
//The exact implementation does not matter in current context
}
//And don't forget to close your controllers
#override
dispose() {
super.dispose();
_producer.dispose();
_consumer.dispose();
}
}
This way, any value generated by ProducerBLoC will immediately be consumed by ConsumerBLoC. And, what's the most important, - both BLoCs are completely independent from one another!
The exact same way as you'd do with streams: Passing it as parameter
class Bloc {
final Sink<int> _external;
Bloc(this._external);
}

A build function returned null The offending widget is: StreamBuilder<Response>

I'm new to Flutter and I'm trying to accomplish a simple thing:
I want to create a signup functionality using BLoC pattern and streams.
For the UI part I have a stepper, that on the very last step should fire a request to the server with the collected data.
I believe I have everything working until the StreamBuilder part. StreamBuilders are meant to return Widgets, however, in my case I don't need any widgets returned, if it's a success I want to navigate to the next screen, otherwise an error will be displayed in ModalBottomSheet.
StreamBuilder is complaining that no widget is returned.
Is there anything else that could be used on the View side to act on the events from the stream?
Or is there a better approach to the problem?
If you don't need to render anything, don't use StreamBuilder to begin with.
StreamBuilder is a helper widget used to display the content of a Stream.
What you want is different. Therefore you can simply listen to the Stream manually.
The following will do:
class Foo<T> extends StatefulWidget {
Stream<T> stream;
Foo({this.stream});
#override
_FooState createState() => _FooState<T>();
}
class _FooState<T> extends State<Foo<T>> {
StreamSubscription streamSubscription;
#override
void initState() {
streamSubscription = widget.stream.listen(onNewValue);
super.initState();
}
void onNewValue(T event) {
Navigator.of(context).pushNamed("my/new/route");
}
#override
void dispose() {
streamSubscription.cancel();
super.dispose();
}
#override
Widget build(BuildContext context) {
return Container();
}
}

Resources