Flutter Function returns empty list using Firebase - flutter

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

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)

Fetch & show item details

I'm currently learning Flutter and developing a Shopping List.
In the backend, there's a Laravel instance providing the following Endpoints:
/lists -> A list of all shopping lists
/lists/{listId}/items -> All items in the list
For the Flutter side, I've created a provider which fetches all lists from the server.
Future<void> init() async {
this.lists = await this.service.getLists();
notifyListeners();
}
This data (<List<ListModel>>) is retreived by my widget and displayed through ListView.builder. The RefreshIndicator just refreshes the provider data by run the init method again.
Widget build(BuildContext context) {
final provider = Provider.of<ShoppingListProvider>(context);
List<ListModel> lists = provider.lists;
...
child: RefreshIndicator(
onRefresh: () => provider.init(),
Now, if I click on the ListTile, I'd like to display all items. They should be fetched from the API as well, and I'd like to refresh the item list too. Unfortunately, I don't manage to get this data provided through the provider, as it requires the "listId".
Maybe someone can give me a hint on this?
Fetched from inside the widget, which works, but then I can't refresh the data. It somehow should be provided through the provider

How to combine features of FutureProvider/FutureBuilder (waiting for async data) and ChangeNotifierProvider (change data and provide on request)

What I wanted to achieve, was to retrieve a document from firestore, and provide it down the widget tree, and be able to make changes on the data of the document itself.
show something like "No data available/selected" when there's nothing to display,
show loading-screen/widget if the document is loading,
show data in the UI when document is ready
be able to make changes to the data of the document itself (in firestore) AND reflect those changes in the UI, WITHOUT reading the document again from firestore
be able to reload the document/load another document from firestore, show a loading screen while waiting for document, then show data again
The whole purpose of this, is to avoid too many firestore read operations. I update the document data on the server (firestore), then make the same updates in the frontend (an ugly alternative would be to retrieve the document from firestore each time I make a change).
If the changes are too complex though, or if it is an operation that is rarely executed, it might just be a better idea to read the whole document again.
"Answer your own question – share your knowledge, Q&A-style" -> see my solution to this problem in my answer below
The above described functionalities are NOT POSSIBLE with:
FutureProvider: you can use this to retrieve a document from firestore and show a loading-screen while waiting, but can't make changes to it afterwards, that will also show in the UI
FutureBuilder: same as above, even worse, this can't provide data down the widget-tree
It was however possible with ChangeNotifierProvider (& ChangeNotifier), and this is how I did it:
enum DocumentDataStatus { noData, loadingData, dataAvailable }
...
class QuestionModel extends ChangeNotifier {
DocumentSnapshot _questionDoc; //the document object, as retrieved from firestore
dynamic _data; //the data of the document, think of it as Map<String, dynamic>
DocumentDataStatus _documentDataStatus;
DocumentDataStatus get status => _documentDataStatus;
//constructors
QuestionModel.example1NoInitialData() {
_documentDataStatus = DocumentDataStatus.noData; //no question selected at first
}
QuestionModel.example2WithInitialData() {
_documentDataStatus = DocumentDataStatus.loadingData; //waiting for default document or something...
//can't use async/await syntax in a dart constructor
FirebaseFirestore.instance.collection('quiz').doc('defaultQuestion').get().then((doc) {
_questionDoc = doc;
_data = _questionDoc.data();
_documentDataStatus = DocumentDataStatus.dataAvailable;
notifyListeners(); //now UI will show document data
});
}
//all kinds of getters for specific data of the firestore document
dynamic get questionText => _data['question'];
dynamic get answerText => _data['answer'];
// dynamic get ...
///if operation too complex to update in frontend (or if lazy dev), just reload question-document
Future<void> loadQuestionFromFirestore(String questionID) async {
_data = null;
_documentDataStatus = DocumentDataStatus.loadingData;
notifyListeners(); //now UI will show loading-screen while waiting for document
_questionDoc = await FirebaseFirestore.instance.collection('quiz').doc(questionID).get();
_data = _questionDoc.data();
_documentDataStatus = DocumentDataStatus.dataAvailable;
notifyListeners(); //now UI will show document data
}
///instantly update data in the UI
void updateQuestionTextInUI(String newText) {
_data['question'] = newText; //to show new data in UI (this line does nothing on the backend)
notifyListeners(); //UI will instantly update with new data
}
}
...
Widget build(BuildContext context) {
QuestionModel questionModel = context.watch<QuestionModel>();
return [
Text('No question to show'),
Loading(),
Text(questionModel.questionText),
][questionModel.status.index]; //display ONE widget of the list depending on the value of the enum
}
The code in my example is simplified for explanation. I have implemented it in a larger scale, and everything works just as expected.

Dynamically added widget state is null in Flutter

I am developing a feature where users can press a button to add a new group of text fields to the screen. Each group of text fields is stored in its own stateful widget. The abridged code to add the new widget is shown below:
List<EducationField> fieldList = [];
List<GlobalKey<EducationFieldState>> keyList = [];
// Function that adds new widgets to the list view
onTap: () {
GlobalKey<EducationFieldState> key = new GlobalKey<EducationFieldState>();
setState(() {
fieldList.add(EducationField(key: key));
keyList.add(key);
});
},
I can dynamically add the new widgets just fine. However when I try to access the state of the widgets, I get an error saying that the state of the respective widget is null. There is a function in each widget state that gets the values from their text fields. The code I'm using to do that is also shown below:
void _getUserData(){
List<EducationModel> modelList = [];
for(int i = 0; i < fieldList.length; i++){
modelList.add(keyList[i].currentState!.getData()); // this line is causing the error
modelList.last.printModel();
}
}
I have done a lot of research on this issue and still have no idea why I am getting a null error. Is my approach wrong or is it something more minor? I can also give more code if necessary.
Thanks in advance!
Checkout GlobalKey docs.
Your dynamically added Text widget doesn't exist in the Widget tree yet, as you just created it and you're trying to add it to the Widget tree. So it doesn't have a current state.
Maybe this helps?
keyList[i].currentState?.getData() ?? '' // I'm guessing getData return a String
Something else you could try:
// only call _getUserData() after widget finished building
WidgetsBinding.instance!.addPostFramecallback(() => _getUserData());

I have a stored List data and how can I localize it in flutter?

I can translate the text inside the widget using AppLocalizations.of(context)!.translated and it work fine
However, when I want to localize a stored List<> data which will be used in listView Builder, I am not able to do so, since it need (context).
My question is how to localize a stored list data(outside widget) and pass to list view builder?
Thanks for your help.
I think you should do 2 things
In case if you have a list in another file, then it should be static like this:
class Presets {
static var presetData = [];
}
after you loaded up your list, you can do these steps:
First,
Create Widgets as children like this:
List<Widget> presetsLoadUp() {
List<Widget> children = <Widget>[];
for(i < yourList.length){
children.add(Text(yourList[i]));
}
return children;
}
You can create your own unique widget inside the children.add method.
Secondly,
Create the list:
...
child: ListView(
children: presetsLoadUp(),
),
If anyone has similar problem, please take it for reference.
I just build another widget to solve this.
What I do is :
build another widget,
then put the list data inside and
return ListViewBuilder.
Everything is same,
just put your list data inside another widget can solve the problem