future builder keeps rebuilding on every setstate - flutter

I am building an app which uses an api and I am using the future builder to fetch the data but the problem is when the state changes it rebuilds and I want to prevent this from happen.
Thanks,

try using this :
class Example extends StatefulWidget {
#override
_ExampleState createState() => _ExampleState();
}
class _ExampleState extends State<Example> {
Future<response> future;
#override
void initState() {
future = _asyncmethodCall();
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: future,
builder: (context, snapshot) {
// create some layout here
},
);
}
Future<someResponse> _asyncmethodCall() async {
// async code here
}
}
similar question: How to deal with unwanted widget build?

Related

Calling async event in flutter_bloc

I am trying to fetch data from API as soon as the flutter app loads but I am unable to achieve so
class MarketBloc extends Bloc<MarketListEvent, MarketListState> {
MarketBloc() : super(MarketLoading()) {
on<MarketSelectEvent>((event, emit) async {
emit(MarketLoading());
final data = await ApiCall().getData(event.value!);
globalData = data;
emit(MarketDataFetched(marDat: globalData.data, dealType: event.value));
});
}
}
I have called MarketLoading state as the initial state and I want to call MarketSelectEvent just after that but in the current code, action is required to do so and i want to achieve it without any action.
You have 2 options:
add an event from the UI as soon you instantiate the MarketBloc
MarketBloc()..add(MarketSelectEvent())
add an event in the initialization code
MarketBloc() : super(MarketLoading()) {
add(MarketSelectEvent());
}
You could do this with in the initState of whatever the first page is that your app loads.
class TestPage extends StatefulWidget {
#override
State<TestPage> createState() => _TestPageState();
}
class _TestPageState extends State<TestPage> {
late MarketBloc marketBloc;
#override
void initState() {
super.initState();
marketBloc = BlocProvider.of<MarketBloc>(context);
marketBloc.add(MarketSelectEvent());
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(
child: BlocBuilder<MarketBloc, MarketListState>(
builder: (context, state) {
if (state is MarketLoading) {
return Text('loading...');
}
if (state is MarketDataFetched) {
return ...your UI that contains data from API call
}
},
),
),
);
}
}

Flutter: Stateful Widget does not update

Imagine two Widgets: Main that manages a tabbar and therefore holds several Widgets - and Dashboard.
On Main Constructor I create a first Instance of Dashboard and the other tabbar Widgets with some dummy data (they are getting fetched in the meanwhile in initState). I build these with Futurebuilder. Once the data arrived I want to create a new Instance of Dashboard, but it won't change.
class _MainState extends State<HomePage> {
var _tabs = <Widget>[];
Future<dynamic> futureData;
_MainState() {
_tabs.add(Dashboard(null));
}
#override
void initState() {
super.initState();
futureData = _getData();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: futureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.data != null) {
tabs[0] = Dashboard(snapshot.data);
} else {
return CircularProgressIndicator();
}
});
}
}
class DashboardScreen extends StatefulWidget {
final data;
DashboardScreen(this.data,
{Key key})
: super(key: key) {
print('Dashboard Constructor: ' + data.toString());
}
#override
_DashboardScreenState createState() => _DashboardScreenState(data);
}
class _DashboardScreenState extends State<DashboardScreen> {
var data;
_DashboardScreenState(this.data);
#override
void initState() {
super.initState();
print('InitState: ' + data.toString());
}
#override
void didUpdateWidget(Widget oldWidget) {
super.didUpdateWidget(oldWidget);
print('didUpdateWidget');
}
#override
void didChangeDependencies() {
super.didChangeDependencies();
print('didChangeDependencies' + data.toString());
}
#override
Widget build(BuildContext context) {
return Text(data.toString());
}
}
When I print on several available methods it comes clear that the DasboardScreenState is not recreated. Only the DashboardScreen Constructor is called again when the data arrived, but not it's state...
flutter: MainConstructor: null
flutter: Dashboard Constructor: null
flutter: InitState: null
flutter: didChangeDependencies: null
flutter: Dashboard Constructor: MachineStatus.Manual <- Here the data arrived in futureBuilder
How can I force the State to recreate? I tried to use the key parameter with UniqueKey(), but that didn't worked. Also inherrited widget seems not to be the solution either, despite the fact that i don't know how to use it in my use case, because the child is only available in the ..ScreenState but not the updated data..
I could imagine to inform dashboardScreenState by using Stream: listen to messages and then call setState() - I think, but that's only a workaround.
Can anyone help me please :)?
I know I have had issues with the if statement before, try:
return FutureBuilder(
future: futureData,
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.hasData) { //use hasData
DataType data = snapshot.data; //Declare Values first
tabs[0] = Dashboard(data);
} else {
return CircularProgressIndicator();
}
});

Triggering initial event in BLoC

example_states:
abstract class ExampleState extends Equatable {
const ExampleState();
}
class LoadingState extends ExampleState {
//
}
class LoadedState extends ExampleState {
//
}
class FailedState extends ExampleState {
//
}
example_events:
abstract class ExampleEvent extends Equatable {
//
}
class SubscribeEvent extends ExampleEvent {
//
}
class UnsubscribeEvent extends ExampleEvent {
//
}
class FetchEvent extends ExampleEvent {
//
}
example_bloc:
class ExampleBloc extends Bloc<ExampleEvent, ExampleState> {
#override
ExampleState get initialState => LoadingState();
#override
Stream<ExampleState> mapEventToState(
ExampleEvent event,
) async* {
if (event is SubscribeEvent) {
//
} else if (event is UnsubscribeEvent) {
//
} else if (event is FetchEvent) {
yield LoadingState();
try {
// network calls
yield LoadedState();
} catch (_) {
yield FailedState();
}
}
}
}
example_screen:
class ExampleScreenState extends StatelessWidget {
// ignore: close_sinks
final blocA = ExampleBloc();
#override
Widget build(BuildContext context) {
return Scaffold(
body: BlocBuilder<ExampleBloc, ExampleState>(
bloc: blocA,
// ignore: missing_return
builder: (BuildContext context, state) {
if (state is LoadingState) {
blocA.add(Fetch());
return CircularProgressBar();
}
if (state is LoadedState) {
//...
}
if (state is FailedState) {
//...
}
},
),
);
}
}
As you can see in example_bloc, initial state is LoadingState() and in build it shows circular progress bar. I use Fetch() event to trigger next states. But I don't feel comfortable using it there. What I want to do is:
When app starts, it should show LoadingState and start networking calls, then when it's all completed, it should show LoadedState with networking call results and FailedState if something goes wrong. I want to achieve these without doing
if (state is LoadingState) {
blocA.add(Fetch());
return CircularProgressBar();
}
Your discomfort really has reason - no event should be fired from build() method (build() could be fired as many times as Flutter framework needs)
Our case is to fire initial event on Bloc creation
Possibilities overview
case with inserting Bloc with BlocProvider - this is preferred way
create: callback is fired only once when BlocProvider is mounted & BlocProvider would close() bloc when BlocProvider is unmounted
class ExampleScreenState extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Scaffold(
body: BlocProvider(
create: (context) => ExampleBloc()..add(Fetch()), // <-- first event,
child: BlocBuilder<ExampleBloc, ExampleState>(
builder: (BuildContext context, state) {
...
},
),
),
);
}
}
case when you create Bloc in State of Statefull widget
class _ExampleScreenStateState extends State<ExampleScreenState> {
ExampleBloc _exampleBloc;
#override
void initState() {
super.initState();
_exampleBloc = ExampleBloc();
_exampleBloc.add(Fetch());
// or use cascade notation
// _exampleBloc = ExampleBloc()..add(Fetch());
}
#override
void dispose() {
super.dispose();
_exampleBloc.close(); // do not forget to close, prefer use BlocProvider - it would handle it for you
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: BlocBuilder<ExampleBloc, ExampleState>(
bloc: _exampleBloc,
builder: (BuildContext context, state) {
...
},
),
);
}
}
add first event on Bloc instance creation - this way has drawbacks when testing because first event is implicit
class ExampleBloc extends Bloc<ExampleEvent, ExampleState> {
...
ExampleBloc() {
add(Fetch());
}
}
// insert it to widget tree with BlocProvider or create in State
BlocProvider( create: (_) => ExampleBloc(), ...
// or in State
class _ExampleScreenStateState extends State<ExampleScreenState> {
final _exampleBloc = ExampleBloc();
...
PS feel free to reach me in comments
Sergey Salnikov has a great answer. I think I can add another suggestion however.
In my main.dart file I am using a MultiBlocProvider to create all my blocs for use further down the tree. Like so
Widget build(BuildContext context) {
return MultiBlocProvider(
providers: <BlocProvider<dynamic>>[
BlocProvider<OneBloc>(create: (_) => OneBloc()),
BlocProvider<TwoBloc>(create: (_) => TwoBloc()),
],
child: MaterialApp( // Rest of your app )
Then when I need to call an event when I load a page, in this case I wanted to fetch some data depending on a list tile selected, and I needed more options than FutureBuilder can provide me, I simple used initState(); and called the bloc provider and added an event.
class _ExampleScreenState extends State<ExampleScreen> {
#override
void initState() {
super.initState();
BlocProvider.of<OneBloc>(context)
.add(FetchData);
}
It works because the bloc has already been provided from the root widget.
In simple terms:
Using BlocProvider, call it during creation.
BlocProvider(create: (context) => ExampleBloc()..add(Fetch()))
Using BlocState, use it as
class _ExampleScreenStateState extends State<ExampleScreenState> {
ExampleBloc _exampleBloc;
#override
void initState() {
super.initState();
_exampleBloc = ExampleBloc()..add(Fetch());
}
#override
void dispose() {
super.dispose();
_exampleBloc.close();
}

FutureBuilder runs twice

I have problems with FutureBuilder starting twice.
First it fetch the data correctly, returning my StartScreen, then after few seconds, the StartScreen rebuilds and I noticed that the FutureBuilder fires again.
Here is my code and it's pretty simple, so I wonder what may the problem be?!?
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
FirebaseUser user;
#override
void initState() {
// TODO: implement initState
super.initState();
getNewestlocation();
}
#override
void dispose() {
super.dispose();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
debugShowCheckedModeBanner: false,
title: 'APP',
theme: buildTheme(),
home: FutureBuilder<FirebaseUser>(
future: Provider.of<AuthService>(context).getUser(),
builder: (context, AsyncSnapshot<FirebaseUser> snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.error != null) {
print('error');
return Text(snapshot.error.toString());
}
user = snapshot.data;
print('user here $user');
return snapshot.hasData ? StartScreen(user) : LoginScreen();
} else {
return LoadingCircle();
}
},
),
);
}
}
Can anyone help me with this, please?
The future is firing again because you're creating it in the build method at the same time as the FutureBuilder.
From the FutureBuilder docs:
The future must have been obtained earlier, e.g. during State.initState, State.didUpdateConfig, 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.
So to prevent it from firing you'd have to do something like this:
class _MyAppState extends State<MyApp> {
Future<String> _myString;
#override
void initState() {
super.initState();
_myString = _fetchString();
}
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: _myString,
builder: (context, snapshot) {
// build page stuff...
},
),
);
}
}
Future<String> _fetchString() async {
print('running future function');
await Future.delayed(Duration(seconds: 3));
return 'potatoes';
}
Note, to access a provider in initState() you have to set listen to false, as detailed in this answer.
I think you have some things bad in your code, maybe that's not the problem but is good to correct that:
first: It is not recommendable to do that job in your main file, you should have something like a Splash page to handle that.
second: You should use blocs and not write your logic code on the same place at the view(UI)
If you're using android studio, try if running from the terminal fix the issue. The run button attached the debug service, which then force the entire app to be rebuilt

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