Reusable Components in Flutter: Choosing Between Inheritance and Mixins
How do you reuse functionality across your app without bloating your codebase? Should you stick to inheritance, or is there a better approach? In Dart, both inheritance and mixins offer ways to share code effectively, but each comes with its own strengths and trade-offs.
Inheritance organizes related components into hierarchies, while mixins provide a way to share behavior across classes without coupling them. In Flutter, these tools aren’t just theoretical — they help create dynamic widgets and maintainable architectures. Let’s explore how to use them practically, including advanced techniques.
Inheritance in Practice
What Is Inheritance?
Inheritance is a mechanism that allows a class (child) to derive properties and behavior from another class (parent). It’s most effective when there’s a clear is-a relationship between the parent and child classes.
Example: Notifications Hierarchy
Let’s take a real-world scenario: notifications. All notifications share some basic behavior, but each type has specific requirements.
abstract class Notification {
final String message;
Notification(this.message);
void display() {
print("Notification: $message");
}
}
class SnackbarNotification extends Notification {
SnackbarNotification(String message) : super(message);
@override
void display() {
print("Snackbar: $message");
}
}
class DialogNotification extends Notification {
DialogNotification(String message) : super(message);
@override
void display() {
print("Dialog: $message");
}
}
void main() {
final snackbar = SnackbarNotification("You have a new message!");
snackbar.display(); // Snackbar: You have a new message!
final dialog = DialogNotification("Update Available");
dialog.display(); // Dialog: Update Available
}
When to Use Inheritance
- Shared Behavior: Use inheritance when classes share significant functionality.
- Hierarchical Logic: It works best when subclasses logically extend the parent (e.g., a snackbar is-a notification).
Mixins for Code Reuse
What Are Mixins?
Mixins allow you to share functionality across unrelated classes. They’re especially useful when inheritance doesn’t make sense (e.g., sharing animations across components that aren’t hierarchically related).
Using the on
Keyword
The on
keyword in Dart lets you constrain mixins, ensuring they’re only applied to certain classes or subclasses. This provides additional control and allows the mixin to access the parent class’s properties and methods.
Example: Enhancing Notifications with Animations
Here’s how mixins can add sliding animations to our notifications.
mixin SlideAnimation on Notification {
void slideIn() {
print("Sliding in: $message");
}
void slideOut() {
print("Sliding out: $message");
}
}
class AnimatedSnackbar extends SnackbarNotification with SlideAnimation {
AnimatedSnackbar(String message) : super(message);
}
void main() {
final animatedSnackbar = AnimatedSnackbar("New Feature Released!");
animatedSnackbar.display(); // Snackbar: New Feature Released!
animatedSnackbar.slideIn(); // Sliding in: New Feature Released!
animatedSnackbar.slideOut(); // Sliding out: New Feature Released!
}
Why Use Mixins?
- Flexibility: Mixins don’t require a hierarchical relationship.
- Reusability: Apply the same mixin to multiple unrelated classes.
- Control: The
on
keyword ensures mixins are only used where appropriate.
Combining Multiple Mixins
Mixins can be combined to add multiple behaviors to a single class.
mixin FadeAnimation on Notification {
void fadeIn() {
print("Fading in: $message");
}
void fadeOut() {
print("Fading out: $message");
}
}
class FullyAnimatedDialog extends DialogNotification with SlideAnimation, FadeAnimation {
FullyAnimatedDialog(String message) : super(message);
void showAnimated() {
slideIn();
fadeIn();
display();
fadeOut();
slideOut();
}
}
void main() {
final animatedDialog = FullyAnimatedDialog("System Update Required");
animatedDialog.showAnimated();
// Sliding in: System Update Required
// Fading in: System Update Required
// Dialog: System Update Required
// Fading out: System Update Required
// Sliding out: System Update Required
}
When to Choose Inheritance or Mixins
Feature Inheritance Mixins Relationship Requires a parent-child hierarchy No hierarchy required Code Sharing Single parent class only Multiple mixins can be combined Coupling Tightly coupled to the parent class Decoupled, modular behavior Behavior Sharing Shared across a hierarchy Shared across unrelated classes Restrictions Applies only to child classes Can be constrained with on
Applying These Concepts in Flutter
Example 1: Custom Widgets with Inheritance
Inheritance is often used to extend base classes like StatelessWidget
or StatefulWidget
.
import 'package:flutter/material.dart';
class CustomButton extends StatelessWidget {
final String label;
final VoidCallback onPressed;
CustomButton({required this.label, required this.onPressed});
@override
Widget build(BuildContext context) {
return ElevatedButton(
onPressed: onPressed,
child: Text(label),
);
}
}
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Custom Button Example')),
body: Center(
child: CustomButton(
label: 'Click Me',
onPressed: () => print('Button Pressed'),
),
),
),
),
);
}
Example 2: Adding Reusable Animations with Mixins
Let’s use a mixin to add a bounce animation to a widget.
import 'package:flutter/material.dart';
mixin BounceAnimation<T extends StatefulWidget> on State<T> {
late AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
duration: Duration(seconds: 1),
vsync: this as TickerProvider,
)..repeat(reverse: true);
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Animation<double> get scale => Tween<double>(begin: 1.0, end: 1.2).animate(_controller);
}
class AnimatedIconWidget extends StatefulWidget {
@override
_AnimatedIconWidgetState createState() => _AnimatedIconWidgetState();
}
class _AnimatedIconWidgetState extends State<AnimatedIconWidget> with BounceAnimation<AnimatedIconWidget>, SingleTickerProviderStateMixin {
@override
Widget build(BuildContext context) {
return AnimatedBuilder(
animation: scale,
builder: (context, child) {
return Transform.scale(
scale: scale.value,
child: Icon(Icons.star, size: 50, color: Colors.orange),
);
},
);
}
}
void main() {
runApp(
MaterialApp(
home: Scaffold(
appBar: AppBar(title: Text('Bounce Animation Example')),
body: Center(child: AnimatedIconWidget()),
),
),
);
}
Conclusion
In this article, we’ve explored how inheritance and mixins can be used to build smarter, more reusable Flutter code. While inheritance organizes related components into hierarchies, mixins allow for flexible behavior sharing. By understanding their differences and strengths, you can write cleaner, more scalable applications.
Now it’s your turn — experiment with these concepts in DartPad or your favorite IDE, and take your Flutter projects to the next level! 🚀