flutter - FutureBuilder auto rebuild each time press a button in the screen - flutter

I try to use FutureBuilder in Flutter to wait ulti my initState is finished then buil the UI for the app.
But when the app is running, the screen keep rebuilding each time I press another button (the button does totally different thing).
Future loadUser() async {
String jsonString = await storage.read(key: "jwt");
final jsonResponse = json.decode(jsonString);
loggedUser = new LoggedUser.fromJson(jsonResponse);
print(loggedUser.token);
getProfile();
getJourneyByUserId()
.then((receivedList){
addRanges(receivedList);});
}
Future<List<Journey>>getJourneyByUserId() async {
var res = await http.get(
Uri.parse("$baseUrl/journeys/userid=${loggedUser.user.userId}"),
headers: {
'Content_Type': 'application/json; charset=UTF-8',
'Authorization': 'Bearer ${loggedUser.token}',
},
);
if (res.statusCode == 200) {
print("Get journeys successfully");
}
var data = jsonDecode(res.body);
List idList = [];
for (var i in data) {
idList.add(i["journeyId"]);
}
for (var i in idList) {
var res = await http.get(
Uri.parse("$baseUrl/journeys/$i"),
);
var data = jsonDecode(res.body);
Journey userJourney = new Journey.fromJson(data);
setState(() {
journeyList.add(userJourney);
});
}
print("Journey ${journeyList.length}");
return journeyList;
}
addRanges(journeyList){
setState(() {
rangeList=[];
});
if (journeyList.isNotEmpty) {
for (var i in journeyList) {
DateTime startDate =
DateTime(i.startDate.year, i.startDate.month, i.startDate.day);
DateTime endDate =
DateTime(i.endDate.year, i.endDate.month, i.endDate.day);
setState(() {
rangeList.add(PickerDateRange(startDate, endDate));
});
}
}
print("Range ${rangeList.length}");
return rangeList;
}
returnRange() {
List<PickerDateRange> list = [];
for(int i =0; i<rangeList.length;i++){
list.add(rangeList[i]);
}
return list;
}
Future functionForBuilder() async {
return await returnRange();
}
//initState function
#override
void initState() {
super.initState();
loadUser();
functionForBuilder();
}
//build the UI
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("$_name's Profile",style: TextStyle(color: kColorPalette4),),
centerTitle: true,
),
body: Container(
child: FutureBuilder(
future: functionForBuilder(),
builder: (BuildContext context,AsyncSnapshot snapshot){
//here I set the condition for each case of snapshot
}
I have read some documents say that I should assign the functionForBuilder() to a Future variable when initState then use it in the future child of FutureBuilder. Example:
Future _future;
//initState function
#override
void initState() {
super.initState();
loadUser();
_future=functionForBuilder();
}
// then with the FutureBuilder
future: _future
With this way the screen is not rebuild anymore but my function returnRange() seems like not running as my expextation (I called the returnRange() once in the build() function).
Thanks in advance for your answer!

Whenever you assign to the _future variable again, you must do that inside a setState block, otherwise the widget will not rebuild with the new future.
For example:
void updateData() {
setState(() {
_future = functionForBuilder();
});
}

If you use FutureBuilder, it rebuild items again and again.
Try two ways:
Don't use `future: functionForBuilder(), comment it.
Remove FutureBuilder(), simply use Container().
And let me know any issue?

Code:
call your future in the initstate method not in the build as shown in the example.
class MyPage extends StatefulWidget { #override State<MyPage> createState() => _MyPageState(); } class _MyPageState extends State<MyPage> { // Declare a variable. late final Future<int> _future; #override void initState() { super.initState(); _future = _calculate(); // Assign your Future to it. } // This is your actual Future. Future<int> _calculate() => Future.delayed(Duration(seconds: 3), () => 42); #override Widget build(BuildContext context) { return Scaffold( body: FutureBuilder<int>( future: _future, // Use your variable here (not the actual Future) builder: (_, snapshot) { if (snapshot.hasData) return Text('Value = ${snapshot.data!}'); return Text('Loading...'); }, ), ); } }

Related

Can I Use a Future<String> to 'Fill In' a Text() Widget Instead of Using FutureBuilder in Flutter?

I'm trying to better understand Futures in Flutter. In this example, my app makes an API call to get some information of type Future<String>. I'd like to display this information in a Text() widget. However, because my String is wrapped in a Future I'm unable to put this information in my Text() widget, and I'm not sure how to handle this without resorting to a FutureBuilder to create the small widget tree.
The following example uses a FutureBuilder and it works fine. Note that I've commented out the following line near the bottom:
Future<String> category = getData();
Is it possible to turn category into a String and simply drop this in my Text() widget?
import 'package:flutter/material.dart';
import 'cocktails.dart';
class CocktailScreen extends StatefulWidget {
const CocktailScreen({super.key});
#override
State<CocktailScreen> createState() => _CocktailScreenState();
}
class _CocktailScreenState extends State<CocktailScreen> {
#override
Widget build(BuildContext context) {
Cocktails cocktails = Cocktails();
Future<String> getData() async {
var data = await cocktails.getCocktailByName('margarita');
String category = data['drinks'][0]['strCategory'];
print('Category: ${data["drinks"][0]["strCategory"]}');
return category;
}
FutureBuilder categoryText = FutureBuilder(
initialData: '',
future: getData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
if (snapshot.connectionState == ConnectionState.done) {
if (snapshot.hasData) {
return Text(snapshot.data);
} else if (snapshot.hasError) {
return Text(snapshot.error.toString());
}
}
return const CircularProgressIndicator();
},
);
//Future<String> category = getData();
return Center(
child: categoryText,
);
}
}
Here's my Cocktails class:
import 'networking.dart';
const apiKey = '1';
const apiUrl = 'https://www.thecocktaildb.com/api/json/v1/1/search.php';
class Cocktails {
Future<dynamic> getCocktailByName(String cocktailName) async {
NetworkHelper networkHelper =
NetworkHelper('$apiUrl?s=$cocktailName&apikey=$apiKey');
dynamic cocktailData = await networkHelper.getData();
return cocktailData;
}
}
And here's my NetworkHelper class:
import 'package:http/http.dart' as http;
import 'dart:convert';
class NetworkHelper {
NetworkHelper(this.url);
final String url;
Future<dynamic> getData() async {
http.Response response = await http.get(Uri.parse(url));
if (response.statusCode == 200) {
String data = response.body;
var decodedData = jsonDecode(data);
return decodedData;
} else {
//print('Error: ${response.statusCode}');
throw 'Sorry, there\'s a problem with the request';
}
}
}
Yes, you can achieve getting Future value and update the state based on in without using Using FutureBuilder, by calling the Future in the initState(), and using the then keyword, to update the state when the Future returns a snapshot.
class StatefuleWidget extends StatefulWidget {
const StatefuleWidget({super.key});
#override
State<StatefuleWidget> createState() => _StatefuleWidgetState();
}
class _StatefuleWidgetState extends State<StatefuleWidget> {
String? text;
Future<String> getData() async {
var data = await cocktails.getCocktailByName('margarita');
String category = data['drinks'][0]['strCategory'];
print('Category: ${data["drinks"][0]["strCategory"]}');
return category;
}
#override
void initState() {
super.initState();
getData().then((value) {
setState(() {
text = value;
});
});
}
#override
Widget build(BuildContext context) {
return Text(text ?? 'Loading');
}
}
here I made the text variable nullable, then in the implementation of the Text() widget I set to it a loading text as default value to be shown until it Future is done0
The best way is using FutureBuilder:
FutureBuilder categoryText = FutureBuilder<String>(
future: getData(),
builder: (BuildContext context, AsyncSnapshot snapshot) {
switch (snapshot.connectionState) {
case ConnectionState.waiting:
return Text('Loading....');
default:
if (snapshot.hasError) {
return Text('Error: ${snapshot.error}');
} else {
var data = snapshot.data ?? '';
return Text(data);
}
}
},
),
but if you don't want to use FutureBuilder, first define a string variable like below and change your adasd to this :
String category = '';
Future<void> getData() async {
var data = await cocktails.getCocktailByName('margarita');
setState(() {
category = data['drinks'][0]['strCategory'];
});
}
then call it in initState :
#override
void initState() {
super.initState();
getData();
}
and use it like this:
#override
Widget build(BuildContext context) {
return Center(
child: Text(category),
);
}
remember define category and getData and cocktails out of build method not inside it.

Nested Future in Flutter

I'm new to Flutter, (comming from web and especially JS/VueJS)
I'm have a db in firebase that has a collection called edito and inside, i have different artist with a specific Id to call Deezer Api with it.
So what i want to do is first called my db and get the Id for each of artist and then put this id in a function as parameter to complete the url.
I did 2 Future function, one to call the db and one to call the api.
But i don't understand how to use one with the others in the build to get a listview with the information of the api of deezer for each data.
i'm getting the list but it's stuck in and endless loop.
All of my app will be on this nested function, is it possible to do this and call it in any widget that i want ?
here is my code, thanks
import 'package:flutter/material.dart';
import 'package:cloud_firestore/cloud_firestore.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
class GetAlbum extends StatefulWidget {
#override
_GetAlbumState createState() => _GetAlbumState();
}
class _GetAlbumState extends State<GetAlbum> {
Map mapResponse;
Future<QuerySnapshot> getDocument() async{
return FirebaseFirestore.instance.collection("edito").get();
}
Future<dynamic> fetchData(id) async{
http.Response response;
response = await http.get('https://api.deezer.com/album/' + id);
if(response.statusCode == 200){
setState(() {
mapResponse = json.decode(response.body);
});
}
}
Future<dynamic> getDocut;
Future<dynamic> getArtist;
#override
void initState() {
getDocut = getDocument();
getArtist = fetchData(null);
super.initState();
}
#override
Widget build(BuildContext context) {
return FutureBuilder<QuerySnapshot>(
future : getDocut,
builder: (BuildContext context, AsyncSnapshot<QuerySnapshot> snapshot){
if(!snapshot.hasData) {
return CircularProgressIndicator();
}else{
return new ListView(
children: snapshot.data.docs.map<Widget>((document){
print(document.data().length);
return FutureBuilder(
future: fetchData(document.data()['idDeezer'].toString()),
builder: (context, snapshot){
return Container(
child: mapResponse==null?Container(): Text(mapResponse['title'].toString(), style: TextStyle(fontSize: 30),),
);
}
);
}).toList(),
);
}
},
);
}
}
Here's a simplified example of making two linked Future calls where the 2nd depends on data from the first, and using the results in a FutureBuilder:
import 'package:flutter/material.dart';
class FutureBuilder2StatefulPage extends StatefulWidget {
#override
_FutureBuilder2StatefulPageState createState() => _FutureBuilder2StatefulPageState();
}
class _FutureBuilder2StatefulPageState extends State<FutureBuilder2StatefulPage> {
Future<String> _slowData;
#override
void initState() {
super.initState();
_slowData = getAllSlowData(); // combined async calls into one future
}
// linked async calls
Future<String> getAllSlowData() async {
int id = await loadId(); // make 1st async call for id
return loadMoreData(id: id); // use id in 2nd async call
}
Future<int> loadId() async {
int _id = await Future.delayed(Duration(seconds: 2), () => 42);
print('loadId() completed with: $_id'); // debugging
return _id;
}
Future<String> loadMoreData({int id}) async {
return await Future.delayed(Duration(seconds: 2), () => 'Retrieved data for id:$id');
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text('FutureBldr Stateful'),
),
body: FutureBuilder<String>(
future: _slowData,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Center(child: Text(snapshot.data));
}
return Center(child: Text('Loading...'));
},
),
);
}
}
This avoids having to nest the FutureBuilder which may be error prone.
And calling future methods directly from a FutureBuilder is not recommended since the call could be made many times if its containing widget is rebuilt (which can happen a lot).
I tried to add firebase in the first one but i get null for the id in the get AllSlowDAta but i got it right with the Future.delayed.
// linked async calls
Future<String> getAllSlowData() async {
String id = await loadId(); // make 1st async call for id
return loadMoreData(id: id); // use id in 2nd async call
}
Future<dynamic> loadId() async {
//return await Future.delayed(Duration(seconds: 2), () => '302127');
await FirebaseFirestore.instance.collection("edito")
.get()
.then((QuerySnapshot querySnapshot) {
querySnapshot.docs.forEach((doc) {
return doc.data()["idDeezer"];
});
});
}
Future<dynamic> loadMoreData({String id}) async {
http.Response response;
response = await http.get('https://api.deezer.com/album/' + id);
if(response.statusCode == 200){
setState(() {
return json.decode(response.body);
});
}
}

Getx is not working properly with FutureBuilder for update list

I'm using the Getx controller in my project. I have create the controller for FutureBuilder for displaying list but .Obs is not set on Future Function. I'm sharing the code.
class PPHomeController extends GetxController {
Future<List<PPProductRenterModel>> listNearProduct;
// i want to set .Obs end of the "listNearProduct" but it's not working because of Future.
FetchNearProductList({#required int price}) async {
listNearProduct = CallGetNearProducts();// Http API Result
}
}
{
PPHomeController _homeController = Get.put(PPHomeController());
Widget mainProductListView() {
return FutureBuilder<List<PPProductRenterModel>>
(builder: (context, AsyncSnapshot<List<PPProductRenterModel>> projectSnap){
if(!projectSnap.hasData){
if(projectSnap.connectionState == ConnectionState.waiting){
return Container(
child: Loading(),
);
}
}
return ListView.builder(
itemCount: projectSnap.data.length,
itemBuilder: (context, index) {
PPProductRenterModel model = projectSnap.data[index];
PPPrint(tag: "CheckId",value: model.productId);
return ProductMainItemRow(model);
});
},
future: _homeController.listNearProduct,);
There is a cleaner way for implementing List in GetX without worrying about Type-Casting:
Instantiate it:
final myList = [].obs;
Assign it:
myList.assignAll( listOfAnyType );
(Reference) Flutter error when using List.value :
'value' is deprecated and shouldn't be used. List.value is deprecated.
use [yourList.assignAll(newList)]. Try replacing the use of the
deprecated member with the replacement.
Detailed code example
ProductController.dart
class ProductController extends GetxController {
final productList = [].obs;
#override
void onInit() {
fetchProducts();
super.onInit();
}
void fetchProducts() async {
var products = await HttpServices.fetchProducts();
if (products != null) {
productList.assignAll(products);
}
}
}
HttpServices.dart
class HttpServices {
static var client = http.Client();
static Future<List<Product>> fetchProducts() async {
var url = 'https://link_to_your_api';
var response = await client.get(url);
if (response.statusCode == 200) {
return productFromJson(response.body);
} else {
return null;
}
}
}
product.dart
class Product {
Product({
this.id,
this.brand,
this.title,
this.price,
....
});
....
}
Form the docs:
3 - The third, more practical, easier and preferred approach, just add
.obs as a property of your value:
final items = <String>[].obs;
Following that instruction, this should work:
final listNearProduct = Future.value(<PPProductRenterModel>[]).obs;
E.g.:
// controller
final list = Future.value(<String>[]).obs;
#override
void onInit() {
super.onInit();
fetchList();
}
Future<List<String>> callApi() async {
await Future.delayed(Duration(seconds: 2));
return ['test'];
}
void fetchList() async {
list.value = callApi();
}
// screen
#override
Widget build(BuildContext context) {
return GetX<Controller>(
init: Controller(),
builder: (controller) {
return FutureBuilder<List<String>>(
future: controller.list.value,
builder: (context, snapshot) {
if (snapshot.hasData) {
print(snapshot.data[0]); // Output: test
return Text(snapshot.data[0]);
} else if (snapshot.hasError) {
return Text("${snapshot.error}");
}
// By default, show a loading spinner.
return CircularProgressIndicator();
},
);
},
);
};
You never actually call FetchNearProductList.
You need to call it in some place, preferably before the FutureBuilder uses that Future.

How to reference variable in method in FutureBuilder (builder:)?

I want to use the variable dbRef in inputData() in future Builder builder: you can see the variable in between asterisk .
void inputData() async {
FirebaseUser user = await FirebaseAuth.instance.currentUser();
final uid = user.uid;
final **dbRef** = FirebaseDatabase.instance.reference().child("Add Job Details").child(uid).child("Favorites");
}
#override
Widget build(BuildContext context) {
return FutureBuilder (
future: **dbRef**.once(),
builder: (context, AsyncSnapshot<DataSnapshot> snapshot) {
if (snapshot.hasData) {
List<Map<dynamic, dynamic>> list = [];
for (String key in snapshot.data.value.keys) {
list.add(snapshot.data.value[key]);
}
This is one more approach to tackle the problem.
The idea is to use a variable _loading and set it to true initially.
Now, after in your inputData() function, you can set it to false once you get the dbref.
Store dbref, the way I stored _myFuture in the code below i.e., globally within the class.
Use your _loading variable to return a progress bar if its true else return FutureBuilder with your dbref.once() in place. Now, that you have loaded it, it should be available at this point.
class MyWidget extends StatefulWidget {
#override
createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
// Is the future being loaded?
bool _loading;
// This is the future we will be using in our FutureBuilder.
// It is currently null and we will assign it in _loadMyFuture function.
// Until assigned, we will keep the _loading variable as true.
Future<String> _myFuture;
// Load the _myFuture with the future we are going to use in FutureBuilder
Future<void> _loadMyFuture() async {
// Fake the wait for 2 seconds
await Future.delayed(const Duration(seconds: 2));
// Our fake future that will take 2 seconds to return "Hello"
_myFuture = Future(() async {
await Future.delayed(const Duration(seconds: 2));
return "Hello";
});
}
// We initialize stuff here. Remember, initState is called once in the beginning so hot-reload wont make flutter call it again
#override
initState() {
super.initState();
_loading = true; // Start loading
_loadMyFuture().then((x) => setState(() => _loading = false)); // Set loading = false when the future is loaded
}
#override
Widget build(BuildContext context) {
// If loading, show loading bar
return _loading?_loader():FutureBuilder<String>(
future: _myFuture,
builder: (context, snapshot) {
if(!snapshot.hasData) return _loader(); // still loading but now it's due to the delay in _myFuture
else return Text(snapshot.data);
},
);
}
// A simple loading widget
Widget _loader() {
return Container(
child: CircularProgressIndicator(),
width: 30,
height: 30
);
}
}
Here is the output of this approach
This does the job but, you might need to do it for every class where you require your uid.
========================================
Here is the approach I described in the comments.
// Create a User Manager like this
class UserManager {
static String _uid;
static String get uid => _uid;
static Future<void> loadUID() async {
// Your loading code
await Future.delayed(const Duration(seconds: 5));
_uid = '1234'; // Let's assign it directly for the sake of this example
}
}
In your welcome screen:
class MyWidget extends StatefulWidget {
#override
createState() => _MyWidgetState();
}
class _MyWidgetState extends State<MyWidget> {
bool _loading = true;
#override
void initState() {
super.initState();
UserManager.loadUID().then((x) => setState(() => _loading = false));
}
#override
Widget build(BuildContext context) {
return _loading ? _loader() : Text('Welcome User ${UserManager.uid}!');
}
// A simple loading widget
Widget _loader() {
return Container(child: CircularProgressIndicator(), width: 30, height: 30);
}
}
The advantage of this method is that once you have loaded the uid, You can directly access it like this:
String uid = UserManager.uid;
thus eliminating use of futures.
Hope this helps!

Get data from future function to text widget

I am trying to implement a Column with a Text:
Column(
children: <Widget>[
Text('data from future function')
],
),
I can't get the data from initState() cause initState() it's only void
If I get data directly from the function
Text(function)
I get
instance of function
The function:
Future<double> calculate(int index) async {
LocData _getUser = await getLoc();
double uLat = _getUser.latitude;
double uLng = _getUser.latitude;
double pLat = parks[data].loc.lat;
double pLng = parks[data].loc.lng;
double dis = await Geolocator()
.distanceBetween(uLat , uLng, uLng , pLat );
return dis ;
}
Any idea what can i do to get this data from the function directly to the text wigdet?
There 2 ways to get data from a future.
Option #1:
(with 2 suboptions)
class MyWidgetState extends State<MyWidget> {
String _someAsyncData;
#override
void initState() {
super.initState();
// opt 1.
aDataFuture.then((val) {
setState(() {
_someAsyncdata = val;
});
});
// opt 2.
_setAsyncData(aDataFuture);
}
void _setAsyncData(Future<String> someFuture) async {
// void + async is considered a "fire and forget" call
// part of opt. 2
_someAsyncData = await someFuture;
// must trigger rebuild with setState
setState((){});
}
Widget build(BuildContext context) {
return _someAsyncData == null ? Container() : Text('$_someAsyncData');
}
}
Option #2
Use FutureBuilder
class MyWidget extends StatelessWidget {
Widget build(BuildContext context) {
return FutureBuilder<String>(
future: _someFuture,
builder: (ctx, snapshot) {
// can also check for snapshot.hasData or snapshot.hasError to
// provide more user feedback eg.
if(snapshot.connectionState == ConnectionState.done)
return Text('${snapshot.data}');
return Text('No data available yet...');
}
);
}
}
Here is the full working code.
class _InfoPageState extends State<InfoPage> {
String _text = "";
#override
void initState() {
super.initState();
calculate(10).then((value) {
setState(() {
_text = value.toString();
});
});
}
Future<double> calculate(int index) async {
LocData _getUser = await getLoc();
double uLat = _getUser.latitude;
double uLng = _getUser.latitude;
double pLat = parks[data].loc.lat;
double pLng = parks[data].loc.lng;
double dis = await Geolocator().distanceBetween(uLat, userLng, uLng, pLat);
return dis;
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(),
body: Column(children: <Widget>[Text(_text)]),
);
}
}