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:
const
Widget
s that redirect to other "base" Widget
s?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();
}