Flutter: How to manually render (layout and paint) a widget subtree? - flutter

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.

Related

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

how to use shouldRepaint properly in flutter CustomPaint

Now I have a canvas of CustomPaint which draws a sketch, the sketch illustrates a basic 2D movement of "fluids" in pipe. The pipe is generated only once, but the fluid movement is controlled by user input.
I have a result that needs optimization. I used one CustomPaint class to draw both in one canvas, and it redraws everything each time the user changes input.
What I need is optimization, i.e. to draw the pipe part which does not change and only repaint the fluids each time the user changes input. That would be better for resources as things get more complex.
I don't know how to use shouldRepaint or whether it's gonna be useful.
Would it be possible to draw on two layers, keep one static and the other "updateable".
class PipeProfile extends CustomPainter {
FluidsData pipeData;
WellProfileData fluidsData;
PipeProfile({
#required this.pipeData Data,
#required this.fluidsData});
#override
void paint(Canvas canvas, Size size) {
cutomFunctionToDrawPipe(pipeData);
cutomFunctionToDrawFluids(fluidsData);
#override
bool shouldRepaint(covariant CustomPainter oldDelegate) {
return true;
}
}

Mock a Widget in Flutter tests

I am trying to create tests for my Flutter application. Simple example:
class MyWidget extends StatelessWidget {
#override
build(BuildContext context) {
return MySecondWidget();
}
}
I would like to verify that MyWidget is actually calling MySecondWidget without building MySecondWidget.
void main() {
testWidgets('It should call MySecondWidget', (WidgetTester tester) async {
await tester.pumpWidget(MyWidget());
expect(find.byType(MySecondWidget), findsOneWidget);
}
}
In my case this will not work because MySecondWidget needs some specific and complex setup (like an API key, a value in a Provider...). What I would like is to "mock" MySecondWidget to be an empty Container (for example) so it doesn't raise any error during the test.
How can I do something like that ?
There is nothing done out of the box to mock a widget. I'm going to write some examples/ideas on how to "mock"/replace a widget during a test (for example with a SizedBox.shrink().
But first, let me explain why I think this is not a good idea.
In Flutter you are building a widget tree. A specific widget has a parent and usually has one or several children.
Flutter chose a single pass layout algorithm for performance reasons (see this):
Flutter performs one layout per frame, and the layout algorithm works in a single pass. Constraints are passed down the tree by parent objects calling the layout method on each of their children. The children recursively perform their own layout and then return geometry up the tree by returning from their layout method. Importantly, once a render object has returned from its layout method, that render object will not be visited again until the layout for the next frame. This approach combines what might otherwise be separate measure and layout passes into a single pass and, as a result, each render object is visited at most twice during layout: once on the way down the tree, and once on the way up the tree.
From this, we need to understand that a parent needs its children to build to get their sizes and then render itself properly. If you remove its children, it might behave completely differently.
It is better to mock the services if possible. For example, if your child makes an HTTP request, you can mock the HTTP client:
HttpOverrides.runZoned(() {
// Operations will use MyHttpClient instead of the real HttpClient
// implementation whenever HttpClient is used.
}, createHttpClient: (SecurityContext? c) => MyHttpClient(c));
If the child needs a specific provider you can provide a dummy one:
testWidgets('My test', (tester) async {
tester.pumpWidget(
Provider<MyProvider>(
create: (_) => MyDummyProvider(),
child: MyWidget(),
),
);
});
If you still want to change a widget with another one during your tests, here are some ideas:
1. Use Platform.environment.containsKey('FLUTTER_TEST')
You can either import Platform from dart:io (not supported on web) or universal_io (supported on web).
and your build method could be:
#override
Widget build(BuildContext context) {
final isTest = Platform.environment.containsKey('FLUTTER_TEST');
if (isTest) return const SizedBox.shrink();
return // Your real implementation.
}
2. Use the annotation #visibleForTesting
You can annotate a parameter (ex: mockChild) that is only visible/usable in a test file:
class MyWidget extends StatelessWidget {
const MyWidget({
#visibleForTesting this.mockChild,
});
final Widget? child;
#override
Widget build(BuildContext context) {
return mockChild ?? // Your real widget implementation here.
}
}
And in your test:
tester.pumpWidget(
MyWidget(
mockChild: MyMockChild(),
),
);
You can mock MySecondWidget (eg using Mockito) but you do need to change your real code to create a MockMySecondWidget when in test mode, so it's not pretty. Flutter does not support object instantiation based on a Type (except through dart:mirrors but that is not compatible with Flutter), so you cannot 'inject' the type as a dependency. To determine if you are in test mode use Platform.environment.containsKey('FLUTTER_TEST') - best to determine this once upon startup and set the result as a global final variable, which will make any conditional statements quick.
One way to do it, is to wrap the child widget into a function, and pass the function to parent widget's constructor:
class MyWidget extends StatelessWidget {
final Widget Function() buildMySecondWidgetFn;
const MyWidget({
Key? key,
this.buildMySecondWidgetFn = _buildMySecondWidget
}): super(key: key);
#override
build(BuildContext context) {
return buildMySecondWidgetFn();
}
}
Widget _buildMySecondWidget() => MySecondWidget();
Then you can make up your mock widget, pass it thru buildMySecondWidgetFn in test.

Extracting Class Members like Widget Builders to a Different File?

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'

How do you clip GestureDetector (or InkWell) input Area with a path?

I've created a custom drawn widget using CustomPaint that has a path as an outline. But wrapping the widget in a GestureDetector makes the click area a rectangle around the whole canvas, Is there a way to clip the GestureDetector so that that click only works within the path?
You can implement the hitTest method from the CustomPainter, add your Path there and use the condition path.contains(position) to ensure the touch only covers the Path part.
class MyCustomPainter extends CustomPainter {
#override
void paint(Canvas canvas, Size size) {
// TODO: implement paint
}
#override
bool shouldRepaint(CustomPainter oldDelegate) {
// TODO: implement shouldRepaint
return null;
}
#override
bool hitTest(Offset position) {
Path path = Path();
//add your lines/curves here
path.close();
return path.contains(position);
}
}
More info about bool hitTest(Offset position):
Called whenever a hit test is being performed on an object that is
using this custom paint delegate.
The given point is relative to the same coordinate space as the last
[paint] call.
The default behavior is to consider all points to be hits for
background painters, and no points to be hits for foreground painters.
Return true if the given position corresponds to a point on the drawn
image that should be considered a "hit", false if it corresponds to a
point that should be considered outside the painted image, and null to
use the default behavior.
I answered a similar question here: Flutter: What is the correct way to detect touch enter, move and exit on CustomPainter objects