Dynamically added widget state is null in Flutter - 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());

Related

Open dialog box in Flutter Stateless widget using ChangeNotifierProvider

I have a Stateless flutter widget which shows a list of TODOs. The list is shown using a ChangeNotifierProvider (of TodoList object, which contains a list of todos). Now when I load the page, I want to show a dialog box asking user to enter a new TODO if and only if the existing todos is empty. Inside the builder of the ChangeNotifierProvider, i tried below logic
if (todoList.todos.length == 0) {
_showDialog(context);
return Column...;
} else {
return ListBuilder...;
}
But its showing 2 dialog box (probably due to the build method executing twice). I have to pass context to dialog box because I'm updating the todoList inside it, which should trigger a rebuild.
How do I handle this scenario. I've tried using flag (_isDialogOpen) but its not still working?
make the widget Stateful in order to use it's lifecycle methods, you can use then initState() for showing the dialog when the page widgets are inserted in the widget tree, but you will need to use an addPostFrameCallback() to schedule showing it 1 frame after the initState's code gets executed:
First, import:
import package:flutter/scheduler.dart.
Then use this:
#override
void initState() {
// ...
SchedulerBinding.addPostFrameCallback((_) => _showDialog(context),);
}

Detect user created widgets in flutter widget tree

I've been working on a problem today to detect certain widgets in the widget tree so I've been playing around with context.visitChildElements and element. visitChildren. I can see all the widgets in the tree, but there's just too many.
I looked at the way the flutter widget inspector does it and they have some internal expectations that won't exist within other users code bases. The example, I have a scaffold with a body Center and a child Material button. Passing the context to my function below prints out about 200+ widgets with those three scattered in between. I would like to only print out those three, or at least elliminate all widgets created by Flutter automatically and not created by the code the user supplied.
List<WidgetInfo> getElements(BuildContext context) {
var widgetsOfInterest = <WidgetInfo>[];
widgetsOfInterest.clear();
int indentation = 0;
void visitor(Element element) {
indentation++;
Key? key = element.widget.key;
String className = element.widget.runtimeType.toString();
while (element.findRenderObject() is! RenderBox) {}
RenderBox box = element.findRenderObject() as RenderBox;
var offset = box.getTransformTo(null).getTranslation();
final indent = ' ' * indentation;
// Here I want to check if this is a widget we created and print its name and offset
if (debugIsLocalCreationLocation(element)) print('$className $offset');
if ((MaterialButton).toString() == className) {
widgetsOfInterest.add(WidgetInfo(
indentation: indentation,
size: box.size,
paintBounds: box.paintBounds.shift(
Offset(offset.x, offset.y),
),
key: key,
className: className,
));
}
element.visitChildren(visitor);
}
context.visitChildElements(visitor);
return widgetsOfInterest;
}
If anyone have any insights or experience with the Flutter widget tree that could point me in the right direction I would appreciate that.
it's obviously seems not the best solution here(and will increase unnecessary code) but this might work.
you can create a custom widget key that have some prefix inside of it and use it in every component you want it to be detected
for example
//1
Appbar(key: FSKey())
//2
Center(key:FSKey("awesome_widget"))
internally if you have access to those key while you iterate through elements you can detect those widgets using the prefix you set.
actuall key values
//1
"fskey_1273zj72ek628"
//2
"fskey_awesome_widget"
again this might not be a very optimal solution but iit gives you some control of what parts of the tree you want it to be detected and eventually if there is no other way.. this will work.

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

changing variables inside listview builder

i want to change variable values when the widget is loading the data from the web,
just want to do something like this:
_playid = _notes[1].id;
title = _notes[1].title;
but wherever I put it, i get an error,
I tried to put it in a set state inside listview builder, but no luck since I don't want it with onPress or on tap methods
could someone help, please?
i just found out,
simply we have to use setstate in fetch inside initstate
void initState() {
title = " ";
fetchNotes().then((value) {
setState(() {
_notes.addAll(value);
_playid = _notes[0].numb;
});
});
}

Understanding Flutter Render Engine

The docs here about how to update a ListView say:
In Flutter, if you were to update the list of widgets inside a
setState(), you would quickly see that your data did not change
visually. This is because when setState() is called, the Flutter
rendering engine looks at the widget tree to see if anything has
changed. When it gets to your ListView, it performs a == check, and
determines that the two ListViews are the same. Nothing has changed,
so no update is required.
For a simple way to update your ListView, create a new List inside of
setState(), and copy the data from the old list to the new list.
I don't get how the Render Engine determines if there are any changes in the Widget Tree in this case.
AFAICS, we care calling setState, which marks the State object as dirty and asks it to rebuild. Once it rebuilds there will be a new ListView, won't it? So how come the == check says it's the same object?
Also, the new List will be internal to the State object, does the Flutter engine compare all the objects inside the State object? I thought it only compared the Widget tree.
So, basically I don't understand how the Render Engine decides what it's going to update and what's going to ignore, since I can't see how creating a new List sends any information to the Render Engine, as the docs says the Render Engine just looks for a new ListView... And AFAIK a new List won't create a new ListView.
Flutter isn't made only of Widgets.
When you call setState, you mark the Widget as dirty. But this Widget isn't actually what you render on the screen.
Widgets exist to create/mutate RenderObjects; it's these RenderObjects that draw your content on the screen.
The link between RenderObjects and Widgets is done using a new kind of Widget: RenderObjectWidget (such as LeafRenderObjectWidget)
Most widgets provided by Flutter are to some extent a RenderObjectWidget, including ListView.
A typical RenderObjectWidget example would be this:
class MyWidget extends LeafRenderObjectWidget {
final String title;
MyWidget(this.title);
#override
MyRenderObject createRenderObject(BuildContext context) {
return new MyRenderObject()
..title = title;
}
#override
void updateRenderObject(BuildContext context, MyRenderObject renderObject) {
renderObject
..title = title;
}
}
This example uses a widget to create/update a RenderObject. It's not enough to notify the framework that there's something to repaint though.
To make a RenderObject repaint, one must call markNeedsPaint or markNeedsLayout on the desired renderObject.
This is usually done by the RenderObject itself using custom field setter this way:
class MyRenderObject extends RenderBox {
String _title;
String get title => _title;
set title(String value) {
if (value != _title) {
markNeedsLayout();
_title = value;
}
}
}
Notice the if (value != previous).
This check ensures that when a widget rebuilds without changing anything, Flutter doesn't relayout/repaint anything.
It's due to this exact condition that mutating List or Map doesn't make ListView rerender. It basically has the following:
List<Widget> _children;
List<Widget> get children => _children;
set children(List<Widget> value) {
if (value != _children) {
markNeedsLayout();
_children = value;
}
}
But it implies that if you mutate the list instead of creating a new one, the RenderObject will not be marked as needing a relayout/repaint. Therefore there won't be any visual update.