the question simlar on Flutter Provider setState() or markNeedsBuild() called during build
following the question , i have implemented the consume like these one
....
fetchEvents(){
final modelBrainR = context.read<ModelBrain>();
modelBrainR.fetchEvents();
}
...
...
...
child: Consumer<ModelBrain>(
builder: (context, modelData, child) {
if(modelData.action == 're_fetch'){ // here i need to trigger to load a new data again
fetchEvents() // and i would like to change loading state to EventState.Loading // this just make sure of data // and maybe can be infinity loading but in the real case its just one time
}
})
the conslusion of the question [mention again]
EventLoadingStatus _eventLoadingStatus = EventLoadingStatus.Loading;
...
Future<void> fetchEvents(String email) async {
List<Event> events = await EventService().getEventsByUser(email);
_events.clear();
_events.addAll(events);
_eventLoadingStatus = EventLoadingStatus.Loaded;
notifyListeners();
}
so i still need to change _eventLoadingStatus to EventLoadingStatus.Loading; whenever modelData.action == 're_fetch' in consumer ...
or am i missing on implementation ?
its work only on outside of consumer for example i have appBar thats use InkWel / gesture detector.
// outside of consumer
AnimationClick(
function: () {
context.read<ModelBrain>().eventLoading();
fetchEvents();
},
)
class ModelBrain with ChangeNotifier {
...
Future<void> eventLoading() async {
_eventState = EvenState.Loading;
notifyListeners();
}
...
}
Related
I'm creating an app with firebase as a database. After sending data to firebase, app screen should pop out for that I had bloclistener on the screen but after sending the data to firestore database, nothing is happening, flow is stopped after coming to loaded state in bloc file why? check my code so that you will know. I can see my data in firebase but it is not popping out because flow is not coming to listener.
state:
class SampletestInitial extends SampletestState {
#override
List<Object> get props => [];
}
class SampletestLoaded extends SampletestState {
SampletestLoaded();
#override
List<Object> get props => [];
}
class SampletestError extends SampletestState {
final error;
SampletestError({required this.error});
#override
List<Object> get props => [error];
}
bloc:
class SampletestBloc extends Bloc<SampletestEvent, SampletestState> {
SampletestBloc() : super(SampletestInitial()) {
on<SampletestPostData>((event, emit) async {
emit(SampletestInitial());
try {
await Repo().sampleTesting(event.des);
emit(SampletestLoaded());
} catch (e) {
emit(SampletestError(error: e.toString()));
print(e);
}
});
}
}
Repo: ---- Firebase post data
Future<void> sampleTesting(String des) async {
final docTicket = FirebaseFirestore.instance.collection('sample').doc();
final json = {'Same': des};
await docTicket.set(json);
}
TicketScreen:
//After clicking the button ---
BlocProvider<SampletestBloc>.value(
value: BlocProvider.of<SampletestBloc>(context, listen: false)
..add(SampletestPostData(description.text)),
child: BlocListener<SampletestBloc, SampletestState>(
listener: (context, state) {
if (state is SampletestLoaded) {
Navigator.pop(context);
print("Popped out");
}
},
),
);
im not sure but i think that you have the same hash of:
AllData? data;
try to remove AllData? data; and create new data variable so you can be sure that you has a new hash code every time you call createTicket method;
final AllData data = await repo.createTicket(AllData(
Check your AllData class properties.
BLoC will not show a new state if it not unique.
You need to check whether all fields of the AllData class are specified in the props field.
And check your BlocProvider. For what you set listen: false ?
BlocProvider.of<SampletestBloc>(context, listen: false)
I have Flutter app with simple Provider and Consumer flow class SomeProvider with ChangeNotifier. My SubscriptionProvider has next methods:
methodA() async {
isLoading = true;
data = {};
notifyListeners();
await Future.delayed(Duration.zero, otherMethod);
methodB();
}
methodB() {
isLoading = false;
data = {
notifyListeners();
}
Widget Code:
Consumer<SomeProvider>(
builder: (ctx, someProvider, _) {
if (someProvider.isLoading) {
return WidgetC();
}
return WidgetD();
}
)
My problem is that when methodA is called build runs only once. On the other hand if I run await Future.delayed(Duration(seconds: 2), otherMethod); instead of await Future.delayed(Duration.zero, otherMethod); everything works fine and I have two builds. Are there any ways to perform build every time I call notifyListeners in provider.
I have a screen(A) which use 'AccountFetched' state to load list of data from database:
return BlocProvider<AccountBloc>(
create: (context) {
return _accountBloc..add(FetchAccountEvent());
},
child: BlocBuilder<AccountBloc, AccountState>(
builder: (context, state) {
if (state is AccountFetched) {
accounts = state.accounts;
}
And in my second screen(B) I call AddAccountEvent to add data to database and navigate back to screen(A) which is the parent screen.
onPressed: () {
BlocProvider.of<AccountBloc>(context)
..add(AddAccountEvent(account: account));
Navigator.of(context).pop();
}
But when I navigate back to screen A, the list of data is not updated.
Should I refresh the screen(A) manually or how can I update the state of bloc?
This is my bloc class:
class AccountBloc extends Bloc<AccountEvent, AccountState> {
AccountBloc() : super(AccountInitial());
#override
Stream<AccountState> mapEventToState(AccountEvent event) async* {
if (event is FetchAccountEvent) yield* _fetchAccountEvent(event, state);
if (event is AddAccountEvent) yield* _addAccountEvent(event, state);
}
Stream<AccountState> _fetchAccountEvent(
FetchAccountEvent event, AccountState state) async* {
yield FetchingAccount();
final dbAccount = await DBAccountRepository.instance;
List<Account> accounts = await dbAccount.accounts();
yield AccountFetched(accounts: accounts);
}
Stream<AccountState> _addAccountEvent(
AddAccountEvent event, AccountState state) async* {
yield AddingAccount();
final dbAccount = await DBAccountRepository.instance;
final insertedId = await dbAccount.insertAccount(event.account);
yield AccountAdded(insertedId: insertedId);
}
}
I was experiencing the same problem, the solution I found was to make screen A wait for screen B to close, and after that trigger the data reload event for screen A.
onTap: () async {
await Navigator.of(context).push(ScreenB());
_bloc.add(RequestDataEvent());
}
So it will only execute the event after screen B is closed
well, you are creating a new bloc on page A. try to move BlocProvider 1 level up. so when the build function of page A calls, doesn't create a new bloc.
You should have a BlocListener in screen A listening for AccountState , with a if ( state is AccountFetched ) {} statement and perform your data update in a setState() . Depending on how long it takes to display screen A you might have the AccountFetched state been yielded before so screen A won't see it, in this case you could delay by 250 milliseconds yielding it in your _addAccount method either with a Timer or a Future.delayed.
Hope it helped.
Every time the screen is rebuilt the getJSONfromTheSite seems to get invoked. Is seems because the future is placed inside the Widget build that every time I rebuild the screen it's just calling the apiResponse.getJSONfromTheSite('sitelist') future. But When I try to simply move the apiResponse.getJSONfromTheSite('sitelist') call outside the Widget and into the initState it doesn't work at all.
I'm not fully grasping the interplay of Futures in relation to a stateful widget, but in this case I need to keep the widget stateful because Im using a pull to refresh function to rebuild my state
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource();
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: FutureBuilder(
future: _apiResponse.getJSONfromTheSite('sitelist'),
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
AppData sitelistCollection = (snapshot.data as SuccessState).value;
}
},
),
);
}
}
// (Do some UI stuff)
class RemoteDataSource {
//Creating Singleton
RemoteDataSource._privateConstructor();
static final RemoteDataSource _apiResponse =
RemoteDataSource._privateConstructor();
factory RemoteDataSource() => _apiResponse;
MyClient client = MyClient(Client());
void init() {}
Future<Result> getJSONfromTheSite(String call, {counter = 0}) async {
debugPrint('Network Attempt by getJSONfromTheSite');
try {
final response = await client
.request(requestType: RequestType.GET, path: call)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
return Result<AppData>.success(AppData.fromRawJson(response.body));
} else {
return Result.error(
title: "Error", msg: "Status code not 200", errorcode: 1);
}
} catch (error) {
if (counter < 3) {
counter += 1;
await Future.delayed(Duration(milliseconds: 1000));
return getJSONfromTheSite(call, counter: counter);
} else {
return Result.error(
title: "No connection", msg: "Status code not 200", errorcode: 0);
}
}
}
void dispose() {}
}
A FutureBuilder, as the name suggests, wants to build you something using a FUTURE value that you provide. For that to happen, you should perform an operation outside the build method (for example, in the State class or in the initState function) and store its Future value (like a promise in javascript), to be used later on the FutureBuilder.
You have access to this value inside the FutureBuilder on the snapshot.data variable, as I can see you already know by looking at your code. The way I coded the following solution, you should no longer have issues about multiple requests to the website each time it builds the widget UI (getJSONfromTheSite will only be called once and the result from this call will be available to you inside the FutureBuilder!)
The solution:
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource(); // I left this here because I'm not sure if you use this value anywhere else (if you don't, simply delete this line)
// when creating the widget's state, perform the call to the site once and store the Future in a variable
Future<Result> _apiResponseState = RemoteDataSource().getJSONfromTheSite('sitelist');
#override
void initState() {
super.initState();
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: FutureBuilder<SuccessState>(
future: _apiResponseState,
builder: (BuildContext context, AsyncSnapshot<Result> snapshot) {
if (snapshot.data is SuccessState) {
AppData sitelistCollection = (snapshot.data as SuccessState).value;
}
},
),
);
}
}
EDIT: Edited answer to use Result as the inner type of the Future (instead of SuccessState).
The FutureBuilder's behavior can be expected as following according to the documentation
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateWidget, or State.didChangeDependencies.
It must not be created during the State.build or StatelessWidget.build method call when constructing the FutureBuilder.
If the future is created at the same time as the FutureBuilder, then every time the FutureBuilder's parent is rebuilt, the asynchronous task will be restarted.
As stated above, if the future is created at the same time as the FutureBuilder, the FutureBuilder will rebuilt every time there's change from the parent. To avoid this change, as well as making the call from initState, one easy way is to use another Widget call StreamBuilder.
An example from your code:
class RemoteDataSource {
final controller = StreamController<AppData>();
void _apiResponse.getJSONfromTheSite('sitelist') {
// ... other lines
if (response.statusCode == 200) {
// Add the parsed data to the Stream
controller.add(AppData.fromRawJson(response.body));
}
// ... other lines
}
In your SiteListScreen:
class _SitelistScreenState extends State<SitelistScreen> {
RemoteDataSource _apiResponse = RemoteDataSource();
#override
void initState() {
super.initState();
_apiResponse.getJSONfromTheSite('sitelist');
}
#override
Widget build(BuildContext context) {
return Scaffold(
child: StreamBuilder<AppData>(
stream: _apiResponse.controller.stream, // Listen to the Stream using StreamBuilder
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) {
AppData sitelistCollection = snapshot.data;
}
},
),
);
}
This StreamBuilder is a popular concept through out most of Flutter's apps nowadays (and is the basis of many Flutter's architecture), so it's a good idea to take a good look and use the best of it.
There is a simple way you do not need to change too much coding. Like
class RemoteDataSource {
Result _result;
//Creating Singleton
RemoteDataSource._privateConstructor();
static final RemoteDataSource _apiResponse =
RemoteDataSource._privateConstructor();
factory RemoteDataSource() => _apiResponse;
MyClient client = MyClient(Client());
void init() {}
Future<Result> getJSONfromTheSite(String call, {counter = 0}) async {
debugPrint('Network Attempt by getJSONfromTheSite');
if (_result != null) {
return _result;
}
try {
final response = await client
.request(requestType: RequestType.GET, path: call)
.timeout(const Duration(seconds: 8));
if (response.statusCode == 200) {
_result = Result<AppData>.success(AppData.fromRawJson(response.body));
return _result;
} else {
return Result.error(
title: "Error", msg: "Status code not 200", errorcode: 1);
}
} catch (error) {
if (counter < 3) {
counter += 1;
await Future.delayed(Duration(milliseconds: 1000));
return getJSONfromTheSite(call, counter: counter);
} else {
return Result.error(
title: "No connection", msg: "Status code not 200", errorcode: 0);
}
}
}
void dispose() {}
}
I only store the success result to _result, I do not sure that you want store the error result. When you rebuild the widget, it will check if it already get the success result. If true, return the stored result, it not, call api.
I want to load a list of events and display a loading indicator while fetching data.
I'm trying Provider pattern (actually refactoring an existing application).
So the event list display is conditional according to a status managed in the provider.
Problem is when I make a call to notifyListeners() too quickly, I get this exception :
════════ Exception caught by foundation library ════════
The following assertion was thrown while dispatching notifications for EventProvider:
setState() or markNeedsBuild() called during build.
...
The EventProvider sending notification was: Instance of 'EventProvider'
════════════════════════════════════════
Waiting for some milliseconds before calling notifyListeners() solve the problem (see commented line in the provider class below).
This is a simple example based on my code (hope not over simplified) :
main function :
Future<void> main() async {
runApp(
MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => LoginProvider()),
ChangeNotifierProvider(create: (_) => EventProvider()),
],
child: MyApp(),
),
);
}
root widget :
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
final LoginProvider _loginProvider = Provider.of<LoginProvider>(context, listen: true);
final EventProvider _eventProvider = Provider.of<EventProvider>(context, listen: false);
// load user events when user is logged
if (_loginProvider.loggedUser != null) {
_eventProvider.fetchEvents(_loginProvider.loggedUser.email);
}
return MaterialApp(
home: switch (_loginProvider.status) {
case AuthStatus.Unauthenticated:
return MyLoginPage();
case AuthStatus.Authenticated:
return MyHomePage();
},
);
}
}
home page :
class MyHomePage extends StatelessWidget {
#override
Widget build(BuildContext context) {
final EventProvider _eventProvider = Provider.of<EventProvider>(context, listen: true);
return Scaffold(
body: _eventProvider.status == EventLoadingStatus.Loading ? CircularProgressIndicator() : ListView.builder(...)
)
}
}
event provider :
enum EventLoadingStatus { NotLoaded, Loading, Loaded }
class EventProvider extends ChangeNotifier {
final List<Event> _events = [];
EventLoadingStatus _eventLoadingStatus = EventLoadingStatus.NotLoaded;
EventLoadingStatus get status => _eventLoadingStatus;
Future<void> fetchEvents(String email) async {
//await Future.delayed(const Duration(milliseconds: 100), (){});
_eventLoadingStatus = EventLoadingStatus.Loading;
notifyListeners();
List<Event> events = await EventService().getEventsByUser(email);
_events.clear();
_events.addAll(events);
_eventLoadingStatus = EventLoadingStatus.Loaded;
notifyListeners();
}
}
Can someone explain what happens?
You are calling fetchEvents from within your build code for the root widget. Within fetchEvents, you call notifyListeners, which, among other things, calls setState on widgets that are listening to the event provider. This is a problem because you cannot call setState on a widget when the widget is in the middle of rebuilding.
Now at this point, you might be thinking "but the fetchEvents method is marked as async so it should be running asynchronous for later". And the answer to that is "yes and no". The way async works in Dart is that when you call an async method, Dart attempts to run as much of the code in the method as possible synchronously. In a nutshell, that means any code in your async method that comes before an await is going to get run as normal synchronous code. If we take a look at your fetchEvents method:
Future<void> fetchEvents(String email) async {
//await Future.delayed(const Duration(milliseconds: 100), (){});
_eventLoadingStatus = EventLoadingStatus.Loading;
notifyListeners();
List<Event> events = await EventService().getEventsByUser(email);
_events.clear();
_events.addAll(events);
_eventLoadingStatus = EventLoadingStatus.Loaded;
notifyListeners();
}
We can see that the first await happens at the call to EventService().getEventsByUser(email). There's a notifyListeners before that, so that is going to get called synchronously. Which means calling this method from the build method of a widget will be as though you called notifyListeners in the build method itself, which as I've said, is forbidden.
The reason why it works when you add the call to Future.delayed is because now there is an await at the top of the method, causing everything underneath it to run asynchronously. Once the execution gets to the part of the code that calls notifyListeners, Flutter is no longer in a state of rebuilding widgets, so it is safe to call that method at that point.
You could instead call fetchEvents from the initState method, but that runs into another similar issue: you also can't call setState before the widget has been initialized.
The solution, then, is this. Instead of notifying all the widgets listening to the event provider that it is loading, have it be loading by default when it is created. (This is fine since the first thing it does after being created is load all the events, so there shouldn't ever be a scenario where it needs to not be loading when it's first created.) This eliminates the need to mark the provider as loading at the start of the method, which in turn eliminates the need to call notifyListeners there:
EventLoadingStatus _eventLoadingStatus = EventLoadingStatus.Loading;
// or
late EventLoadingStatus _eventLoadingStatus;
#override
void initState() {
super.initState();
_eventLoadingStatus = EventLoadingStatus.Loading;
}
...
Future<void> fetchEvents(String email) async {
List<Event> events = await EventService().getEventsByUser(email);
_events.clear();
_events.addAll(events);
_eventLoadingStatus = EventLoadingStatus.Loaded;
notifyListeners();
}
The issue is you calling notifyListeners twice in one function. I get it, you want to change the state. However, it should not be the responsibility of the EventProvider to notify the app when it's loading. All you have to do is if it's not loaded, assume that it's loading and just put a CircularProgressIndicator. Don't call notifyListeners twice in the same function, it doesn't do you any good.
If you really want to do it, try this:
Future<void> fetchEvents(String email) async {
markAsLoading();
List<Event> events = await EventService().getEventsByUser(email);
_events.clear();
_events.addAll(events);
_eventLoadingStatus = EventLoadingStatus.Loaded;
notifyListeners();
}
void markAsLoading() {
_eventLoadingStatus = EventLoadingStatus.Loading;
notifyListeners();
}
You are calling Apis from within your code for the root widget. Within Apis, you call notifyListeners, which, among other things, calls setState on widgets that are listening to the event provider. So firstly remove setState in your code and make sure Future use when call apis in init state
#override
void initState() {
super.initState();
Future.microtask(() => context.read<SellCarProvider>().getBrandService(context));
}