When should you prefer `Widget` inheritance over composition in Flutter? - flutter

I keep reading things like this post explaining how Flutter heavily prefers composition over inheritance. While I partially understand why, I question what to do in scenarios where this practice becomes verbose. Plus, in Flutter's internal code, there's inheritance all over the place for built-in components. So philosophically, there must be scenarios when it is okay.
Consider this example (based on a real Widget I made):
class MyFadingAnimation extends StatefulWidget {
final bool activated;
final Duration duration;
final Curve curve;
final Offset transformOffsetStart;
final Offset transformOffsetEnd;
final void Function()? onEnd;
final Widget? child;
const MyFadingAnimation({
super.key,
required this.activated,
this.duration = const Duration(milliseconds: 500),
this.curve = Curves.easeOut,
required this.transformOffsetStart,
this.transformOffsetEnd = const Offset(0, 0),
this.onEnd,
this.child,
});
#override
State<MyFadingAnimation> createState() => _MyFadingAnimationBuilder();
}
class _MyFadingAnimationBuilder extends State<MyFadingAnimation> {
#override
Widget build(BuildContext context) {
return AnimatedContainer(
duration: widget.duration,
curve: widget.curve,
transform: Transform.translate(
offset: widget.activated ?
widget.transformOffsetStart : widget.transformOffsetEnd,
).transform,
onEnd: widget.onEnd,
child: AnimatedOpacity(
duration: widget.duration,
curve: widget.curve,
opacity: widget.activated ? 1 : 0,
child: widget.child
),
);
}
}
The goal of MyFadingAnimation is to perform both a translation and opacity animation on a Widget simultaneously. Great!
Now, let's say I wanted to make some "shortcuts" or "aliases" to this widget, like MyHorizontalAnimation for fading in horizontally, or MyVerticalAnimation for fading in vertically. Using composition, you would have to create something like this:
class MyHorizontalAnimation extends StatelessWidget {
final bool activated;
final Duration duration;
final Curve curve;
final double offsetStart;
final void Function()? onEnd;
final Widget? child;
const MyHorizontalAnimation({
super.key,
required this.activated,
this.duration = const Duration(milliseconds: 500),
this.curve = Curves.easeOut,
required this.offsetStart,
this.onEnd,
this.child,
});
#override
Widget build(BuildContext context) {
return MyFadingAnimation(
activated: activated,
duration: duration,
curve: curve,
transformOffsetStart: Offset(offsetStart, 0),
onEnd: onEnd,
child: child,
);
}
}
That seems... very verbose to me. So my initial thought was "well, maybe I should just try extending the class anyway..."
class MyHorizontalAnimation extends MyFadingAnimation {
final double offsetStart;
MyHorizontalAnimation({
super.key,
required super.activated,
super.duration,
super.curve,
this.offsetStart,
super.onEnd,
super.child,
}) : super(
transformOffsetStart: Offset(offsetStart, 0),
);
}
To me this looks cleaner. Plus it carries the added benefit that if I added functionality/props to MyFadingAnimation, it's almost automatically integrated into MyHorizontalAnimation (with the exception of having to add super.newProp). With the composition approach, I'd have to add a new property, possibly copy/maintain a default, add it to the constructor, and by the time I'm done it just feels like a chore.
My main issue with using inheritance though (and this is probably really petty) is I can't have a const constructor for anything except my base widget, MyFadingAnimation. That, coupled with the strong discouragement of inheritance, makes me feel like there's a better way.
So, to sum everything up, here are my two questions:
How should I organize my code above to have const Widgets that redirect to other "base" Widgets?
When is it okay to use inheritance over composition? Is there a good rule of thumb for this?

I wouldn't worry about the lack of const in your redirecting constructors - after all, the composition example also lacks a const in the inner MyFadingAnimation construction. It's impossible to make a const Offset with an unknown integer argument, so this is an unavoidable language limitation.
On the topic of composition vs inheritance, there's another solution for your usecase: Secondary constructors in the base class. This pattern is used all over the framework - look at SizedBox, for example.
Do note that this style does introduce some repetitiveness when it comes to default argument values, however.
class MyFadingAnimation extends StatefulWidget {
final bool activated;
final Duration duration;
final Curve curve;
final Offset transformOffsetStart;
final Offset transformOffsetEnd;
final void Function()? onEnd;
final Widget? child;
const MyFadingAnimation({
super.key,
required this.activated,
this.duration = const Duration(milliseconds: 500),
this.curve = Curves.easeOut,
required this.transformOffsetStart,
this.transformOffsetEnd = const Offset(0, 0),
this.onEnd,
this.child,
});
MyFadingAnimation.horizontal({
super.key,
required this.activated,
this.duration = const Duration(milliseconds: 500),
this.curve = Curves.easeOut,
required double offsetStart,
this.onEnd,
this.child,
}) : transformOffsetStart = Offset(offsetStart, 0),
transformOffsetEnd = const Offset(0, 0);
#override
State<MyFadingAnimation> createState() => _MyFadingAnimationBuilder();
}

Why use composition over inheritance?
Optimization
As you have mentioned, when using composition, the outer widget can feature a const constructor, performing calculations in build rather than in the constructor itself. This is useful in multiple ways:
const is contagious. Any widgets that use your widget can be declared const as well, leading to significant benefits with large widget trees.
No calculations are performed unless the widget is actually used.
Take a widget that crossfades between two child widgets, for example. There are times when one child will never be built. Performing calculations in the build method allows for cheap construction of the child widget that may never be used.
Types don't matter
The Flutter style guide states the following:
Each API should be self-contained and should not know about other features.
Many Widgets take a child. Widgets should be entirely agnostic about the type of that child. Don’t use is or similar checks to act differently based on the type of the child.
For example, many widgets that have a text field accept any Widget, rather than a Text widget.
If this instruction is followed, one of the biggest features of inheritance is irrelevant: inherited public APIs. Widgets should not (usually) be used for anything other than being passed around and returned. What reason is there to use inheritance?
Flexibility
Using inheritance only allows a single, specifically typed child to be used. What if you wish to make the redirecting widget more complex later?
Perhaps, for example, you realize that the HorizontalFadingAnimation could use some added animated padding for a better visual effect.
Wrapping the child widget in another would require a complete rewrite of the outer widget, along with a breaking API change (as the widget's type would have to change).

Related

Is there a way to specify function parameters and return value as const in dart?

I wrote an extension function to add SizedBox between every child in Column or Row to add space between child, instead of putting SizedBox between every child, for which I didn't find any other way around.
Column(
children: [
// widgets
].setSpace(height: 10),
)
Row(
children: [
// widgets
].setSpace(width: 10),
)
So here List<Widget> setSpace({double? height, double? width}) takes height or width for the SizedBox used between child. But since height and width are not const I cannot use const SizedBox. So is there any way in dart to say that both the parameters and the return type will ALWAYS be cosnt? like const List<Widget> setSpace({const double? height, const double? width}) like C/C++?
I don't think that's possible, mostly because const can be applied only on constructors and fields, not on generic functions.
Maybe you can achieve that by creating your own widget that adds the SizedBox in its build method, and create a const constructor.
EDIT: here's a piece of code of mine of a custom widget with a const constructor.
class UnlockPage extends StatefulWidget {
final String pcData;
const UnlockPage({Key? key, required this.pcData}) : super(key: key);
#override
Widget build(BuildContext context) {
[...]
}
}
EDIT 2: here's a piece of code tested in DartPad. I don't think it can get better than this.
class SpacedColumn extends StatelessWidget {
final double height;
final List<Widget> children;
const SpacedColumn({required this.height, required this.children});
#override
Widget build(BuildContext context) {
var actualChildren = <Widget>[];
for (var child in children) {
actualChildren.add(child);
actualChildren.add(SizedBox(height: height));
}
return Column(
children: actualChildren,
);
}
}
You can't. As you pass a value this one can be different from one call to others.
Notice that const as not the same signification on Flutter than on other languages.
With Flutter it indicates to the rendering engine that the widget or the method is always the same and that the rendering engine is not obliged to rebuild this Widget when rebuilding the screen.
The keyword that act as const in other languages is final
In Dart language const doesn't mean the same as in other languages. You should use final if you don't want to change the values later.

Pass parent state to generic child widget

I'm building a flutter app, and I have built a customized AppBar widget for it. This appbar has a SearchBar widget, which calls whatever callback is passed to it onChange. Now, there are multiple screens that use this SearchBar, and each of them will do something different with the user input. But I've noticed that on each of the screens that use the appbar, I'd have to use a state to control the SearchBar inputted text. So, I'm trying to not have to create the state for every screen, and have a Widget that wraps my screens, and controls the input in it's state, and passes it's state down to the child I provide to this apps. This would be similar to React's Higher Order Components, which wrap another component and can pass props to it.
This seems to me like a good design pattern, but I don't know how to implement it. Since the child widget that I would pass to this second order component that would wrap my screens, won't be getting any info from it, the child is simply passed as a widget (note: the code is also doing other stuff, working as a general wraper to replace similar repetitive code in all of my screens):
class CustomScaffold extends StatefulWidget {
final ScreenInfo appBarInfo;
final Builder child;
final Widget bottomBarApp;
final Widget appBar;
final EdgeInsetsGeometry padding;
const CustomScaffold({
#required this.child,
Key key,
this.bottomBarApp,
this.appBar,
this.padding,
this.appBarInfo,
}) : super(key: key);
#override
_CustomScaffoldState createState() => _CustomScaffoldState();
}
class _CustomScaffoldState extends State<CustomScaffold> {
String searchTerm = '';
#override
Widget build(BuildContext context) {
return Scaffold(
body: SingleChildScrollView(
child: Column(
children: [
widget.appBar ??
GradientAppBar(
title: widget.appBarInfo.label,
searchBar: SearchBar(
callback: (String input) {
setState(() {
searchTerm = input;
});
},
placeholder: widget.appBarInfo.searchPlaceholder,
),
),
Padding(
padding: widget.padding,
child: widget.child,
),
],
),
),
bottomNavigationBar: widget.bottomBarApp ?? CustomBottomBarNavigator(),
);
}
}
I'm thinking that I could passa builder instead of a widget as the child, but I'm not sure how this would work. Also, I'm still learning bloc in general, so I'm not sure if it would be a good idea to use bloc for this. I'm guessing bloc's purpose is a little different, and would complicate this specific pattern.
Does this idea make sense? What would be the best way to implement it?
Thanks in advance.

Create builder with pre-built subtree in Flutter

In Flutter doc https://api.flutter.dev/flutter/widgets/ValueListenableBuilder-class.html, under Performance Optimization it states:
If your builder function contains a subtree that does not depend on the value of the ValueListenable, it's more efficient to build that subtree once instead of rebuilding it on every animation tick.
If you pass the pre-built subtree as the child parameter, the ValueListenableBuilder will pass it back to your builder function so that you can incorporate it into your build.
Using this pre-built child is entirely optional, but can improve performance significantly in some cases and is therefore a good practice.
Is there a more "general" builder widget that accepts pre-built subtree (similar to the mentioned ValueListenableBuilder) that is available in Flutter Widgets Catalog? If not, how does something like this work so I can create my own?
I looked at the source code but I don't understand.
I ended up creating my own.
class SubtreeBuilder extends StatelessWidget {
const SubtreeBuilder({
Key key,
#required this.builder,
this.child,
}) : super(key: key);
final Widget Function(BuildContext context, Widget child) builder;
final Widget child;
#override
Widget build(BuildContext context) {
return builder(context, child);
}
}

Standardizing sizes across application, but still use `const`

I'd like to standard size across a flutter application to comfort to a 4 pt grid. Here's one example of how this could be done:
class Spacing {
const Spacing(double val) : points = val * 4;
final double points;
}
class PtPadding extends Padding {
PtPadding({Spacing padding, Widget child}) : super(padding: padding.points, child: child);
}
PtPadding(padding: Spacing(4), child: Text('Hello'));
// or just with regular old Padding
Padding(padding: Spacing(4).points, child: Text('Hello'));
This is great, but it seems I forgo the ability to const my specialized PtPadding forces developers to use Spacing. On the other hand, just using Spacing in a constructor and accessing the points, prevents any widget from being "const"able. So it seems like I have to take a performance hit if I want to implement this spacing in my system.
I could have a class with static const members that point to doubles, but then I'm restrained to the sizes available (ie I can only have so many static members) and I also don't get the benefits of type restrictions.
I'm wondering if anyone else has thoughts in how I might approach this.
For what it's worth, I understand why Spacing(4).points is not a const (methods inherently aren't consts), but not sure how to get around this.
The problem is, you are extending Padding. Widgets are not made to be extended. Instead, you should use composition.
class Spacing {
const Spacing(double val) : points = val * 4;
final double points;
}
class PtPadding extends StatelessWidget {
const PtPadding({
Key key,
#required this.padding,
this.child,
}) : super(key: key);
final Spacing padding;
final Widget child;
#override
Widget build(BuildContext context) {
return Padding(
padding: EdgeInsets.all(padding.points),
child: child,
);
}
}

Image Map -like functionality in Flutter?

I've searched quite a bit and not found a solid answer, so if this is a dupe, I honestly tried.
I have an app I'm rewriting and moving away from an html-based hybrid platform (specifically Trigger.io); doing the rewrite in Flutter and Dart, on the quick.
Part of the app includes a pretty simple screen where the user can click on an image of a human body, and via an image map, get back an identifier and caption for the body part (right forearm, left knee, head, etc).
I simply can not find an analog to this behavior and capability in Flutter. Have I missed something simple because I was totally over thinking it?
Thanks much.
You could wrap your Image in a GestureDetector and specify onTapDown (or onTapUp) callbacks that check the tapped coordinates and act accordingly.
(To convert global coordinates to local coordinates, see: flutter : Get Local position of Gesture Detector)
Here's a rough attempt:
import 'package:quiver/iterables.dart' show enumerate;
class ImageMap extends StatelessWidget {
const ImageMap({
Key key,
#required this.image,
#required this.onTap,
#required this.regions,
}) : super(key: key);
final Widget image;
final List<Path> regions;
/// Callback that will be invoked with the index of the tapped region.
final void Function(int) onTap;
void _onTap(BuildContext context, Offset globalPosition) {
RenderObject renderBox = context.findRenderObject();
if (renderBox is RenderBox) {
final localPosition = renderBox.globalToLocal(globalPosition);
for (final indexedRegion in enumerate(regions)) {
if (indexedRegion.value.contains(localPosition)) {
onTap(indexedRegion.index);
return;
}
}
}
}
#override
Widget build(BuildContext context) {
return GestureDetector(
onTapDown: (details) => _onTap(context, details.globalPosition),
child: image,
);
}
}