Detect user created widgets in flutter widget tree - flutter

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.

Related

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

Flutter paint(): is this legit? Rendering Renderobject in different context

I try to render the child of a listitem (somewhere up) to a different place (widget) in the tree;
In the approach below, BlendMask is the "target" widget that checks for and paints "source" widgets that got themself an GlobalKey which is stored in blendKeys.
This works somewhat. And I'm not quite sure if I might fight the framework, or just missing some points...
The problems are two:
The minor one: This approach doesn't play nice with the debugger. It compiles and runs fine but every hot-reload (on save f.e.) throws an "can't findRenderObject() of inactive element". Maybe I miss some debug flag?
the real problem, that brought me here questioning the idea en gros: As mentioned, the Source-Widget is somewhere in the subtree of the child of a Scrollable (from a ListView.build f.e.): How can I update the Òffset for the srcChild.paint() when the list is scrolled? - without accessing the lists scrolController?! I tried listening via WidgetsBindingObservers didChangeMetrics on the state of the Source widget, but as feared no update on scroll. Maybe a strategically set RepaintBounderyis all it needs? *hope* :D
Anyway, every tip much appreciated. Btw the is an extend of this question which itself extends this...
class BlendMask extends SingleChildRenderObjectWidget {
[...]
#override
RenderObject createRenderObject(context) {
return RenderBlendMask();
}
}
class RenderBlendMask extends RenderProxyBox {
[...]
#override
void paint(PaintingContext context, offset) { <-- the target where we want to render a widget
[...] from somewhere else in the tree!
for (GlobalKey key in blendKeys) {
if (key.currentContext != null) {
RenderObject? srcChild <-- the source we want to render in this sibling widget!
= key.currentContext!.findRenderObject();
if (srcChild != null) {
Matrix4 mOffset = srcChild.getTransformTo(null);
context.pushTransform(true, offset, mOffset, (context, offset) {
srcChild.paint(context, offset);
});
}
}
}
}
} //RenderBlendMask

Is there a way to get rid of the spacing between expansion panels when expanded in an expansion panel list in Flutter?

I am running into a problem where I am wanting to remove the space between expansion panels of an expansion panel list when the panels are expanded.
Images of unwanted behavior, these images are taken from flutter documentation:
List when not expanded, which is fine:
List when expanded:
- You can see the gap between the sections. This is what I do not want for my app.
Any tips are appreciated.
Try Modifying The Actual Implementations
If you have some prior programming experience, this should make sense...
Find these files:
expansion_panel.dart
expansion_title.dart
expand_icon.dart
mergeable_material.dart
These files should be located in the External Library > Dart Packages > Flutter > src > material. Note that the material folder may be expressed as "src.material".
Android Studio should allow you to hover over the ExpansionPanel Widget and right-click it. After right-clicking it you should see a list of shortcuts, one being "Go to". Click on the "Go to" option which should bring up more options, one of which being "Implementation(s)". This should bring you to where you need. According to my version, double clicking the widget name so the entire widget's name is highlighted should allow for a keyboard shortcut Ctrl+Alt+B.
The key file you must go into is "mergeable_material.dart". In "mergeable.dart", go to the MaterialGap constructor:
const MaterialGap({
#required LocalKey key,
this.size = 16.0,
//This is the size of the gap between ExpansionTiles WHEN EXPANDED
}) : assert(key != null),
super(key);
This size variable controls the gap between the Expansion Tiles ONLY when EXPANDED. Setting "this.size = 0.0" should remove the gap.
If you want to know why this is, long story short, when the ExpansionPanels list of widgets property is being defined (which is in expansion_panel.dart) a "MaterialGap" is being added between the children. We modified the material gap to be 0.0 thus effectively removing the gap.
Other things you can do:
Change the Trailing Icon in the Header.
Go to expand.dart file. Go to the bottom of the file where the build method should be. In the return statement, replace the icon button with 'whatever' you want (within reason).
#override
Widget build(BuildContext context) {
assert(debugCheckHasMaterial(context));
assert(debugCheckHasMaterialLocalizations(context));
final MaterialLocalizations localizations = MaterialLocalizations.of(context);
final String onTapHint = widget.isExpanded ? localizations.expandedIconTapHint :
localizations.collapsedIconTapHint;
return Semantics(
onTapHint: widget.onPressed == null ? null : onTapHint,
child: Container() //Replaced the Icon Button here to remove it
);
}
Keep in mind that without a button, you must make canTapOnHeader: true, or you won't
be able to expand the panel. You can make sure you don't forget to do this by going
to the ExpansionPanel constructor and changing "this.canTapOnHeader = false;" to
"this.canTapOnHeader = true;
ExpansionPanel({
#required this.headerBuilder,
#required this.body,
this.isExpanded = false,
this.canTapOnHeader = true, //Changed this to true from false
}) : assert(headerBuilder != null),
assert(body != null),
assert(isExpanded != null),
assert(canTapOnHeader != null);
Remove the shadow and/or dividers
Go to the bottom of the expansion_panel.dart file. This is where the build method should be. At the bottom of the build method should be the return statement, which returns MergeableMaterial. You can do the following:
return MergeableMaterial(
hasDividers: false, //Change the boolean value of this
children: items,
elevation: 0,
//Add this line to remove shadow from elevation, REQUIRES ANOTHER STEP
);
If add the "elevation: 0" property you MUST go back to mergeable_material.dart and add the following line of code:
void _paintShadows(Canvas canvas, Rect rect) {
if (boxShadows == null) return; //ADD THIS LINE OF CODE
for (BoxShadow boxShadow in boxShadows) {
final Paint paint = boxShadow.toPaint();
// TODO(dragostis): Right now, we are only interpolating the border radii
// of the visible Material slices, not the shadows; they are not getting
// interpolated and always have the same rounded radii. Once shadow
// performance is better, shadows should be redrawn every single time the
// slices' radii get interpolated and use those radii not the defaults.
canvas.drawRRect(kMaterialEdges[MaterialType.card].toRRect(rect), paint);
}
}
I think you get the idea, You can do more to remove padding and whatnot by messing around with constants.
Concerns
Note: I'm relatively new to flutter, (2 months last summer and 2 months now- I'm a college student) in fact. I don't know how modifying these files may impact other widgets but I haven't noticed any issues by doing this.
This is also my first post so take it with a grain of salt. Modifying Material implementations may break some rules or conventions that I would refer to the documentation for.
Instead of changing the source code, you're better off making a copy of the expansion_panel.dart and using this. For the space between the items to disappear, you must comment out on lines 486 and 487.
if (_isChildExpanded(index) && index != 0 && !_isChildExpanded(index - 1))
items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 - 1)));
And on lines 558 and 559.
if (_isChildExpanded(index) && index != widget.children.length - 1)
items.add(MaterialGap(key: _SaltedKey<BuildContext, int>(context, index * 2 + 1)));
Another issue with this component which you might want to fix, is with the canTapOnHeader property. Setting it to true allows you to tap the card and expand, but you're stuck with a bunch of dead space on the right side of your card. To fix this, add a check to only show expandIconContainer (line 526) as follows:
if (!child.canTapOnHeader) expandIconContainer,
The ExpansionPanelList has an elevation property, which is causing the panels to appear separated. If you don't set the elevation, it is automatically set to a value of 2. To remove the space between expanded panels, you can set the elevation to 0.
However, when you do this, you may run into other graphical issues. For example, the divider doesn't appear for an expanded panel. Without the space between panels, it makes it had to see where one panel starts and the next ends. Not a tough fix to custom code your own dividers in, but thought it was worth mentioning.

Flutter how to get a parent widget of unknown type

I'm trying to improve error messages from a widget I'm writing.
if an error occurs I want to be able to dump out the widget tree to show the user the ancestors of my widget.
To do this I need to display the widget tree from my widget back up the tree towards the root.
The only way I've see to obtain a parent widget is via the call to:
findAncestorWidgetOfExactType
Of course the problem here is that I don't know the parent's type so that method won't work.
Are there any methods that will allow me to render the tree from my widget back up towards the root of the tree?
The element tree has a method that allows this but I don't know how to access the generated element:
List<Element> debugGetDiagnosticChain() {
final List<Element> chain = <Element>[this];
Element node = _parent;
while (node != null) {
chain.add(node);
node = node._parent;
}
return chain;
}

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.