Composing UI Components in Flutter
In Flutter, page UIs are typically composed of low-level components. When we need to encapsulate some common components, we should first consider whether we can achieve this through the composition of other components. If possible, composition should be prioritized, as directly assembling existing components is usually simpler, more flexible, and more efficient.
70.1 Example: Custom Gradient Button
1. Implementing GradientButton
The buttons in the Flutter Material component library do not support gradient backgrounds by default. To create a gradient background button, we will define a GradientButton
component that needs to support the following features:
Background gradient color
Ripple effect when pressed
Support for rounded corners
Let’s first take a look at the final effect we want to achieve (Figure):
We can use DecoratedBox
to support gradient background colors and rounded corners, and InkWell
to provide the ripple effect when pressed. Therefore, we can implement GradientButton
by composing DecoratedBox
and InkWell
, as shown in the following code:
import 'package:flutter/material.dart'; class GradientButton extends StatelessWidget { const GradientButton({ Key? key, this.colors, this.width, this.height, this.onPressed, this.borderRadius, required this.child, }) : super(key: key); // Gradient color array final List<Color>? colors; // Button width and height final double? width; final double? height; final BorderRadius? borderRadius; // Click callback final GestureTapCallback? onPressed; final Widget child; @override Widget build(BuildContext context) { ThemeData theme = Theme.of(context); // Ensure the colors array is not empty List<Color> _colors = colors ?? [theme.primaryColor, theme.primaryColorDark]; return DecoratedBox( decoration: BoxDecoration( gradient: LinearGradient(colors: _colors), borderRadius: borderRadius, ), child: Material( type: MaterialType.transparency, child: InkWell( splashColor: _colors.last, highlightColor: Colors.transparent, borderRadius: borderRadius, onTap: onPressed, child: ConstrainedBox( constraints: BoxConstraints.tightFor(height: height, width: width), child: Center( child: Padding( padding: const EdgeInsets.all(8.0), child: DefaultTextStyle( style: const TextStyle(fontWeight: FontWeight.bold), child: child, ), ), ), ), ), ), ); } }
As seen, GradientButton
is composed of DecoratedBox
, Padding
, Center
, InkWell
, and other components. Of course, the above code is just a sample; as a button, it is not complete—for instance, it lacks a disabled state. Readers can enhance it as needed. For convenience, I have encapsulated a more complete GradientButton
and added it to the flukit component library, which readers can use directly after importing the flukit library.
2. Using GradientButton
import 'package:flutter/material.dart'; import '../widgets/index.dart'; class GradientButtonRoute extends StatefulWidget { const GradientButtonRoute({Key? key}) : super(key: key); @override _GradientButtonRouteState createState() => _GradientButtonRouteState(); } class _GradientButtonRouteState extends State<GradientButtonRoute> { @override Widget build(BuildContext context) { return Column( mainAxisSize: MainAxisSize.min, children: <Widget>[ GradientButton( colors: const [Colors.orange, Colors.red], height: 50.0, child: const Text("Submit"), onPressed: onTap, ), GradientButton( height: 50.0, colors: [Colors.lightGreen, Colors.green.shade700], child: const Text("Submit"), onPressed: onTap, ), GradientButton( height: 50.0, colors: [Colors.lightBlue.shade300, Colors.blueAccent], child: const Text("Submit"), onPressed: onTap, ), ], ); } onTap() { print("button click"); } }
70.2 Summary
Defining components through composition is not different from writing UIs as we have done previously. However, when extracting standalone components, we need to consider code standards, such as using the required
keyword for necessary parameters and checking for null or setting default values for optional parameters in specific scenarios. This is important because users often may not understand the internal details of the component. To ensure robust code, we need to accommodate or provide error messages when users incorrectly utilize the component (using the assert function).