Flutter GetX, call a controller method after all widgets has been built - flutter

I am using GetX and I have following codes.
RxList<Outlets?> listPharmacyOutlets = <Outlets>[].obs;
RxList listOutletCoordinates = [].obs;
#override
void onInit() async{
super.onInit();
await getPharmacyOutlets();
}
Future getPharmacyOutlets() async{
Map params = {
//some parameters
}
var res = await CommonApiProvider.getPharmacyOutlets(params)
var outlets = res.data;
int idx = 0;
listPharmacyOutlets.clear();
for(final outlet in outlets){
listPharmacyOutlets.add(Outlets(
"latitude": outlet.latitude,
"longitude": outlet.longitude,
"pharmacyId": outlet.id,
"outletName": outlet.name,
"address": null
));
//now populating address list to fetch addresses for all outlets
listOutletCoordinates.add({
"index": idx,
"latitude": outlet.latitude,
"longitude": outlet.longitude,
});
idx++;
}
//I cannot call getOutletAddresses() here because view will not be populated unless this completes.
}
Future getOutletAddresses() async {
await Future.forEach(listOutletCoordinates, (item) async {
var result = await CommonApiProvider.getOutletAddresses(item);
//from result update "address" in "listPharmacyOutlets" by the help of index property in item
});
}
Here is my view.
SingleChildScrollView(
physics: AlwaysScrollableScrollPhysics(),
child: Column(
children: List.generate(controller.listPharmacyOutlets.length, (index) {
var item = controller.listPharmacyOutlets[index];
return Container(
child: .......
);
})
),
);
What I want to do is, first method getPharmacyOutlets() call only fetches outlets from Rest api and when list of outlets is completed rendering in list,
call getOutletAddresses() method to fetch address from google's geocoding service against the supplied latitude and longitudes. After address has been fetched, i will
update each row in the rendered list of outlets.
I am fetching addresses and outlets separately because I don't want user to wait for all results to come.
Problem, I am not sure when to call getOutletAddresses() api so that the addresses are fetched only after outlets has been rendered in the view.
I have in my mind that this could be achieved using WidgetsBinding like this.
WidgetsBinding.instance.addPostFrameCallback((_) {
// here call method to fetch address from google service.
});
But I am not sure where to use this because I am using GetX, which means all my pages are stateless
Any help or any other better Idea will help me a lot.
Thanks

The best approach is to use the worker functions provided by getx controller like:
ever - is called every time the Rx variable emits a new value.
everAll - Much like ever , but it takes a List of Rx values Called every time its variable is changed. That's it.
once - is called only the first time the variable has been changed.
For the present situation,
You can use once or everand based on the required condition you can then call your desired function
So the best time to call the getOutletAddresses() function is after listOutletCoordinates is filled .
And in my opinion it's not about the view rendering and then calling a function. Instead its to focus on the data being fetched sequentially. Because the View updation is taken care by Getx Builder or Obx() provided by Getx once the data is properly fetched
If using ever:
ever(listOutletCoordinates,(value){
// Based on the value you can call the getOutletAdresses() function
});
But with once you get more flexibility , Use something like:
once(listOutletCoordinates,(value){
// Based on the value you can call the getOutletAdresses() function
},condition:() => listOutletCoordinates.isNotEmpty);
Alternatively:
You can use listen property to call function based on listening value.
Example:
listOutletCoordinates.listen((val){
// Based on the value you can call the getOutletAdresses() function
});

Related

Change a dropdown's items when another dropdown's value is chosen in flutter (UI wont update)

I have two drop downs, and I want to do things when they get values selected. One of those is to change the second buttondrop items based on what's selected in the first dropdown.
For example:
Dropdown1 is a list of car manufactuers
Dropdown2 is a list of their models
Dropdown1 selects mercedes
Dropdown2 gets "E Class, S Class" etc
Dropdown1 selects lexus
Dropdown2 gets "ES, LS", etc
(Eventually the second drop down will update a listview as well, but haven't gotten to that yet.)
Data wise, it works, I update the list. The problem is the UI won't update unless I do a hot reload
Currently I am just having the dropdowns fetch their data and using Future builders
Future? data1;
Future? data2;
void initState(){
super.initState();
data1 = _data1AsyncMethod();
data2 = _data2AsyncMethod();
}
_data2AsyncMethod([int? item1_id]) async{
if(item1_id == null){
item2Classes = await DefaultItems().getAllItem2Classes();
listOfItem2ClassNames = DefaultItems().returnListOfItemClassNames(item2Classes);
}
else{
// The methods below calls the DefaultItems methods which have Futures all in them.
// The getAllItems calls a network file with GET methods of future type to get data and decodes them, etc.
// They build a list of the object type, ex List<Item2>
item2Classes = await DefaultItems().getAllItem2Classes(item1_id);
listOfItem2ClassNames = DefaultItems().returnListOfItemClassNames(item2Classes);
}
}
I have this Future Builder nested in some containers and paddings
FutureBuilder{
future: data2,
builder: (context, snapshot){
if(snapshot.connectionState != done...)
// return a circle progress indictator here
else{
return CustomDropDown{
hintText: 'example hint'
dropDownType: 'name'
dropDownList: listOfItem2ClassNames
dropDownCallback: whichDropDown,
}
The onChanged in CustomDropDown passes the dropDownType and the dropDownValue
The callback
whichDropDown(String dropDownType, String dropDownValue){
if(dropDownType == 'item1'){
//so if the first dropdown was used
// some code to get item_1's id and I call the data2 method
_data2AsyncMethod(item1_id);
}
Again the data updates (listOfItem2ClassNames) BUT the UI won't update unless I hot reload. I've even called just setState without any inputs to refresh but doesn't work
So how do I get the UI to update with the data, and is my solution too convoluted in the first place? How should I solve? StreamBuilders? I was having trouble using them.
Thanks
If you do a setState in the whichDropDown function, it will rebuild the UI. Although I'm not exactly sure what you want, your question is really ambiguous.
whichDropDown(String dropDownType, String dropDownValue){
if(dropDownType == 'item1'){
//so if the first dropdown was used
// some code to get item_1's id and I call the data2 method
_data2AsyncMethod(item1_id).then((_) {
setState(() {});
});
}
}
I notice a couple things:
nothing is causing the state to update, which is what causes a rebuild. Usually this is done explicitly with a call to setState()
in whichDropdown(), you call _data2AsyncMethod(item1_id), but that is returning a new Future, not updating data2, which means your FutureBuilder has no reason to update. Future's only go from un-completed to completed once, so once the Future in the FutureBuilder has been completed, there's no reason the widget will update again.
You may want to think about redesigning this widget a bit, perhaps rather than relying on FutureBuilder, instead call setState to react to the completion of the Futures (which can be done repeatedly, as opposed to how FutureBuilder works)

How can I get a transformed Stream<List<Object>> to display data with asynchronous code?

I am having trouble getting a stream with a list of objects to populate in a ViewModel.
Load an asynchronous Stream<List<Habit>> from a Firestore Service
in a DailyViewModel.
Call a transform method to turn that stream into a Stream<List<HabitCompletionViewModel>> that looks at a specific
instance of each of my habits for today, and creates that instance if one doesn't exist. There are a few component pieces to this:
For each habit in the initial stream, run a private method that checks if there is an instance of the habit for today, and initializes one if not. This is an asynchronous method because it calls back to the database to update the habit with the new instance.
Then find the instance for today, and return a HabitCompletionViewModel with the habit and today's instance.
Map these to a list.
That new stream is set in a property in the DailyViewModel as todaysHabits.
todaysHabits is called as the stream in a StreamBuilder in the DailyView widget.
The issue I am running into is that I know a completion for today is being found.
I am very fuzzy on what can/should be called as asynchronous code, and whether I'm using correct async/async* return/yield statements in my code, especially since I am trying to kick this process off as part of my constructor function for the DailyViewModel. I've used a bunch of print comments and it seems like everything is running, but the todaysHabits in my ViewModel is always set to null, and the StreamBuilder doesn't return any items.
Is there anything off in my code that could be causing this?
The DailyViewModel has this todaysHabits property, which is loaded near the bottom of the constructor function:
late Stream<List<HabitCompletionViewModel>> todaysHabits;
DailyViewModel({required WeekDates week}) {
_log.v('initializing the daily viewmodel');
_week = week;
_habitService
.loadActiveHabitsByUser(_loginAndUserService.loggedInUser!.id!)
.then(
(activeUserHabits) {
todaysHabits = _getTodaysHabits(activeUserHabits);
_log.v('todaysHabits has a length of ${todaysHabits.length}');
},
);
setBusy(false);
}
That constructor calls this _getTodaysHabits function which is supposed to convert that Stream<List<Habit>> into a Stream<List<HabitCompletionViewModel>>:
Stream<List<HabitCompletionViewModel>> _getTodaysHabits(
Stream<List<Habit>> habitsStream) {
return habitsStream.asyncMap((habitsList) {
return Stream.fromIterable(habitsList).asyncMap(
(habit) async {
await _updateHabitWithNewCompletions(habit);
HabitCompletion completion = habit.completions!.firstWhere(
(completion) => completion.date
.dayEqualityCheck(DateTime.now().startOfDate()));
return HabitCompletionViewModel(completion: completion, habit: habit);
},
).toList();
});
}
And my view (which is used the Stacked package to display the contents of the ViewModel and update accordingly) contains this StreamBuilder that should be returning a list of tiles for each HabitCompletionViewModel:
StreamBuilder<List<HabitCompletionViewModel>>(
stream: vm.todaysHabits,
builder: ((context, snapshot) {
if (snapshot.hasData == false) {
return Center(child: Text('No Habits Found'));
} else {
return Column(children: [
ListView.builder(
itemCount: snapshot.data!.length,
itemBuilder: (context, i) => HabitCompletionTile(
key: ValueKey(snapshot.data![i].habit.id),
vm: snapshot.data![i],
),
),
]);
}
})),
Based on pskink's comment, I made the following updates that seem to work. (There is a slight lag when I switch to that view), but it is now showing the correct data.
Basically seems like the issue was that my previous code was returning a list of futures, instead of just a list of HabitCompletionViewModels, and using Future.wait waits for all those to complete.
Pulled out the mapping from List to List into a separate sub-method (here is the main method):
Stream<List<HabitCompletionViewModel>> _getTodaysHabits(
Stream<List<Habit>> habitsStream) {
return habitsStream.asyncMap(
(habitsList) async => await _mapHabitsToViewModel(habitsList));
}
Updated that sub-method so it first returns a List<Future>, and then uses Future.wait to wait for those to complete as HabitCompletionViewModels before returning that new list:
Future<List<HabitCompletionViewModel>> _mapHabitsToViewModel(
List<Habit> habitsList) async {
List<Future<HabitCompletionViewModel>> newList =
habitsList.map((habit) async {
HabitCompletion completion = habit.completions!.firstWhere((completion) =>
completion.date.dayEqualityCheck(DateTime.now().startOfDate()));
return HabitCompletionViewModel(completion: completion, habit: habit);
}).toList();
List<HabitCompletionViewModel> transformed = await Future.wait(newList);
return transformed;
}

send data through arguments with GetX

is it a good practice to send data with arguments in flutter using GetX (or any other way)? i mean is it good for performance and ram capcity ? ... like this example:
Get.toNamed(AppPages.servicesDetails, arguments: [service]);
when (service) contain a data for only one product came from API : like
(id, name, info, image ...etc).
and in the servicesDetails page:
final s = Get.arguments[0];
#override
Widget build(BuildContext context) {
return Scaffold(
body: Center(child: Text(s.name),),
You can also used parameters.
var data = {
"email" : "test#gmail.com",
"message" : "hi!"
};
Get.toNamed(YourRouteName.name, parameters: data);
Also from getting it from another pages is like this.
print(Get.parameters['email']);
Also on Getx you can pass it like a url link on data as the document written.
https://github.com/jonataslaw/getx/blob/master/documentation/en_US/route_management.md
If you want to pass whole item data,
You can also pass a model from list if have onTap function though you need to decode it again
e.g
MyCardItemFromList(
name: list[index].name,
ontapFunction: () => Get.toNamed(
YourRouuteName.name,
parameters: {
/// Lets assume this is the item selected also it's own item data
"itembyIndex": jsonEncode(list[index]),
}
),
),
from controller
class MyXampleController extends GetxController{
//declare the model
final Rx<Model> model = Model().obs;
#override
void onInit() {
convertToDecode();
super.onInit();
}
convertToDecode(){
final decode = jsonDecode(Get.parameters['itembyIndex'])
final passToModel = Model.fromJson(decode);
model(passToModel);
// or if you are using getBuilder
// try do it like this
// model.value = passToModel;
// update();
// don't forget to call update() since it's needed from getbuilder;
}
}
Now, For calling the data from ui will be like this
final controller = Get.put(MyXampleController());
///// this will be the data we got from the item
controller.model.value.name
You can simply use arguments
Get.toNamed(
'/my-route',
arguments: "Hello",
);
on the second screen, you can do
final title = Get.arguments as String;
There is another way of accessing values on another page
By accessing the Controller
ControllerName obj = Get.find();
obj.function();
obj.variable;
obj.anythingYouWantToAccess;
& use it like it's on the current Controller
NOTE: The Controller you want to access should b open in simple words it should be the previous screen.

Flutter HTTP Request Called Multiple Times On App Load [duplicate]

This question already has an answer here:
Flutter: Why setState(( ) { }) set data again and again
(1 answer)
Closed 2 years ago.
I have a flutter project that makes an http request to gather json of inventory items then render it on screen in a list view. When a user scrolls to the bottom it then loads more inventory by triggering another http call. However my initial HTTP call is being called multiple times. And I am not sure how to handle that.
The result I get is my finish print statement just triggers continuously. At first I thought I had it working because it does load the inventory to my list view. But then I noticed It just keeps loading the same data into my list view non stop. When I added that finish print statement it became clear that it is actually continuously making that http call non stop.
Id like to have it only make the call once, then make a new call again when the user scrolls to bottom.
Any tips will help thank you.
Here is my code.
Network Code
Future <List <dynamic>> fetchInventory() async{
dynamic response = await http.get('https:somelink.com',
headers: {'x-api-key': 'mykey'},);
var inventory = List<dynamic>();
//if 200 response is given then set inventory var to inventoryJson value from the http call
if (response.statusCode == 200){
dynamic inventoryJson = jsonDecode(response.body);
inventory = inventoryJson['page'];
print('finish');
}
//Inventory is returned
return inventory;
}
}
Here is my how I am using that code in my main file
class _WelcomeScreenState extends State<WelcomeScreen> {
//Create Network Helper Obj
NetworkHelper networkHelper = NetworkHelper();
//invenrtory List is set to empty
var inventory = [];
var _controller = ScrollController();
#override
void initState() {
super.initState();
// set up listener here
_controller.addListener(() {
if (_controller.position.pixels == _controller.position.maxScrollExtent) {
// Perform your task
//This method fetches the data from the fetchInventory method
print('at bottom');
networkHelper.fetchInventory().then((value) {
//This set state will set the value returned from fetchInventory method to the local inventory List
setState(() {
this.inventory = value;
});
});
}
});
}
#override
Widget build(BuildContext context) {
//Used to format currency shown in view
var currencyFormat = NumberFormat('##,###.00', 'en_US');
//This method fetches the data from the fetchInventory method
networkHelper.fetchInventory().then((value){
//This set state will set the value returned from fetchInventory method to the local inventory List
setState(() {
this.inventory.addAll(value);
});
});
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
elevation: 0.0,
title: AppBarTitle(),
),
body: Padding(
padding: const EdgeInsets.only(left: 5.0, right: 5.0, bottom: 25.0),
child: ListView.builder(
controller: _controller,
itemCount: inventory.length,
itemBuilder: (context, index){
enter code here
enter code here
The responsibility of the build method is to construct a widget tree, and this method may be called repeatedly by the framework whenever it thinks that the screen might have changed and need to be re-rendered. As a result, its important that as little work as possible is done here. Remote calls over the network should be avoided, and implementers should try not to call setState as this will cause the UI rendering to loop.
Listening to the scroll controller as demonstrated is a good way of 'Loading more', what is missing is an initial load of the inventory in the initState() method.
So, you should move the call to networkHelper.fetchInventory() from the build method to the scrollcontroller callback (as it adds to the inventory) and move the existing call to networkHelper.fetchInventory() that initialises the inventory from the scrollcontroller callback to the start of the initState method.

Flutter Function returns empty list using Firebase

I'm using Flutter web and firebase realtime database. In my databse I have some questions, structured as maps, and I'm trying to display them in my app.
For each question, I create a widget, I add it inside a list and then I append the list to a Column() widget.
The Column() widget is the following (view_question.dart file):
Column(
children: Question().view(),
),
As you can see, I use the method view() from a class I created named Question().
The view() method returns a list of widgets.
Before I show you the .view() method, I need to mention that outside of the Question class I have initialized the following:
List<Widget> widgets = List();
Below you can see the .view() method from the Question class (Question.dart file):
List<Widget> view() {
Database db = database();
DatabaseReference ref = db.ref('questions/');
ref.orderByKey().once("value").then((e) {
DataSnapshot datasnapshot = e.snapshot;
datasnapshot.val().forEach((key, values) {
widgets.add(QuestionWidget(values));
print(widgets);
});
});
print(widgets);
return widgets;
I'm getting my questions' data from my database, then for each question I'm creating a QuestionWidget() widget, where I pass inside it the respective data and I add it to the widgets list. Afterwards, I print() the widgets list, so I can monitor the whole process.
As you also can see, I've added a print(widgets) just before the return statement.
This is the prints' output in my console
From that, what I understand is that the function returns an empty list and then I retrieve my data from the database and add them to the list.
So I'm asking, how can I add my data to the list, and when this process has finished, return the list and append it to the Column() widget? Should I add a delay to the return statement? What am I missing?
Thank you for your time
Data is loaded from Firebase asynchronously. By the time your return widgets runs, the widgets.add(QuestionWidget(values)) hasn't been called yet. If you check the debug output of your app, the print statements you have should show that.
For this reason you should set the widgets in the state once they're loaded. That will then trigger a repaint of the UI, which can pick them up from the state. Alternatively you can use a FutureBuilder to handle this process.
See:
How can I put retrieved data from firebase to a widget in Flutter?
Slow data loading from firebase causing "RangeError (index)"
// return type is like this
Future<List<Widget>> view() {
Database db = database();
DatabaseReference ref = db.ref('questions/');
// return this
return ref.orderByKey().once("value").then((e) {
DataSnapshot datasnapshot = e.snapshot;
datasnapshot.val().forEach((key, values) {
widgets.add(QuestionWidget(values));
});
// return widgets after added all values
return widgets;
});