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
Related
I have a non-interactive (i.e. no GestureDetector, etc.) widget subtree that I would like to be drawn identically > 1 time.
Currently I am achieving this by placing the widget subtree into the tree multiple times. There are some downsides to this:
Each StatefulWidget in the subtree is given its own unique State instance, whereas a single one would suffice and be preferable.
Performance: identical redundant layout and painting is performed for each subtree.
If possible, I would like to manually render (layout and paint) the subtree just once, and then draw the resulting rendering as needed.
I am able to draw to a separate canvas with PictureRecorder and paint the resulting picture as needed using Canvas.drawPicture().
What I am missing is how to perform layout and painting of widget trees in Flutter. I've not been successful in finding any information about this, perhaps because I'm unaware of the best search term to use.
What I'm looking for is high-level pointers about how to approach manual rendering of widget subtrees, including any links to relevant documentation, articles, example code that does something similar, or even just search terms that will lead me in the right direction. Legitimate, reasoned criticisms of this approach are also welcome from those with first-hand experience attempting something similar. :)
Thank you!
Although not directly answering the question of "how to manually layout and paint a widget subtree," an approach which achieves a similar net effect (disregarding performance differences) toward the stated goal of "a non-interactive widget subtree to be drawn identically > 1 time" is to paint the subtree multiple times:
/// Caveat: if the transform is accessed (either directly, or
/// indirectly via [RenderBox.globalToLocal], etc.) outside of
/// painting, the transform will be the one that would apply for
/// an offset of [Offset.zero].
class MultiOffsetPainter extends SingleChildRenderObjectWidget {
final List<Offset> offsets;
const MultiOffsetPainter({super.key, super.child, required this.offsets});
#override
RenderObject createRenderObject(BuildContext context) {
return _MultiOffsetPainterRenderObject(offsets);
}
#override
void updateRenderObject(BuildContext context, _MultiOffsetPainterRenderObject renderObject) { // ignore: library_private_types_in_public_api
renderObject.offsets = offsets;
}
}
class _MultiOffsetPainterRenderObject extends RenderProxyBox {
List<Offset> offsets;
Offset? _currentTranslation;
_MultiOffsetPainterRenderObject(this.offsets);
#override
void applyPaintTransform(RenderObject child, Matrix4 transform) {
if (_currentTranslation != null) {
transform.translate(_currentTranslation!.dx, _currentTranslation!.dy, 0);
}
super.applyPaintTransform(child, transform);
}
#override
void paint(PaintingContext context, Offset offset) {
for (var translation in offsets) {
_currentTranslation = translation;
super.paint(context, offset + translation);
_currentTranslation = null;
}
}
}
Note the important caveat.
Cataloging here on the chance this might be useful to a future reader.
In developing some of the screens for my flutter app, I regularly need to dynamically render widgets based on the state of the screen. For circumstances where it makes sense to create a separate widget and include it, I do that.
However, there are many use cases where what I need to render is not fit for a widget, and leverages existing state from the page. Therefore I use builder methods to render the appropriate widgets to the page. As anyone who uses Flutter knows, that can lead to lengthy code where you need to scroll up/down a lot to get to what you need to work on.
For better maintainability, I would love to move those builder methods into separate files, and then just include them. This would make it much easier to work on specific code widgets rendered and make the screen widget much cleaner.
But I haven't found a proper way to extract that dynamic widget code, which makes use of state, calls to update state, etc. I'm looking for a type of "include" file that would insert code into the main screen and render as if it's part of the core code.
Is this possible? How to achieve?
With the introduction of extension members, I came across this really neat way of achieving exactly what your described!
Say you have a State class defined like this:
class MyWidgetState extends State<MyWidget> {
int cakes;
#override
void initState() {
super.initState();
cakes = 0;
}
#override
Widget build(BuildContext context) {
return Builder(
builder: (context) => Text('$cakes'),
);
}
}
As you can see, there is a local variable cakes and a builder function. The very neat way to extract this builder now is the following:
extension CakesBuilderExtension on MyWidgetState {
Widget cakesBuilder(BuildContext context) {
return Text('$cakes');
}
}
Now, the cakes member can be accessed from the extension even if the extension is placed in another file.
Now, you would update your State class like this (the builder changed):
class MyWidgetState extends State<MyWidget> {
int cakes;
#override
void initState() {
super.initState();
cakes = 0;
}
#override
Widget build(BuildContext context) {
return Builder(
builder: cakesBuilder,
);
}
}
The cakesBuilder can be referenced from MyWidgetState, even though it is only declared in the CakesBuilderExtension!
Note
The extension feature requires Dart 2.6. This is not yet available in the stable channel, but should be around the end of 2019 I guess. Thus, you need to use the dev or master channels: flutter channel dev or flutter channel master and update the environment constraint in your pubspec.yaml file:
environment:
sdk: '>=2.6.0-dev.8.2 <3.0.0'
I'm new to dart/flutter and having a little trouble getting my head around communication patterns.
One reoccurring problem is that I keep looking to expose a public method on a widget so it can be called by other widgets.
The problem is with stateful widgets. In these cases, I need to call down to the widgets state to do the actual work.
The problem is that the widget doesn't have a copy of the state.
I have been saving a copy of the state in the widget but of course this throws a warning as it makes the widget mutable.
Let me give a specific example:
I have a specialised menu which can have a set of menu items.
Each are stateful.
When the menu is closing it needs to iterate over the list of menu items that it owns and tell each one to hide (the menu items are not visually contained within the menu so hiding the menu doesn't work).
So the menu has the following code:
class Menu{
closeMenu() {
for (var menuItem in menuItems) {
menuItem.close();
}
}
So that works fine, but of course in the MenuItem class I need to:
class MenuItem {
MenuItemState state;
close()
{
state.close();
}
But of course having the state object stored In the MenuItem is a problem given that MenuItem is meant to be immutable. (It is only a warning so the code works, but its clearly not the intended design pattern).
I could do with seeing more of your code to get a better idea of how to solve your specific issue but it appears that the Flutter documentation will help you in some regard, specifically the section on Lifting state up:
In Flutter, it makes sense to keep the state above the widgets that use it.
Why? In declarative frameworks like Flutter, if you want to change the UI, you have to rebuild it.
…it’s hard to imperatively change a widget from outside, by calling a method on it. And even if you could make this work, you would be fighting the framework instead of letting it help you.
It appears you're trying to fight the framework in your example and that you were correct to be apprehensive about adding public methods to your Widgets. What you need to do is something closer to what's detailed in the documentation (which details all of the new classes etc you'll see below). I've put a quick example together based on this and the use of Provider which makes this approach to state management easy. Here's a Google I/O talk from this year encouraging its use.
void main() {
runApp(
ChangeNotifierProvider(
builder: (context) => MenuModel(),
child: MyApp(),
),
);
}
class MyApp extends StatelessWidget {
…
// call this when the menu is closed
void onMyMenuClosed(BuildContext context) {
var menuModel = getMyMenuModel(context);
menuModel.hideMenuItems();
}
}
class MenuModel extends ChangeNotifier {
bool _displayItems = false;
void hideMenuItems() {
_displayItems = false;
notifyListeners();
}
void showMenuItems() {
_displayItems = true;
notifyListeners();
}
}
Calling hideMenuItems() makes a call to notifyListeners() that'll do just that; notify any listeners of a change which in turn prompts a rebuild of the Widget/s you wrap in a Consumer<MenuModel> Now, when the Widget that displays the menu is rebuilt, it just grabs the appropriate detail from the MenuModel class - the one source of truth for the state. This reduces the number of code paths you'd otherwise have to deal with to one and makes it far easier to see what's happening when you make further changes.
#override
Widget build(BuildContext context) {
return Consumer<MenuModel>(
builder: (context, menuModel, child) {
return menuModel._displayItems() ? MenuItemsWidget() : Container();
},
);
}
I recommend you read the entire page on state management.
I have a screen, which contains a Form with a StreamBuilder. when I load initial data from StreamBuilder, TextFormField show data as expected.
When I tap inside the TextFormField, the software keyboard shows up, which causes the widgets to rebuild. The same happens again when the keyboard goes down again.
Unfortunately, the StreamBuilder is subscribed again and the text box values is replaced with the initial value.
Here is my code:
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: _bloc.inputObservable(),
builder: (context, snapshot) {
if (snapshot.hasData) {
return TextFormField(
// ...
);
}
return const Center(
child: CircularProgressIndicator(),
);
},
);
}
How do I solve this?
Keyboard causing rebuilds
It makes total sense and is expected that the software keyboard opening causes rebuilds. Behind the scenes, the MediaQuery is updated with view insets. These MediaQueryData.viewInsets make sure that your UI knows about the keyboard obscuring it. Abstractly, the keyboard obscuring a screen causes a change to the window and most of the time to your UI, which requires changes to the UI - a rebuild.
I can make the confident guess that you are using a Scaffold in your Flutter application. Like many other framework widgets, the Scaffold widgets depends (see InheritedWidget) on the MediaQuery (that gets its data from the Window containing your app) using MediaQuery.of(context).
See MediaQueryData for more information.
It all boils down to the Scaffold having a dependency on the view insets. This allows it to resize when these view insets change. Basically, when the keyboard is opened, the view insets update, which allows the scaffold to shrink at the bottom, removing the obscured space.
Long story short, the scaffold adapting to the adjusted view insets requires the scaffold UI to rebuild. And since your widgets are necessarily children of the scaffold (likely the body), your widgets are also rebuilt when that happens.
You can disable the view insets resizing behavior using Scaffold.resizeToAvoidBottomInset. However, this will not necessarily stop the rebuilds as there might still be a dependency on the MediaQuery. I will explain how you should really think about the problem in the following.
Idempotent build methods
You should always build your Flutter widgets in a way where your build methods are idempotent.
The paradigm is that a build call could happen at any point in time, up to 60 times per second (or more if on a higher refresh rate).
What I mean by idempotent build calls is that when nothing about your widget configuration (in the case of StatelessWidgets) or nothing about your state (in the case of StatefulWidgets) changes, the resulting widget tree should be strictly the same. Thus, you do not want to handle any state in build - its only responsibility should be representing the current configuration or state.
The software keyboard opening causing rebuilds is simply a good example for why this is so. Other examples are rotating the device, resizing on web, but it can really be anything as your widget tree starts to get complex (more on that below).
StreamBuilder resubscribing on rebuild
To come back to the original question: in this case, your problem is that you are approaching the StreamBuilder incorrectly. You should not feed it a stream that is recreated each build.
The way stream builders work is by subscribing to the initial stream and then resubscribing whenever the stream is updated. This means that when the stream property of the StreamBuilder widget is different between two build calls, the stream builder will unsubscribe from the first and subscribe to the second (new) stream.
You can see this in the _StreamBuilderBaseState.didUpdateWidget implementation:
if (oldWidget.stream != widget.stream) {
if (_subscription != null) {
_unsubscribe();
_summary = widget.afterDisconnected(_summary);
}
_subscribe();
}
The obvious solution here is that you will want to supply the same stream between different build calls when you do not want to resubscribe. This goes back to idempotent build calls!
A StreamController for example will always return the same stream, which means that it is safe to use stream: streamController.stream in your StreamBuilder. Basically, all controller, behavior subject, etc. implementations should behave this way - as long as you are not recreating your stream, StreamBuilder will properly take care of it!
The faulty function in your case is therefore _bloc.inputObservable(), which creates a new stream each time instead of returning the same one.
Notes
Note that I said that build calls can happen "at any point in time". In reality, you can (technically) control exactly when every build happens in your app. However, a normal app will be so complex that you cannot possibly have control over that, hence, you will want to have idempotent build calls.
The keyboard causing rebuilds is a good example for this.
If you think about it on a high level, this is exactly what you want - the framework and its widget (or widgets that you create) take care of responding to outside changes and rebuilding whenever necessary. Your leaf widgets in the tree should not care about whether a rebuild happens - they should be fine being placed in any environment and the framework takes care of reacting to changes to that environment by rebuilding correspondently.
I hope that I was able to clear this up for you :)
I faced a similar issue in my application. What resolved my issue was to make my "widget tree clean" as suggested by one of the programmers on this forum.
Try moving the definition of your stream to init state. This will prevent your stream from disconnecting and reconnecting every time there is a rebuild.
var datastream;
#override
void initState() {
dataStream = _bloc.inputObservable();
super.initState();
}
#override
Widget build(BuildContext context) {
return StreamBuilder(
stream: dataStream,
builder: (context, snapshot) {
if (snapshot.hasData) {
return TextFormField(
// ...
);
}
return const Center(
child: CircularProgressIndicator(),
);
},
);
}
It can be resolved by creating a stateful widget like following
class StatefulWrapper extends StatefulWidget {
final Function onInit;
final Widget child;
const StatefulWrapper({#required this.onInit, #required this.child});
#override
_StatefulWrapperState createState() => _StatefulWrapperState();
}
class _StatefulWrapperState extends State<StatefulWrapper> {
#override
void initState() {
if (widget.onInit.call != null) {
widget.onInit();
}
super.initState();
}
#override
Widget build(BuildContext context) {
return widget.child;
}
}
and wrapping the stateless widget using the wrapper
Widget body;
class WidgetStateless extends StatelessWidget {
WidgetStateless();
#override
Widget build(BuildContext context) {
return StatefulWrapper(
onInit: () async {
//Create the body widget in the onInit
body = Container();
},
child : body
)
}
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.