How to pop screen using Mobx in flutter - flutter

I have a Food object that contains properties like name, id, calories, etc. With a series of screens, the user populates the food object properties.
Once done, the user can press the submit button, that will call the addFood method in the store.
The problem is, after uploading the food to the server, i want to pop the screen or show error message in toast based on the response. I just don't know how to do this.
Following is my code (only the important bits):
FoodDetailStore.dart
class FoodDetailStore = _FoodDetailStore with _$FoodDetailStore;
abstract class _FoodDetailStore with Store {
Repository _repository;
Food _food;
#observable
String msg = '';
// ... Other Observables and actions
#action
addFood(bool toAdd) {
if (toAdd) {
_repository.addFood(food).then((docId) {
if (docId != null) {
// need to pop the screen
}
}).catchError((e) {
// show error to the user.
// I tried this, but it didn't work
msg = 'there was an error with message ${e.toString()}. please try again.';
});
}
// .. other helper methods.
}
FoodDetailScreen.dart (Ignore the bloc references, I am currently refactoring code to mobx)
class FoodDataScreen extends StatefulWidget {
final String foodId;
final Serving prevSelectedServing;
final bool fromNewRecipe;
FoodDataScreen({#required this.foodId, this.prevSelectedServing, this.fromNewRecipe});
#override
_FoodDataScreenState createState() => _FoodDataScreenState(
this.foodId,
this.prevSelectedServing,
this.fromNewRecipe,
);
}
class _FoodDataScreenState extends State<FoodDataScreen> {
final String foodId;
final Serving prevSelectedServing;
final bool fromNewRecipe;
FoodDataBloc _foodDataBloc;
_FoodDataScreenState(
this.foodId,
this.prevSelectedServing,
this.fromNewRecipe,
);
FoodDetailStore store;
#override
void initState() {
store = FoodDetailStore();
store.initReactions();
store.initializeFood(foodId);
super.initState();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
// I know this is silly, but this is what i tried. Didn't worked
Observer(
builder: (_) {
_showMsg(store.msg);
}
);
}
#override
Widget build(BuildContext context) {
return Container(
// ... UI
);
}
_popScreen() {
_showMsg('Food Added');
Majesty.router.pop(context);
}
_showMsg(String msg) {
Fluttertoast.showToast(msg: msg);
}
#override
void dispose() {
store.dispose();
super.dispose();
}
}

Constructing an Observer instance inside the didChangeDependencies() is indeed "silly" as you have rightly noted already :)
Observer is a widget and widget needs to be inserted into the widgets tree in order to do something useful. In our case non-widget Mobx reactions come to the rescue.
I will show how I did it in my code for the case of showing a Snackbar upon observable change so you will get an idea how to transform your code.
First of all, import import 'package:mobx/mobx.dart';.
Then in the didChangeDependencies() create a reaction which will use some of your observables. In my case these observables are _authStore.registrationError and _authStore.loggedIn :
final List<ReactionDisposer> _disposers = [];
#override
void dispose(){
_disposers.forEach((disposer) => disposer());
super.dispose();
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
_authStore = Provider.of<AuthStore>(context);
_disposers.add(
autorun(
(_) {
if (_authStore.registrationError != null)
_scaffoldKey.currentState.showSnackBar(
SnackBar(
content: Text(_authStore.registrationError),
backgroundColor: Colors.redAccent,
duration: Duration(seconds: 4),
),
);
},
),
);
_disposers.add(
reaction(
(_) => _authStore.loggedIn,
(_) => Navigator.of(context).pop(),
),
);
}
I use two types of Mobx reactions here: autorun and reaction. autorun triggers the first time immediately after you crate it and then every time the observable changes its value. reaction does not trigger the first time, only when the observable change.
Also pay attention to dispose the created reactions in the dispose() method to avoid resources leak.
Here is a code of my Mobx store class with used observables to complete the picture:
import 'package:mobx/mobx.dart';
import 'dart:convert';
part "auth_store.g.dart";
class AuthStore = AuthStoreBase with _$AuthStore;
abstract class AuthStoreBase with Store{
#observable
String token;
#observable
String registrationError;
#observable
String loginError;
#action
void setToken(String newValue){
token = newValue;
}
#action
void setRegistrationError(String newValue){
registrationError = newValue;
}
#action
void setLoginError(String newValue){
loginError = newValue;
}
#action
void resetLoginError(){
loginError = null;
}
#computed
bool get loggedIn => token != null && token.length > 0;
#action
Future<void> logOut() async{
setToken(null);
}
}

Related

Flutter GetX state management initial null value

This is what I'm trying to achieve using flutter GetX package but not working properly.
I have a Firestore document, if the document is changed I want to call an api and keep the data up to date as observable.
The code below seems to work but initial screen shows null error then it shows the data.
I don't know how I can make sure both fetchFirestoreUser() and fetchApiData() (async methods) returns data before I move to the home screen.
GetX StateMixin seems to help with async data load problem but then I don't know how I can refresh the api data when the firestore document is changed.
I'm not sure if any other state management would be best for my scenario but I find GetX easy compared to other state management package.
I would very much appreciate if someone would tell me how I can solve this problem, many thanks in advance.
Auth Controller.
class AuthController extends SuperController {
static AuthController instance = Get.find();
late Rx<User?> _user;
FirebaseAuth auth = FirebaseAuth.instance;
var _firestoreUser = FirestoreUser().obs;
var _apiData = ProfileUser().obs;
#override
void onReady() async {
super.onReady();
_user = Rx<User?>(auth.currentUser);
_user.bindStream(auth.userChanges());
//get firestore document
fetchFirestoreUser();
//fetch data from api
fetchApiData();
ever(_user, _initialScreen);
//Refresh api data if firestore document has changed.
_firestoreUser.listen((val) {
fetchApiData();
});
}
Rx<FirestoreUser?> get firestoreUser => _firestoreUser;
_initialScreen(User? user) {
if (user == null) {
Get.offAll(() => Login());
} else {
Get.offAll(() => Home());
}
}
ProfileUser get apiData => _apiData.value;
void fetchFirestoreUser() async {
Stream<FirestoreUser> firestoreUser =
FirestoreDB().getFirestoreUser(_user.value!.uid);
_firestoreUser.bindStream(firestoreUser);
}
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
#override
void onDetached() {}
#override
void onInactive() {}
#override
void onPaused() {}
#override
void onResumed() {
fetchApiData();
}
}
Home screen
class Home extends StatelessWidget {
const Home({Key? key}) : super(key: key);
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: Container(
child: Obx(() =>
Text("username: " + AuthController.instance.apiData.username!))),
),
);
}
}
To be honest, I never used GetX so I'm not too familiar with that syntax.
But I can see from your code that you're setting some mutable state when you call this method:
fetchApiData() async {
var result = await RemoteService.getProfile(_user.value!.uid);
if (result != null) {
_apiData.value = result;
}
}
Instead, a more robust solution would be to make everything reactive and immutable. You could do this by combining providers if you use Riverpod:
final authStateChangesProvider = StreamProvider.autoDispose<User?>((ref) {
final authService = ref.watch(authRepositoryProvider);
return authService.authStateChanges();
});
final apiDataProvider = FutureProvider.autoDispose<APIData?>((ref) {
final userValue = ref.watch(authStateChangesProvider);
final user = userValue.value;
if (user != null) {
// note: this should also be turned into a provider, rather than using a static method
return RemoteService.getProfile(user.uid);
} else {
// decide if it makes sense to return null or throw and exception when the user is not signed in
return Future.value(null);
}
});
Then, you can just use a ConsumerWidget to watch the data:
#override
Widget build(BuildContext context, WidgetRef ref) {
// this will cause the widget to rebuild whenever the auth state changes
final apiData = ref.watch(apiDataProvider);
return apiData.when(
data: (data) => /* some widget */,
loading: () => /* some loading widget */,
error: (e, st) => /* some error widget */,
);
}
Note: Riverpod has a bit of a learning curve (worth it imho) so you'll have to learn it how to use it first, before you can understand how this code works.
Actually the reason behind this that you put your controller in the same page that you are calling so in the starting stage of your page Get.put() calls your controller and because you are fetching data from the API it takes a few seconds/milliseconds to get the data and for that time your Obx() renders the error. To prevent this you can apply some conditional logic to your code like below :
Obx(() => AuthController.instance.apiData != null ? Text("username: " + AuthController.instance.apiData.username!) : CircularProgressIndicator())) :

I want to use data from a Future inside a ChangeNotifier Provider and a ListView

I can't figure out how to get the data from the myProvider before I call the getWalletItems(). Should I do 2 seperate providers??
My goal here is just to get all these items from a Future<List<Wallet'>> and return them into a listview that is able to have each item be selectable with a checkbox which will then pass on all the selected items to a different page. They will not be rebuilt there so I don't think I need another model but if I do just let me know. Here is my code for the ChangeNotifier:
class WalletModel extends ChangeNotifier {
List<Wallet> _wallet = [];
List<Wallet> get wallet => _wallet;
set wallet(List<Wallet> newValue) {
_wallet = newValue;
notifyListeners();
}
myProvider() {
loadValue();
}
Future<void> loadValue() async {
wallet = await WalletApi.getWalletItems();
}
UnmodifiableListView<Wallet> get allWalletItems =>
UnmodifiableListView(_wallet);
UnmodifiableListView<Wallet> get incompleteTasks =>
UnmodifiableListView(_wallet.where((_wallet) => !_wallet.isSelected));
UnmodifiableListView<Wallet> get completedTasks =>
UnmodifiableListView(_wallet.where((_wallet) => _wallet.isSelected));
void toggleWallet(Wallet wallet) {
final walletIndex = _wallet.indexOf(wallet);
_wallet[walletIndex].toggleSelected();
notifyListeners();
}
}
Here is the checkbox to select
Checkbox(
value: wallet.isSelected,
onChanged: (bool? checked) {
Provider.of<WalletModel>(context, listen: false)
.toggleWallet(wallet);
},
),
Here is the listview and if I need to post anyother code just let me know because I'm quite lost on what to do.
class WalletList extends StatelessWidget {
final List<Wallet> wallets;
WalletList({required this.wallets});
#override
Widget build(BuildContext context) {
return ListView(
children: getWalletListItems(),
);
}
List<Widget> getWalletListItems() {
return wallets
.map((walletItem) => WalletListItem(wallet: walletItem))
.toList();
}
}
make myProvider() a future and then use below code for WalletList Widget
before build runs for WalletList we want to get the items from the provider so we have used didChangedDependencies() as it runs before build and can be converted to future.
when the list is got we use the list that was set by above the make the UI
Note : Consumer changes its state whenever notifyListener() is called in Provider.
import 'package:flutter/material.dart';
class WalletList extends StatefulWidget {
#override
_WalletListState createState() => _WalletListState();
}
class _WalletListState extends State<WalletList> {
bool _isInit = true;
#override
void didChangeDependencies() async {
//boolean used to run the set list fucntion only once
if (_isInit) {
//this will save the incoming data to list before build runs
await Provider.of<WalletModel>(context, listen: false).myProvider();
_isInit = false;
}
super.didChangeDependencies();
}
#override
Widget build(BuildContext context) {
return Consumer<WalletModel>(builder: (context, providerInstance, _) {
return ListView(
children: providerInstance
.wallet
.map<Widget>((walletItem) => WalletListItem(wallet: walletItem))
.toList(),
);
});
}
// List<Widget> getWalletListItems() {
// return Provider.of<WalletModel>(context, listen: false)
// .wallet
// .map((walletItem) => WalletListItem(wallet: walletItem))
// .toList();
// }
}

Flutter: How to make a sequence of http requests on a widget before build method

I have 3 classes: Users, Posts and Comments. User has many Posts and
Posts has many Comments.
I want that all data to be fetched before the widget's build method is called.
I tryed to use initState() to do this:
class FetchDataExample extends StatefulWidget {
final User _user;
FetchDataExample(this._user);
#override
_State createState() => _State(_user);
}
class _State extends State<FetchDataExample> {
final User _user;
_State(this._user);
#override
void initState() {
_user.setPosts();
super.initState();
}
#override
Widget build(BuildContext context) {
print(this._user.posts[0]);
return Container(
);
}
}
In User class I have:
void setPosts() async {
String url = 'https://jsonplaceholder.typicode.com/posts?userId=' + this.id.toString();
var request = Requester.get(url); // Returns a Future<Response>
await request.then((value) => this.posts = Post.jsonToPosts(json.decode(value.body)));
this.posts.forEach((post) => post.setComments());
print(this.posts[0]);
}
The 'setComments()' has the same logic.
I have two prints:
Inside build that returns null;
Inside setPosts the returns Instance of 'Post';
So, by the time that Build method is called in the widget, the initState has not finished yet.
I need it be finished, does anyone know how can I do that?
You can use a FutureBuilder to build a widget by using latest result from a future.
And also you can combile multiple futures into a single one using Future.wait method.
Here is a sample code:
_getPageData() async {
var _combinedFutures = await Future.wait([setPosts, setComments]);
//do stuff with data
}
...
#override
Widget build(BuildContext context) {
return FutureBuilder(
future:_getPageData(),
builder: (context, snapshot) {
return Container();
}),
);
});

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

setState() called after dispose()

When I click the raised button, the timepicker is showing up. Now, if I wait 5 seconds, for example, and then confirm the time, this error will occur:
setState() called after dispose()
I literally see in the console how flutter is updating the parent widgets, but why? I don't do anything - I just wait 5 seconds?!
The example below will work in a normal project, however in my project which is quite more complex it won't work because Flutter is updating the states while I am waiting... What am I doing wrong? Does anyone have a guess at what it could be that Flutter is updating randomly in my more complex project and not in a simple project?
[UPDATE]
I took a second look at it and found out it is updating from the level on where my TabBar and TabBarView are.
Could it have to do something with the "with TickerProviderStateMixin" which I need for the TabBarView? Could it be that it causes the app to refresh regularly and randomly?
class DateTimeButton extends State<DateTimeButtonWidget> {
DateTime selectedDate = new DateTime.now();
Future initTimePicker() async {
final TimeOfDay picked = await showTimePicker(
context: context,
initialTime: new TimeOfDay(hour: selectedDate.hour, minute: selectedDate.minute),
);
if (picked != null) {
setState(() {
selectedDate = new DateTime(selectedDate.year, selectedDate.month, selectedDate.day, picked.hour, picked.minute);
});
}
}
#override
Widget build(BuildContext context) {
return new RaisedButton(
child: new Text("${selectedDate.hour} ${selectedDate.minute}"),
onPressed: () {
initTimePicker();
}
);
}
}
Just check boolean property mounted of the state class of your widget before calling setState().
if (this.mounted) {
setState(() {
// Your state change code goes here
});
}
Or even more clean approach
Override setState method in your StatelfulWidget class.
class DateTimeButton extends StatefulWidget {
#override
void setState(fn) {
if(mounted) {
super.setState(fn);
}
}
}
If it is an expected behavior that the Future completes when the widget already got disposed you can use
if (mounted) {
setState(() {
selectedDate = new DateTime(selectedDate.year, selectedDate.month, selectedDate.day, picked.hour, picked.minute);
});
}
Just write one line before setState()
if (!mounted) return;
and then
setState(() {
//Your code
});
I had the same problem and i solved changing the super constructor call order on initState():
Wrong code:
#override
void initState() {
foo_bar(); // call setState();
super.initState(); // then foo_bar()
}
Right code:
#override
void initState() {
super.initState();
foo_bar(); // first call super constructor then foo_bar that contains setState() call
}
To prevent the error from occurring, one can make use of the mounted property of the State class to ensure that a widget is mounted before settings its state:
// First Update data
if (!mounted) {
return;
}
setState(() { }
Try this
Widget build(BuildContext context) {
return new RaisedButton(
child: new Text("${selectedDate.hour} ${selectedDate.minute}"),
onPressed: () async {
await initTimePicker();
}
);
}
class MountedState<T extends StatefulWidget> extends State<T> {
#override
Widget build(BuildContext context) {
return null;
}
#override
void setState(VoidCallback fn) {
if (mounted) {
super.setState(fn);
}
}
}
Example
To prevent the error,Instead of using State use MountedState
class ExampleStatefulWidget extends StatefulWidget {
const ExampleStatefulWidget({Key key}) : super(key: key);
#override
_ExampleStatefulWidgetState createState() => _ExampleStatefulWidgetState();
}
class _ExampleStatefulWidgetState extends MountedState<ExampleStatefulWidget> {
....
}
I had this error when I mistakenly called super.initState before the variable. Check this:
#override
void initState() {
super.initState();
bloc = MainBloc();
}
Should be fixed as
#override
void initState() {
bloc = MainBloc();
super.initState();
}
The problem could occur when you have long asynchronous operation in stateful widget that could be closed/disposed before the operation finished.
Futures in Dart are not preemptive, so the only way is to check if a widget mounted before calling setState.
If you have a lot of widgets with asynchrony, adding ton of if (mounted) checks is tedious and an extension method might be useful
extension FlutterStateExt<T extends StatefulWidget> on State<T> {
void setStateIfMounted(VoidCallback fn) {
if (mounted) {
// ignore: invalid_use_of_protected_member
setState(fn);
}
}
}