Flutter: How to fetch data from api only once while using FutureBuilder? - flutter

How can I fetch data only once while using FutureBuilder to show a loading indicator while fetching?
The problem is that every time the user opens the screen it will re-fetch the data even if I set the future in initState().
I want to fetch the data only the first time the user opens the screen then I will use the saved fetched data.
should I just use a stateful widget with a loading variable and set it in setState()?
I'm using Provider package
Future<void> fetchData() async {
try {
final response =
await http.get(url, headers: {'Authorization': 'Bearer $_token'});......
and my screen widget:
class _MyScreenState extends State<MyScreen> {
Future<void> fetchData;
#override
void initState() {
fetchData =
Provider.of<Data>(context, listen: false).fetchData();
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: fetchData,
builder: (ctx, snapshot) =>
snapshot.connectionState == ConnectionState.done
? Consumer<Data>(
builder: (context, data, child) => Text(data.fetchedData)): Center(
child: CircularProgressIndicator(),
),
);
}
}

If you want to fetch the data only once even if the widget rebuilds, you would have to make a model for that. Here is how you can make one:
class MyModel{
String value;
Future<String> fetchData() async {
if(value==null){
try {
final response =
await http.get(url, headers: {'Authorization': 'Bearer $_token'});......
value=(YourReturnedString)
}
}
return value;
}
}
Don't forget to place MyModel as a Provider. In your FutureBuilder:
#override
Widget build(context) {
final myModel=Provider.of<MyModel>(context)
return FutureBuilder<String>(
future: myModel.fetchData(),
builder: (context, snapshot) {
// ...
}
);
}

A simple approach is by introducing a StatefulWidget where we stash our Future in a variable. Now every rebuild will make reference to the same Future instance:
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
Future<String> _future;
#override
void initState() {
_future = callAsyncFetch();
super.initState();
}
#override
Widget build(context) {
return FutureBuilder<String>(
future: _future,
builder: (context, snapshot) {
// ...
}
);
}
}
Or you can simply use a FutureProvider instead of the StatefulWidget above:
class MyWidget extends StatelessWidget {
// Future<String> callAsyncFetch() => Future.delayed(Duration(seconds: 2), () => "hi");
#override
Widget build(BuildContext context) {
// print('building widget');
return FutureProvider<String>(
create: (_) {
// print('calling future');
return callAsyncFetch();
},
child: Consumer<String>(
builder: (_, value, __) => Text(value ?? 'Loading...'),
),
);
}
}

You can implement provider and pass data among its child.
Refer this example for fetching the data once and using it throughout its child.

As Aashutosh Poudel suggested, you could use an external object to maintain your state,
FOR OTHERS COMING HERE!
To manage state for large applications, the stateful widgets management becomes a bit painful. Hence you have to use an external state object that is shall be your single source of truth.
State management in flutter is done by the following libraries | services:
i. Provider: Well, i have personally played with this a little bit, even did something with it. I could suggest this for beginners.
ii. GetX: That one library that can do everything, its a good one and is recommended for novice || noob.
iii. Redux: For anyone coming from the react and angular world to flutter, this is a very handy library. I personally love this library, plus when you give it additional plugins, you are just superman
iv. Bloc: Best for data that is in streams. in other words, best for reactive programming approach....
Anyways, that was a lot given your question. Hope i helped

Related

Flutter - FutureBuilder fires twice on hot reload

In my flutter project when I start the project in the simulator everything works fine and the future builder only fires once, but when I do hot reload the FutureBuilder fires twice which causes an error any idea how to fix this?
Future frameFuture() async {
var future1 = await AuthService.getUserDataFromFirestore();
var future2 = await GeoService.getPosition();
return [future1, future2];
}
#override
void initState() {
user = FirebaseAuth.instance.currentUser!;
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: frameFuture(),
builder: (context, snap) {
if (snap.connectionState == ConnectionState.done && snap.hasData) return HomePage();
else return Container(
color: Colors.black,
child: Center(
child: spinKit,
),
);
}
);
}
I solved the issue. I put the Future function in the initState and then used the variable in the FutureBuilder. I'm not sure why it works this way, but here's the code:
var futures;
Future frameFuture() async {
var future1 = await AuthService.getUserDataFromFirestore();
var future2 = await GeoService.getPosition();
return [future1, future2];
}
#override
void initState() {
user = FirebaseAuth.instance.currentUser!;
super.initState();
futures = frameFuture();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: futures,
builder: (context, snap) {
if (snap.connectionState == ConnectionState.done && snap.hasData) return HomePage();
else return Container(
color: Colors.black,
child: Center(
child: spinKit,
),
);
}
);
}
The solution as you already figured out is to move the future loading process to the initState of a StatefulWidget, but I'll explain the why it happens:
You were calling your future inside your build method like this:
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: frameFuture(),
The issue is that Flutter calls the build method each time it renders the Widget, whenever a dependency changes(InheritedWidget, setState) or Flutter decides to rebuild it. So each time you redraw your UI frameFuture() gets called, this makes your build method to have side effects (this async call) which it should not, and is encouraged for widgets not to have side effects.
By moving the async computation to the initState you're only calling it once and then accessing the cached variable futures from your state.
As a plus here is an excerpt of the docs of the FutureBuilder class
"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."
Hope this makes clear the Why of the solution.
This can happen even when the Future is called from initState. The prior solution I was using felt ugly.
The cleanest solution is to use AsyncMemoizer which effectively just checks if a function is run before
import 'package:async/async.dart';
class SampleWid extends StatefulWidget {
const SampleWid({Key? key}) : super(key: key);
final AsyncMemoizer asyncResults = AsyncMemoizer();
#override
_SampleWidState createState() => _SampleWidState();
}
class _SampleWidState extends State<SampleWid> {
#override
void initState() {
super.initState();
_getData();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: widget.asyncResults.future,
builder: (context, snapshot) {
if (!snapshot.hasData) return yourLoadingAnimation();
// ... Do things with the data!
});
}
// The async and await here aren't necessary.
_getData() async () {
await widget.asyncResults.runOnce(() => yourApiCall());
}
}
Surprisingly, there's no .reset() method. It seems like the best way to forcibly rerun it is to override it with a new AsyncMemoizer(). You could do that easily like this
_getData() async ({bool reload = false}) {
if (reload) widget.asyncResults = AsyncMemoizer();
await widget.asyncResults.runOnce(() => yourApiCall());
}

Flutter set state not updating my UI with new data

I have a ListView.builder widget wrapped inside a RefreshIndicator and then a FutureBuilder. Refreshing does not update my list, I have to close the app and open it again but the refresh code does the same as my FutureBuilder.
Please see my code below, when I read it I expect the widget tree to definitely update.
#override
void initState() {
super.initState();
taskListFuture= TaskService().getTasks();
}
#override
Widget build(BuildContext context) {
return Consumer<TaskData>(builder: (context, taskData, child) {
return FutureBuilder(
future: taskListFuture,
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
taskData.tasks = (snapshot.data as ApiResponseModel).responseBody;
return RefreshIndicator(
onRefresh: () async {
var responseModel = await TaskService().getTasks();
setState(() {
taskData.tasks = responseModel.responseBody;
});
},
child: ListView.builder(
...
...
Let me know if more code is required, thanks in advance!
Points
I am using a StatefulWidget
Task data is a class that extends ChangeNotifier
When I debug the refresh I can see the new data in the list, but the UI does not update
getTasks()
Future<ApiResponseModel> getTasks() async {
try {
var _sharedPreferences = await SharedPreferences.getInstance();
var userId = _sharedPreferences.getString(PreferencesModel.userId);
var response = await http.get(
Uri.parse("$apiBaseUrl/$_controllerRoute?userId=$userId"),
headers: await authorizeHttpRequest(),
);
var jsonTaskDtos = jsonDecode(response.body);
var taskDtos= List<TaskDto>.from(
jsonTaskDtos.map((jsonTaskDto) => TaskDto.fromJson(jsonTaskDto)));
return ApiResponseModel(
responseBody: taskDtos,
isSuccessStatusCode: isSuccessStatusCode(response.statusCode));
} catch (e) {
return null;
}
}
The issue here seems to be that you are updating a property that is not part of your StatefulWidget state.
setState(() {
taskData.tasks = responseModel.responseBody;
});
That sets a property part of TaskData.
My suggestion is to only use the Consumer and refactor TaskService so it controls a list of TaskData or similar. Something like:
Provider
class TaskService extends ChangeNotifier {
List<TaskData> _data;
load() async {
this.data = await _fetchData();
}
List<TaskData> get data => _data;
set data(List<TaskData> data) {
_data = data;
notifyListeners();
}
}
Widget
class MyTaskList extends StatelessWidget {
#override
Widget build(BuildContext context) {
return Consumer<TaskService>(builder: (context, service, child) {
return RefreshIndicator(
onRefresh: () {
service.getTasks();
},
child: ListView.builder(
itemCount: service.data.length,
itemBuilder: (BuildContext context, int index) {
return MyTaskItem(data:service.data[index]);
},
),
);
});
}
}
and make sure to call notifyListeners() in the service.getTasks() method to make the Consumer rebuild
I think (someone will correct me if I'm wrong) the problem is that you are using the FutureBuilder, once it's built, you need to refresh to whole widget for the FutureBuilder to listen to changes. I can suggest a StreamBuilder that listens to any changes provided from the data model/api/any kind of stream of data. Or better yet, you can use some sort of state management like Provider and use Consumer from the Provider package that notifies the widget of any changes that may occurred.

How to access Shared Preferences String in build method flutter

I'm trying to access userEmail in shared preferences, inside my build method. Here's some of the code for context:
Widget build(BuildContext context) {
final prefs = await SharedPreferences.getInstance();
final userEmail = prefs.getString('userEmail') ?? '';
...
Return Scaffold(
body: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: StreamBuilder<QuerySnapshot>(
stream: _firestore.collection(userEmail).orderBy('time', descending: false).snapshots(),
...
The issue I have is, an error comes up highlighting the await. When i hover over it with my cursor for more info, it say The await expression can only be used in an async function. Try marking the function body with either 'async' or 'async*'.
There is then an option to add 'async' modifier. So i clicked that, which transformed code into this:
Future<Widget> build(BuildContext context) async {
...
This causes another error message: '_HomeScreenState.build' ('Future<Widget> Function(BuildContext)') isn't a valid override of 'State.build' ('Widget Function(BuildContext)').
Any ideas how to solve this issue? I've tried saving the userEmail using the Provider package. This works perfectly when the user first signs in or registers, but if you hot reload, the stream doesn't work.
You can use WidgetsBinding.instance.addPostFrameCallback, this helps you to run a callback during a frame, just after the persistent frame callbacks (which is when the main rendering pipeline has been flushed). If a frame is in progress and post-frame callbacks haven't been executed yet, then the registered callback is still executed during the frame. Otherwise, the registered callback is executed during the next frame.
In code, you can use it something like this.
#override
Widget build(BuildContext context) {
WidgetsBinding.instance.addPostFrameCallback((_) async {
final prefs = await SharedPreferences.getInstance();
final userEmail = prefs.getString('userEmail') ?? '';
});
Hope this answers your question.
Long Story short you should not perform any side effects inside your build method . See here
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
late final prefs ;
#override
void initState() async{
super.initState();
prefs = await SharedPreferences.getInstance();
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: prefs,
builder: (BuildContext context, AsyncSnapshot<dynamic> snapshot) {
if(snapshot.hasData){}else if (snapshot.hasError)
return Center(child: CircularProgressIndicator());
},);
}
}
As your build method can be called multiple times you should not perform network calls or call complex methods because as the docs say. This method can and will be called multiple times.
In your case I used a FutureBuilder to handle the future's state and awaited it in the initState insida a stateful widget.
Check this article for more info
As #croxx5f and #AhmetKAYGISIZ suggested, I ended up using FutureBuilder to solve this problem. Thank you both so much for your help with this.
Here's the final code for anyone else who is stuck on this problem:
class MyWidget extends StatefulWidget {
#override
_MyWidgetState createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
var prefs;
#override
void initState() {
super.initState();
getUserEmailFromSharedPrefs();
}
Future<String> getUserEmailFromSharedPrefs() async {
prefs = await SharedPreferences.getInstance();
final userEmail = prefs.getString('userEmail') ?? '';
return userEmail;
}
#override
Widget build(BuildContext context) {
return FutureBuilder(
future: getUserEmailFromSharedPrefs(),
builder: (context, AsyncSnapshot<String> snapshot) {
if(snapshot.hasData) {
return SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: StreamBuilder<QuerySnapshot>(
stream: _firestore.collection(snapshot.data).orderBy('time', descending: false).snapshots(),
...
So in summary, I wrapped my streambuilder in a futurebuilder.

How do you test StreamBuilder in Flutter using Mockito?

I'm trying to test a widget that contains a StreamBuilder and I'm unable to return data.
Widget:
class HomeView extends StatelessWidget {
final EventServiceRepository eventService;
HomeView({this.eventService});
#override
Widget build(BuildContext context) {
return Scaffold(
body: StreamBuilder<List<Event>>(
stream: this.eventService.getEvents(dates: dates),
builder: (context, snapshot) {
if (!snapshot.hasData) return HomeLoading();
return EventsView(events: snapshot.data);
},
)
);
}
}
Test:
class MockedEventService extends Mock implements EventServiceRepository {}
class MockedEventStream extends Mock implements Stream<List<Event>> {}
testWidgets('Should find EventsView', (WidgetTester tester) async {
final mockedEventService = MockedEventService();
final mockedEventStream = MockedEventStream();
when(mockedEventService.getEvents(dates: ['1/1/2021']))
.thenAnswer((_) => mockedEventStream);
when(mockedEventStream); // return list of events?
await tester.pumpWidget(HomeView(eventService: mockedEventService));
expect(find.byType(EventsView), findsOneWidget);
});
I've tried a few things with mockedEventStream but It always returns snapshot.hasData == false
Two issues - first, you're mocking the Stream, but you're not putting any data on it, so the StreamBuilder assumes the Stream is empty. Second, once you do put data on the Stream, you need to pump the Widget a second time to refresh the StreamBuilder. I also personally wouldn't bother mocking the Stream itself.
My slightly revised version:
final mockedEventService = MockedEventService();
when(mockedEventService.getEvents()).thenAnswer((_) => Stream.value(true));
await tester.pumpWidget(MyHomePage(eventService: mockedEventService));
await tester.pump(Duration.zero);
expect(find.byKey(ObjectKey("B")), findsOneWidget);
Obviously need to change to reflect your original code (mock EventServiceRepository, return a Stream.value(List[]), check for the right Widget, etc etc).

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