Flutter (70): Combining existing components

Time: Column:Mobile & Frontend views:193

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):

Flutter (70): Combining existing components

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).