How to call setState outside class/stateful widget? - dart

I have the following variables
String _warningMessage;
bool _warningVisibility;
Which I want to update via a Class which implements an interface
class _UserSignupInterface extends _SignupSelectUsernamePageState
implements UserSignupInterface {
#override
void onSuccess() {
_hideWarning();
_navigateToUserPage();
}
#override
void onError(String message) {
_isSignupClickable = true;
if(message != null) {
_displayWarning(message);
}
}
}
with the _displayWarning code (which is inside the _SignupSelectUsernamePageState)
void _displayWarning(String message) {
if (message != null) {
setState(() {
widget._warningMessage = message;
widget._warningVisibility = true;
});
}
}
However, whenever I call the _displayWarning(message) from outside the _SignupSelectUsernamePageState. I get an error saying
Unhandled Exception: setState() called in constructor
Is there a proper way of updating these variable states outside their class? Which in my case, I'm calling the _displayWarning(message) from another class that implements an interface

You have to decide whether this is a value that is changed internally within the widget, or if that's a value that changes externally to it.
If it's internal, the common thing is to place them in the State class with the _ on them, they could start with a value for instance set on initState and every time they change you call setState to indicate that.
However, if they change outside the widget, then you place them on the StatefulWidget class (as you seem to have done), you leave them without the _ as they are actually public and you even make them final and place them in the constructor to allow them to be set.
In this last case, if in the State class you must be aware of a change in the widget, you can implement didUpdateWidget, but that's not mandatory.
Of course you can mix both things, having a _warningMessage in the State, so you can update it with setState, but with an initial value defined in initState that comes from the widget.
Again, if the widget changes externally, you can again update the value of the _warningMessage with the new widgets value.
Something like that: (I didn't test this code)
class YourWidget extends StatefulWidget {
YourWidget({this.warningMessage});
final String warningMessage;
#override
State<YourWidget> createState() => new _YourWidgetState();
}
class _YourWidgetState extends State<YourWidget> {
String _warningMessage;
#override
void initState() {
super.initState();
_warningMessage = widget.warningMessage;
}
#override
didUpdateWidget(ReorderableListSimple oldWidget) {
super.didUpdateWidget(oldWidget);
_warningMessage = widget.warningMessage;
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text(_warningMessage),
RaisedButton(
child: Text("Change Message"),
onPressed: () {
setState(() {
_warningMessage = "new message from within the State class";
});
}
)
],
);
}
}
So in this example you can change the warningMessage externally, like in the parent Widget you are able to pass a different message. However, if you need, you can also set it internally using setState, as it's happening in the button's onPressed.
What you might check is wether you actually need that property exposed in the Widget, maybe you don't! Then, the example would look like that:
class YourWidget extends StatefulWidget {
#override
State<YourWidget> createState() => new _YourWidgetState();
}
class _YourWidgetState extends State<YourWidget> {
String _warningMessage;
#override
void initState() {
super.initState();
_warningMessage = "default message, no need for widget";
}
#override
Widget build(BuildContext context) {
return Column(
children: <Widget>[
Text(_warningMessage),
RaisedButton(
child: Text("Change Message"),
onPressed: () {
setState(() {
_warningMessage = "new message from within the State class";
});
}
)
],
);
}
}

Just create a static value in the state of your widget class, then when you build the widget, set it's value to the widget. So whenever you want to call it to setState(), just call the static value.

Related

flutter: Unhandled Exception: Bad state: Cannot add new events after calling close

I am trying to use the bloc pattern to manage data from an API and show them in my widget. I am able to fetch data from API and process it and show it, but I am using a bottom navigation bar and when I change tab and go to my previous tab, it returns this error:
Unhandled Exception: Bad state: Cannot add new events after calling
close.
I know it is because I am closing the stream and then trying to add to it, but I do not know how to fix it because not disposing the publishsubject will result in memory leak.
here is my Ui code:
class CategoryPage extends StatefulWidget {
#override
_CategoryPageState createState() => _CategoryPageState();
}
class _CategoryPageState extends State<CategoryPage> {
#override
void initState() {
serviceBloc.getAllServices();
super.initState();
}
#override
void dispose() {
serviceBloc.dispose();
super.dispose();
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: serviceBloc.allServices,
builder: (context, AsyncSnapshot<ServiceModel> snapshot) {
if (snapshot.hasData) {
return _homeBody(context, snapshot);
}
if (snapshot.hasError) {
return Center(
child: Text('Failed to load data'),
);
}
return CircularProgressIndicator();
},
);
}
}
_homeBody(BuildContext context, AsyncSnapshot<ServiceModel> snapshot) {
return Stack(
Padding(
padding: EdgeInsets.only(top: screenAwareSize(400, context)),
child: _buildCategories(context, snapshot))
],
);
}
_buildCategories(BuildContext context, AsyncSnapshot<ServiceModel> snapshot) {
return Padding(
padding: EdgeInsets.symmetric(vertical: 20),
child: GridView.builder(
gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 3, crossAxisSpacing: 3.0),
itemCount: snapshot.data.result.length,
itemBuilder: (BuildContext context, int index) {
return InkWell(
child: CategoryWidget(
title: snapshot.data.result[index].name,
icon: Icons.phone_iphone,
),
onTap: () {},
);
},
),
);
}
here is my bloc code:
class ServiceBloc extends MainBloc {
final _repo = new Repo();
final PublishSubject<ServiceModel> _serviceController =
new PublishSubject<ServiceModel>();
Observable<ServiceModel> get allServices => _serviceController.stream;
getAllServices() async {
appIsLoading();
ServiceModel movieItem = await _repo.getAllServices();
_serviceController.sink.add(movieItem);
appIsNotLoading();
}
void dispose() {
_serviceController.close();
}
}
ServiceBloc serviceBloc = new ServiceBloc();
I did not include the repo and API code because it is not in the subject of this error.
Use StreamController.isClosed to check if the controller is closed or not, if not closed add data to it.
if (!_controller.isClosed)
_controller.sink.add(...); // safe to add data as _controller isn't closed yet
From Docs:
Whether the stream controller is closed for adding more events.
The controller becomes closed by calling the close method. New events cannot be added, by calling add or addError, to a closed controller.
If the controller is closed, the "done" event might not have been delivered yet, but it has been scheduled, and it is too late to add more events.
If the error is actually caused by the code you posted, I'd just add a check to ensure no new events are added after dispose() was called.
class ServiceBloc extends MainBloc {
final _repo = new Repo();
final PublishSubject<ServiceModel> _serviceController =
new PublishSubject<ServiceModel>();
Observable<ServiceModel> get allServices => _serviceController.stream;
getAllServices() async {
// do nothing if already disposed
if(_isDisposed) {
return;
}
appIsLoading();
ServiceModel movieItem = await _repo.getAllServices();
_serviceController.sink.add(movieItem);
appIsNotLoading();
}
bool _isDisposed = false;
void dispose() {
_serviceController.close();
_isDisposed = true;
}
}
ServiceBloc serviceBloc = new ServiceBloc();
I run into same error and noticed that if you check isClosed, the screen is not updated. In your code you have to remove the last line from Bloc file:
ServiceBloc serviceBloc = new ServiceBloc();
and put this line in CategoryPage just before the initState(). This way your widget is creating and disposing the bloc. Before, the widget only disposes the bloc but it is never re-created when the widget is re-created.
besides the provided solution I think you should also drain the stream allServices used in your ServiceBloc with:
#override
void dispose() {
...
allServices?.drain();
}
#cwhisperer is absolutely right. Initialize and dispose your block inside widget just like bellow.
final ServiceBloc serviceBloc = new ServiceBloc();
#override
void initState() {
serviceBloc.getAllServices();
super.initState();
}
#override
void dispose() {
serviceBloc.dispose();
super.dispose();
}
and delete ServiceBloc serviceBloc = new ServiceBloc(); from your class ServiceBloc
You should not worry about memory leak while using flutter_bloc as
When using bloc you do not need to close the bloc manually, if you have used a bloc provider to inject the bloc. Bloc Providers handle that for you out of the box as mentioned in the flutter_bloc docs.
BlocProvider is responsible for creating the bloc, it will automatically handle closing the bloc
You can test this in your application. Try printing on the close() override of bloc.
If the Screen at which the bloc was provided is removed from navigation stack then the close() method for that given bloc is called out of the box.
ServiceBloc serviceBloc = new ServiceBloc();
// remove this code
// don't init class in the same page that will cause of bad state.
I also faced this issue in production, and I realized that we should either dispose BehaviorSubject (or any other StreamController) when the Widget is disposed or Check to see if Stream is closed before adding new value.
Here is a nice extension to do all the job:
extension BehaviorSubjectExtensions <T> on BehaviorSubject<T> {
set safeValue(T newValue) => isClosed == false ? add(newValue) : () {};
}
You can use it like so:
class MyBloc {
final _data = BehaviorSubject<String>();
void fetchData() {
// get your data from wherever it is located
_data.safeValue = 'Safe to add data';
}
void dispose() {
_data.close();
}
}
How to dispose in Widget:
class CategoryPage extends StatefulWidget {
#override
_CategoryPageState createState() => _CategoryPageState();
}
class _CategoryPageState extends State<CategoryPage> {
late MyBloc bloc;
#override
void initState() {
bloc = MyBloc();
bloc.fetchData();
super.initState();
}
#override
void dispose() {
bloc.dispose();
super.dispose();
}
// Other part of your Widget
}
even better, if you aren't sure you won't reuse the stream after disposing:
call the drain() function on the stream before closing the stream.
dispose() async{
await _coinDataFetcher.drain();
_coinDataFetcher.close();
_isdisposed = true;
}
Check if the bloc/cubit is closed by isClosed variable. Wrap this if conditions to those states which are throwing exception.
Example code
class LandingCubit extends Cubit<LandingState> {
LandingCubit(this.repository) : super(LandingInitial());
final CoreRepository repository;
// Fetches image urls that needs to shown in landing page
void getLandingImages() async {
emit(LandingImagesLoading());
try {
List<File> landingImages = await repository.landingImages();
if (!isClosed) {
emit(LandingImagesSuccess(landingImages));
}
} catch (e) {
if (!isClosed) {
emit(LandingImagesFetchError(e.toString()));
}
}
}
}

Flutter: Where to add listeners in StatelessWidget?

If I were using a StatefulWidget, then I would be listening to a Stream for example inside the initState method. Where would I do the equivalent in a StatelessWidget (like to use Bloc with streams for state management)? I could do it in the build method but since these are repetitively I wondered if there is a more efficient way than checking for existent listeners like below. I know that this is a redundant and useless example but it's just to show the problem.
import "package:rxdart/rxdart.dart";
import 'package:flutter/material.dart';
final counter = BehaviorSubject<int>();
final notifier = ValueNotifier<int>(0);
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
if (!counter.hasListener)
counter.listen((value) => notifier.value += value);
return MaterialApp(
home: Scaffold(
body: Center(
child:FlatButton(
onPressed: () => counter.add(1),
child: ValueListenableBuilder(
valueListenable: notifier,
builder: (context, value, child) => Text(
value.toString()
),
),
)
),
)
);
}
}
There is no clean way to have a StatelessWidget listen to a Listenable/Stream.
You will always need a StatefulWidget.
On the other hand, you can use composition to write that StatefulWidget just once, and be done with it.
Common examples for that pattern are widgets such as ValueListenableBuilder, StreamBuilder, or AnimatedBuilder. But it is possible to do the same thing, for listening too.
You'd use it this way:
class Foo extends StatelessWidget {
Foo({Key key, this.counter}): super(key: key);
final ValueListenable<int> counter;
#override
Widget build(BuildContext context) {
return ValueListenableListener(
valueListenable: counter,
onChange: (value) {
// TODO: do something
},
child: Something(),
);
}
}
Where ValueListenableListener is implemented this way:
class ValueListenableListener<T> extends StatefulWidget {
const ValueListenableListener(
{Key key, this.valueListenable, this.onChange, this.child})
: super(key: key);
final ValueListenable<T> valueListenable;
final ValueChanged<T> onChange;
final Widget child;
#override
_ValueListenableListenerState createState() =>
_ValueListenableListenerState();
}
class _ValueListenableListenerState extends State<ValueListenableListener> {
#override
void initState() {
super.initState();
widget.valueListenable?.addListener(_listener);
_listener();
}
#override
void didUpdateWidget(ValueListenableListener oldWidget) {
super.didUpdateWidget(oldWidget);
if (oldWidget.valueListenable != widget.valueListenable) {
oldWidget.valueListenable?.removeListener(_listener);
widget.valueListenable?.addListener(_listener);
_listener();
}
}
#override
void dispose() {
widget.valueListenable?.removeListener(_listener);
super.dispose();
}
void _listener() {
widget.onChange?.call(widget.valueListenable.value);
}
#override
Widget build(BuildContext context) {
return widget.child;
}
}
You shouldn't. Not handling variables that might have their values modified is the very purpose of a Stateless widget:
A stateless widget never changes.
UPDATE:
I think this is a problem of understanding Flutter's state management concepts. This new recommended way by the Flutter team should clear some confusions.
You could do something like this:
class ExampleWidget extends StatelessWidget {
bool _initialized = false;
#override
Widget build(BuildContext context) {
if (!_initialized) {
_initialized = true;
// Add listeners here only once
}
return Container();
}
}
But you shouldn't! In fact, your IDE will give you a warning, because this is not the way to go with Stateless widget as it is marked as #immutable. If you need to use lifecycle methods (like initState()) you should make it a Stateful widget. There's no big deal.
This is achievable with flutter_bloc package. The code to be run in initstate can be added inside BlocListener on whatever state you want.
BlocProvider(
create: (BuildContext context) =>
CategoryBlock()..add(LoadCategories()),
child: BlocListener<CategoryBlock, CategoryStates>(
listener: (context, state) {
//Example to add a listener for listview
if (state is LoadCategoriesSuccess) {
itemPositionsListener.itemPositions.addListener(() {
print(itemPositionsListener.itemPositions.value);
});
}
}
You could have your streams being instantiated in a StatefulWidget and then passed down to your StatelessWidgets as an option, so the parent widget would only have a role of controlling the lifecycle of the stream while the child would be using the stream to update the view.
Regarding the earlier answer:
There's no problem in using StreamBuilders inside your StatelessWidgets since the StreamBuilder itself is a a Widget that extends from StatefulWidget and will take care of it's own state and dispose correctly on its own.

Is it safe to have a Widget class that returns a widget conditionally?

Quite often I only want to draw a Widget based on a condition.
For example, I may be creating a component that displays a FadeIn.image but the image: may not be set in the CMS. In this case I want to ignore drawing the FadeIn.image and just return an empty container.
for context, I had done,
child: (someValue == null) ? new Container() : new LabelComponent(label: myStringLabel)
But this broke hot reload and I needed to replace with,
child: _createLabelComponent(myStringLabel),
Widget _createLabelComponent(String label)
{
if(label == null) {
return new Container();
} else {
return new LabelComponent(label: label)
}
}
Is the below safe to work and will not break hot reload? It seems to work at the moment but before I replace all my conditions with this Widget, I'd like some more feedback.
class ConditionalWidget extends StatefulWidget {
final bool condition;
final Widget conditionalWidget;
ConditionalWidget(this.condition, this.conditionalWidget, {Key key});
#override
State createState() => new ConditionalWidgetState();
}
class ConditionalWidgetState extends State<ConditionalWidget> {
ConditionalWidgetState();
#override
void initState()
{
super.initState();
}
#override
Widget build(BuildContext context)
{
if(widget.condition) {
return widget.conditionalWidget;
} else {
return new Container();
}
}
}
Nothing magic here, child is a property of a Container class:
it is safe to use whatever expression returns a widget for child property, obviously also conditional expressions
condition ? expr1 : expr2.
If hot reload in broken check for other causes.
For example your code that break hot reload:
child: (someValue == null) ? new Container() : new LabelComponent(label: myStringLabel)
uses someValue and myStringLabel, whereas in _createLabelComponent there is only a label variable.
class StatmentExample extends StatelessWidget {
Widget build(BuildContext context) {
return Text((() {
if(true){
return "tis true";}
return "anything but true";
})());
}
}
wrap your statements in a function
(() {
// your code here
}())

Trigger a function from a widget to a State object

This is a simplified version of the scenario:
class ParentWdiegt extends StatelessWidget{
//
//
floatinActionButton: FloatingActionButtonWidget(onPressed:()=>CustomWidgetState.someMethod(someValue))
//
//somewhere in the ParentWidget tree
child: CustomWidget() //is stateful
}
CustomWidgetState
class CustomWidgetState extends State<CustomWidget>{
//trigger this function when FAB is pressed in parent widget
someMethod(SomeValue) {//}
}
Is there any way that I can expose someMethod in the state object to be triggered when FAB is pressed without using InheritedWidget?
While GlobalKey allows for an easy access to any widget's state ; avoid it.
Widgets should not interact with other widgets directly. This is one of the core principle of Flutter.
Flutter uses reactive programming instead. Where widgets communicate with each others by submitting events. Not by directly editing the desired widget.
The obvious benefit is that widgets stays independant. And potentially dozens of widgets can communicate with each others using the same principle.
I already made an example here on how to make two different widgets share a common editable value.
If you want to call methods instead, this uses the same principle : A Listenable or Stream shared between widgets. But without using AnimatedWidget or StreamBuilder for the listening.
Instead we'll do the listening manually (which requires slighly more boilerplate) to trigger a custom function.
Here's an example using Stream.
import 'dart:async';
import 'package:flutter/material.dart';
class ParentWidget extends StatefulWidget {
#override
_ParentWidgetState createState() => _ParentWidgetState();
}
class _ParentWidgetState extends State<ParentWidget> {
final changeNotifier = new StreamController.broadcast();
#override
void dispose() {
changeNotifier.close();
super.dispose();
}
#override
Widget build(BuildContext context) {
return new Column(
children: <Widget>[
new AnotherWidget(
shouldTriggerChange: changeNotifier.stream,
),
new RaisedButton(
child: new Text("data"),
onPressed: () => changeNotifier.sink.add(null),
)
],
);
}
}
class AnotherWidget extends StatefulWidget {
final Stream shouldTriggerChange;
AnotherWidget({#required this.shouldTriggerChange});
#override
_AnotherWidgetState createState() => _AnotherWidgetState();
}
class _AnotherWidgetState extends State<AnotherWidget> {
StreamSubscription streamSubscription;
#override
initState() {
super.initState();
streamSubscription = widget.shouldTriggerChange.listen((_) => someMethod());
}
#override
didUpdateWidget(AnotherWidget old) {
super.didUpdateWidget(old);
// in case the stream instance changed, subscribe to the new one
if (widget.shouldTriggerChange != old.shouldTriggerChange) {
streamSubscription.cancel();
streamSubscription = widget.shouldTriggerChange.listen((_) => someMethod());
}
}
#override
dispose() {
super.dispose();
streamSubscription.cancel();
}
void someMethod() {
print('Hello World');
}
#override
Widget build(BuildContext context) {
return Container();
}
}
In this example, someMethod of AnotherWidget will be called whenever a click on the RaisedButton instantiated by _ParentWidgetState is performed.
You can use GlobalKey for that:
// some global place
final customWidgetKey = new GlobalKey<CustomWidgetState>();
...
// import the file with "customWidgetKey"
new CustomWidget(key: customWidetKey, ...)
...
// import the file with "customWidgetKey"
floatinActionButton: FloatingActionButtonWidget(
onPressed: ()=>customWidgetKey.currentState.someMethod(someValue))

Am not rebuilding objects on build() call

Despite flutter calling build (and printing the correct information as below), it doesn't seem to build new TaskWidgets (the print in TaskWidgetState's constructor is not called). This is creating some unusual behaviour in my application (for example, the persistence of deleted ListView items).
I have the following code:
class TaskWidget extends StatefulWidget {
TaskWidget({this.task, this.callToSave, this.callToDelete});
final Task task;
final Function callToSave;
final Function callToDelete;
#override
State<StatefulWidget> createState() {
return new TaskWidgetState(task, callToSave, callToDelete);
}
}
class TaskWidgetState extends State<TaskWidget>{
Task task;
Function toCallOnChange;
Function callToDelete;
TaskWidgetState(Task task, Function callToSave, Function callToDelete){
print("I'm a task widget for " + task.serialise().toString());
this.task = task;
toCallOnChange = callToSave;
this.callToDelete = callToDelete;
}
}
and
class ToDoListWidget extends State<ToDoList>{
List<Task> _toDo = new List<Task>();
...
#override
Widget build(BuildContext context) {
print("building");
return new Scaffold(
body: new ListView(
children: <Widget> [
generateCard(),
...
]
),
);
}
Widget generateCard() {
return new Card(
child: new Column (
children: generateWidgets()
),
...
);
}
List<Widget> generateWidgets() {
print("generating Widgets");
List<Task> tasks = getTasks();
List<Widget> widgets = new List<Widget>();
print("I have " + tasks.length.toString() + " widgets to build");
for(Task t in tasks) {
print(t.title);
TaskWidget widget = new TaskWidget(task: t, callToSave: saveList, callToDelete: deleteTask,);
widgets.add(widget);
}
return widgets;
}
}
Prints out:
building
I/flutter (28783): Returning for Daily
I/flutter (28783): // correct, undeleted task
but onscreen state doesn't reflect this
You're not using State and Stateful Widget properly.
How it works in flutter is that the Widget can be created many times, but there will most likely only be one instance of a State to go along with it.
It's a bit of an anti-pattern to have a constructor for a state.
Instead you should be doing something like this:
class TaskWidget extends StatefulWidget {
TaskWidget({this.task, this.callToSave, this.callToDelete});
final Task task;
final Function callToSave;
final Function callToDelete;
#override
State<StatefulWidget> createState() => new TaskWidgetState();
}
class TaskWidgetState extends State<TaskWidget>{
Widget build(Context context) {
// you can just use the widget.task, this is to illustrate.
var task = widget.task;
var callToSave = widget.callToSave;
var callToDelete = widget.calltoDelete;
}
}
This way, when the widget changes, your state will be re-built and will use whatever the updated values are that were passed into the widget.

Resources