Solved!
I am getting date from FireBase via a futurebuilder.
It returns a Row whose children is a list of a widget i created called SmallDogCard (since my app is about dogs).
On a screen i want users to be able to press on the SmallDogCard to select it and change the border color, howerver. This causes the futurebuilder to load the data again... How should i approach this?
My code:
Futurebuilder:
FutureBuilder(
future: DogOwnerModel().getUserData(),
builder: (context, snapshot) {
if (snapshot.hasError) {
return Text('error');
}
if (snapshot.connectionState ==
ConnectionState.done) {
List<SmallDogCard> dogCardList = [];
final userData =
snapshot.data as Map<String, dynamic>;
List<Map<String, dynamic>>? userDogs =
userData['user dog data'];
if (userDogs == null) {
return Text('no dogs yet');
} else {
for (Map<String, dynamic> dog
in userDogs) {
dogCardList.add(SmallDogCard(
dogName: dog['name'],
imageUrl: dog['profile image'],
isSelected: selectedDogs
.contains(dog['name'])
? true
: false,
selectCallback: selectDogCallback));
}
return Row(
mainAxisAlignment:
MainAxisAlignment.start,
children: dogCardList);
}
}
return Text('Loading');
},
),
Onpress function callback passed to the SmallDogCard (which uses setstate and makes the futurbuilder get the data again).
selectDogCallback(String name, bool isSelected) {
if (isSelected == false) {
selectedDogs.add(name);
} else {
selectedDogs.remove(name);
}
setState(() {
selectedDogs;
});
}
How can i accomplish this without the futurebuilder being called again?
So i can setstate which changes the border color of the SmallDogCard which is returned from the FutureBuilder.
Thank you in advance!
Problem solved!
Initializing the future in initstate, so after initstate it wont be called again!
Like this:
#override
void initState() {
// TODO: implement initState
super.initState();
_future = setFuture();
}
setFuture() async {
return await DogOwnerModel().getUserData(email: email);
}
Related
I'm trying to save a value in the shared preferences in flutter then get it. But it's always returning null. The value is being retrieved from an API that is working fine in the backend.
Here is my code:
Method in which i'm getting the data from the api:
List<LastOrder>? lastOrders;
var isLoaded3 = false;
int od_id = 0;
getLastOrderMethod() async {
lastOrders = await RemoteService().getLastOrder(2);
if (lastOrders != null) {
setState(() {
isLoaded = true;
});
return ListView.builder(
itemCount: 1,
itemBuilder: (BuildContext context, int index) {
setState(() {
od_id = lastOrders![0].id;
print('getLastOrderMethod: $od_id');
saveIdOrder(od_id);
});
return;
});
}
}
Method in which i'm trying to save the variable value in the shared preferences:
Future<bool> saveIdOrder(value) async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
print('save: $od_id');
return await sharedPreferences.setInt('order_id', value);
}
Method in which i'm trying to get the variable value in the shared preferences:
static Future getIdOrder() async {
SharedPreferences sharedPreferences = await SharedPreferences.getInstance();
final x = sharedPreferences.getInt('order_id');
print('get: $x');
return x;
}
#override
void initState() {
// TODO: implement initState
print('intial ${od_id}'); => 0
getIdOrder(); => null
getLastOrderMethod();
super.initState();
}
I'd be glad for any kind of help!
getIdOrder() is a future method, it will take some time to fetch the data. While initState cant be async method, you can use .then and inside it call setState to update the ui. but Using FutureBuilder will be best option.
late final future = getIdOrder();
#override
Widget build(BuildContext context) {
return Scaffold(
body: FutureBuilder(
future: future,
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text("${snapshot.data}"); // your widget
}
return CircularProgressIndicator();
},
),
floatingActionButton: FloatingActionButton(onPressed: () {}),
);
}
More about using FutureBuilder
Solved the issue by doing all the logic inside the listView.builder(), then updated the variable value inside a setState()
This is the future function I want to call:
Future<List<Marker>> getMarkers() async {
List<Marker> markerTemp = [];
List<String> friendsList = [];
QuerySnapshot snapshot = FireStoreUtils.getFriendsList(current.userID);
for (var doc in snapshot.docs) {
friendsList.add(doc.reference.id);
}
for (var friend in friendsList) {
DocumentSnapshot document = await locationRef.doc(friend).get();
MarkerTemp.add(Marker(...))
}
return markerTemp;
}
Now I want it to be called in FutureBuilder widget to save the results in a variable called markerList that is useful for my view. How can I do?
return FutureBuilder<List<Marker>>(
future: getMarkers(),
builder: (context, snapshot) {
if (snapshot.connectionState != ConnectionState.done) {
// async call has not finished
return const Center(child: CircularProgressIndicator());
}
if (snapshot.hasError) {
// getMarkers() throws an exception
return Center(child: Text(snapshot.error.toString()));
}
if (!snapshot.hasData) {
// getMarkers() returns null
return const Center(child: Text("getMarkers() returns null!"));
}
markerList = snapshot.data as List<Marker>; // cast to List<Marker>
return SomeWidget(); // use markerList in this Widget
},
);
Your future builder, when the future finishes in your case, returns a list of markers. Now to use that list, you don't have to store it again, it's already returned and stored in your snapshot in your future builder. You can validate this by printing the length of it:
if(snapshot.hasData) print(snapshot.data.length.toString());
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 can I update the UI if I need to wait for the FutureBuilder? Do I need to call my future function twice, one for for the builder and one again to change the UI?
FutureBuilder<String>(
future: getUserOrder(4045),
builder: (context, snapshot) {
if (snapshot.hasData) {
return Text(snapshot.data,style: Theme.of(context).textTheme.headline);
} else if (snapshot.hasError) {
// I need to change the state at this point
return Text("${snapshot.error}",style: Theme.of(context).textTheme.headline);
} else {
return CircularProgressIndicator();
}
}),
Calling setState inside the FutureBuilder throws this error:
setState() or markNeedsBuild() called during build.
I don't need to display a button or any other other to be clicked. I want to perform the action automatically when the date is loaded in the futureBuilder
Since I couldn't call setState inside FutureBuilder the solution was remove it and do something like this:
getBillingInfo() {
Provider.of<MyRents>(context, listen: false)
.getBillingInfo(context)
.then((billingInfo) {
setState(() {
if (billingInfo["companyInfo"] != null &&
billingInfo["taxes"].isNotEmpty) {
_canGenerateInvoices = true;
} else {
_canGenerateInvoices = false;
}
});
});
}
...
void initState() {
super.initState();
getBillingInfo();
}
...
Visibility(
visible: _canGenerateInvoices,
child: MyWidget()
)
Having this, when I perform other actions I can always change the value of _canGenerateInvoices
I have a problem with Streams. I'm getting null form the stream. My objective is to combine 2 futures so I'm using first future when user has not searched any parameters and other one when user searched because I have 2 endpoints one for searched list and one that has no filters. I'm not really sure if that's standard industry practice for app dev. If its not please tell me so and how should I set up back end
Widget Body(BuildContext context, TabController taby) {
Future<List<littleOffers>> searchdata = null;
Stream<List<littleOffers>> _dataSwitch() async* {
if (searchdata == null)
yield* Stream.fromFuture(fetchOffers());
else
yield* Stream.fromFuture(searchdata);
}
void resetSearchData() {
searchdata = null;
}
void setSearchData(Future<List<littleOffers>> data) {
searchdata = data;
}
return Column(
children: <Widget>[
Search_Sort(context),
Expanded(
child: StreamBuilder(
stream: _dataSwitch(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.none || !snapshot.hasData) {
print('project snapshot data is: ${snapshot.data}');
print('project snapshot state is: ${snapshot.connectionState.toString()}');
return Container();
} else {
return TabBarView(controller: taby, children: [
getList(snapshot.data),
getList(snapshot.data),
getList(snapshot.data),
getList(snapshot.data),
]);
}
}),
),
],
);
}
That could work but I think you missed async and await on setSearchData
void setSearchData(Future<List<littleOffers>> newData) {
setState(()async{
searchdata = await newData;
});
}
and to remove data
void clearSearchData() {
setState((){
searchdata = null;
});
}
You can define only one List<littleOffers> (not future) ;
Widget Body(BuildContext context, TabController taby) {
List<littleOffers> searchdata = null;
Future<void> setSearchData(Future<List<littleOffers>> newData) async {
var _data = await newData;
setState((){
searchdata = _data;
});
}
return Column(
children: <Widget>[
Search_Sort(context),
Expanded(
child: searchData == null ? Center(child: CircularProcessIndicator()) : yourWidget;
),
],
);
}
Creating Stream from 2 futures so that one is alway actice and other one is on stand by when switch is needed.
Stream<List<littleOffers>> _dataOfferSwitch() async* {
if (searchOfferData == null)
yield* Stream.fromFuture(fetchOffers());
else
yield* Stream.fromFuture(searchOfferData);}