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
Widget
s that redirect to other "base"Widget
s? - When is it okay to use inheritance over composition? Is there a good rule of thumb for this?
CodePudding user response:
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();
}
CodePudding user response:
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 declaredconst
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 useis
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).