DEV Community

Ge Ji
Ge Ji

Posted on

Flutter Lesson 6: Buttons and Interactive Components

Interaction is one of the core functions of mobile applications, and buttons and various interactive components are the foundation for implementing user interactions. Flutter provides a rich set of built-in interactive components that can meet user interaction needs in various scenarios. This lesson will detailedly introduce commonly used button components, other interactive controls, and gesture recognition mechanisms in Flutter, helping you build responsive user interfaces.

I. Common Button Components

Flutter offers several predefined button components that follow Material Design specifications, suitable for different interface scenarios.

1. ElevatedButton

ElevatedButton is a button with shadow and background color that has an elevation effect when clicked, suitable for primary actions.

ElevatedButton(
  // Button click callback
  onPressed: () {
    print("ElevatedButton was clicked");
  },
  // Button text content
  child: const Text("Submit"),
  // Button style
  style: ElevatedButton.styleFrom(
    // Background color
    backgroundColor: Colors.blue,
    // Text color
    foregroundColor: Colors.white,
    // Button padding
    padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
    // Font size
    textStyle: const TextStyle(fontSize: 16),
    // Border radius
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(8),
    ),
    // Shadow
    elevation: 4,
  ),
)
Enter fullscreen mode Exit fullscreen mode

When onPressed is null, the button will be in a disabled state:

ElevatedButton(
  onPressed: null, // Disabled button
  child: const Text("Disabled State"),
)
Enter fullscreen mode Exit fullscreen mode

2. TextButton

TextButton is a text button without background color or shadow, containing only text and click effects, suitable for secondary actions.

TextButton(
  onPressed: () {
    print("TextButton was clicked");
  },
  child: const Text("Cancel"),
  style: TextButton.styleFrom(
    foregroundColor: Colors.grey[700], // Text color
    padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
    textStyle: const TextStyle(fontSize: 14),
  ),
)
Enter fullscreen mode Exit fullscreen mode

3. OutlinedButton

OutlinedButton is a button with a border but no background color, suitable for tertiary actions.

OutlinedButton(
  onPressed: () {
    print("OutlinedButton was clicked");
  },
  child: const Text("View Details"),
  style: OutlinedButton.styleFrom(
    foregroundColor: Colors.blue, // Text and border color
    side: const BorderSide(color: Colors.blue), // Border style
    padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
    shape: RoundedRectangleBorder(
      borderRadius: BorderRadius.circular(4),
    ),
  ),
)
Enter fullscreen mode Exit fullscreen mode

4. Buttons with Icons

All buttons can have icons added through the child property to achieve combined text and icon effects:

// ElevatedButton with icon
ElevatedButton.icon(
  onPressed: () {},
  icon: const Icon(Icons.save, size: 18), // Icon
  label: const Text("Save"), // Text
)

// TextButton with icon
TextButton.icon(
  onPressed: () {},
  icon: const Icon(Icons.share, size: 18),
  label: const Text("Share"),
)

// OutlinedButton with icon
OutlinedButton.icon(
  onPressed: () {},
  icon: const Icon(Icons.delete, size: 18),
  label: const Text("Delete"),
)
Enter fullscreen mode Exit fullscreen mode

5. Unified Button Theme Management

To maintain consistency in button styles throughout the application, you can use Theme or ButtonTheme to configure button styles uniformly:

Theme(
  data: ThemeData(
    elevatedButtonTheme: ElevatedButtonThemeData(
      style: ElevatedButton.styleFrom(
        backgroundColor: Colors.purple,
        padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(8),
        ),
      ),
    ),
  ),
  child: Column(
    children: [
      ElevatedButton(
        onPressed: () {},
        child: const Text("Button 1"), // Will apply theme style
      ),
      ElevatedButton(
        onPressed: () {},
        child: const Text("Button 2"), // Will apply theme style
      ),
    ],
  ),
)
Enter fullscreen mode Exit fullscreen mode

II. Button Click Events and State Changes

The core function of buttons is to respond to user click operations, implemented through the onPressed callback. Additionally, button states change with interactions.

1. Basic Click Events

onPressed is the most basic callback function for buttons, triggered when the user clicks the button:

ElevatedButton(
  onPressed: () {
    // Handle click event
    print("Button clicked");
    // You can update state, navigate to new pages, or perform other operations here
  },
  child: const Text("Click Me"),
)
Enter fullscreen mode Exit fullscreen mode

2. Long Press Events

In addition to clicks, buttons also support long press events, implemented through the onLongPress callback:

ElevatedButton(
  onPressed: () {
    print("Click event");
  },
  onLongPress: () {
    print("Long press event");
  },
  child: const Text("Try Long Press"),
)
Enter fullscreen mode Exit fullscreen mode

3. Button State Management

Button states (enabled/disabled, loading) usually need to change dynamically based on application state:

class ButtonStateExample extends StatefulWidget {
  const ButtonStateExample({super.key});

  @override
  State<ButtonStateExample> createState() => _ButtonStateExampleState();
}

class _ButtonStateExampleState extends State<ButtonStateExample> {
  bool _isLoading = false;
  bool _isEnabled = true;

  void _handleSubmit() async {
    // Disable button and show loading state
    setState(() {
      _isLoading = true;
      _isEnabled = false;
    });

    // Simulate network request
    await Future.delayed(const Duration(seconds: 2));

    // Restore button state
    setState(() {
      _isLoading = false;
      _isEnabled = true;
    });
  }

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: _isEnabled ? _handleSubmit : null,
      child: _isLoading
          ? const Row(
              mainAxisSize: MainAxisSize.min,
              children: [
                CircularProgressIndicator(
                  color: Colors.white,
                  strokeWidth: 2,
                ),
                SizedBox(width: 8),
                Text("Processing..."),
              ],
            )
          : const Text("Submit"),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

III. Other Interactive Components

In addition to buttons, Flutter provides various commonly used interactive components for form input, selection, and other functions.

1. Checkbox

Checkbox is used for multiple selection scenarios, allowing users to select multiple options:

class CheckboxExample extends StatefulWidget {
  const CheckboxExample({super.key});

  @override
  State<CheckboxExample> createState() => _CheckboxExampleState();
}

class _CheckboxExampleState extends State<CheckboxExample> {
  bool _isChecked = false;

  @override
  Widget build(BuildContext context) {
    return Checkbox(
      value: _isChecked, // Current selection state
      onChanged: (value) {
        // State change callback
        setState(() {
          _isChecked = value ?? false;
        });
      },
      activeColor: Colors.blue, // Color when checked
      checkColor: Colors.white, // Color of check icon
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

It's usually used in combination with Text and laid out with Row:

Row(
  children: [
    Checkbox(
      value: _isChecked,
      onChanged: (value) {
        setState(() => _isChecked = value ?? false);
      },
    ),
    const Text("I have read and agree to the user agreement"),
  ],
)
Enter fullscreen mode Exit fullscreen mode

CheckboxListTile is a more complete component containing a checkbox, text, and icon:

CheckboxListTile(
  value: _isChecked,
  onChanged: (value) {
    setState(() => _isChecked = value ?? false);
  },
  title: const Text("Receive push notifications"),
  subtitle: const Text("Receive latest activities and offers"),
  secondary: const Icon(Icons.notifications),
  controlAffinity: ListTileControlAffinity.leading, // Checkbox position
)
Enter fullscreen mode Exit fullscreen mode

2. Radio

Radio is used for single selection scenarios where only one option in a group can be selected:

class RadioExample extends StatefulWidget {
  const RadioExample({super.key});

  @override
  State<RadioExample> createState() => _RadioExampleState();
}

class _RadioExampleState extends State<RadioExample> {
  String? _selectedOption;
  final List<String> _options = ['Option A', 'Option B', 'Option C'];

  @override
  Widget build(BuildContext context) {
    return Column(
      children: _options.map((option) {
        return RadioListTile(
          title: Text(option),
          value: option,
          groupValue: _selectedOption,
          onChanged: (value) {
            setState(() {
              _selectedOption = value;
            });
          },
        );
      }).toList(),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

RadioListTile is a complete component containing a radio button and text, more convenient than using Radio alone.

3. Switch

Switch is used to toggle between two states (on/off), usually for settings:

class SwitchExample extends StatefulWidget {
  const SwitchExample({super.key});

  @override
  State<SwitchExample> createState() => _SwitchExampleState();
}

class _SwitchExampleState extends State<SwitchExample> {
  bool _isEnabled = false;

  @override
  Widget build(BuildContext context) {
    return Switch(
      value: _isEnabled,
      onChanged: (value) {
        setState(() {
          _isEnabled = value;
        });
      },
      activeColor: Colors.green, // Color when enabled
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

SwitchListTile is a complete component containing a switch and text:

SwitchListTile(
  title: const Text("Dark Mode"),
  subtitle: const Text("Switch to dark display mode"),
  value: _isDarkMode,
  onChanged: (value) {
    setState(() => _isDarkMode = value);
  },
  secondary: const Icon(Icons.dark_mode),
)
Enter fullscreen mode Exit fullscreen mode

4. Slider

Slider is used to select a value within a range, such as volume adjustment, brightness settings, etc.:

class SliderExample extends StatefulWidget {
  const SliderExample({super.key});

  @override
  State<SliderExample> createState() => _SliderExampleState();
}

class _SliderExampleState extends State<SliderExample> {
  double _value = 50;

  @override
  Widget build(BuildContext context) {
    return Column(
      children: [
        Slider(
          value: _value,
          min: 0, // Minimum value
          max: 100, // Maximum value
          divisions: 10, // Number of divisions
          label: "${_value.round()}", // Label displayed when dragging
          onChanged: (value) {
            setState(() {
              _value = value;
            });
          },
          onChangeStart: (value) {
            print("Start dragging: $value");
          },
          onChangeEnd: (value) {
            print("End dragging: $value");
          },
        ),
        Text("Current value: ${_value.round()}"),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

IV. Gesture Recognition: GestureDetector

GestureDetector is a powerful gesture recognition component that can wrap any widget and detect various user gestures such as taps, swipes, long presses, etc.

1. Basic Tap Gestures

GestureDetector(
  // Single tap
  onTap: () {
    print("Single tap");
  },
  // Double tap
  onDoubleTap: () {
    print("Double tap");
  },
  // Long press
  onLongPress: () {
    print("Long press");
  },
  child: Container(
    width: 100,
    height: 100,
    color: Colors.blue,
    child: const Center(
      child: Text(
        "Tap Me",
        style: TextStyle(color: Colors.white),
      ),
    ),
  ),
)
Enter fullscreen mode Exit fullscreen mode

2. Swipe Gestures

GestureDetector can detect various swipe gestures, including horizontal swipes, vertical swipes, etc.:

class SwipeExample extends StatefulWidget {
  const SwipeExample({super.key});

  @override
  State<SwipeExample> createState() => _SwipeExampleState();
}

class _SwipeExampleState extends State<SwipeExample> {
  String _direction = "Please swipe";

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      // Swipe start
      onPanStart: (details) {
        print("Swipe started: ${details.globalPosition}");
      },
      // Swipe update
      onPanUpdate: (details) {
        // Swipe offset
        print("Swipe offset: ${details.delta}");
      },
      // Swipe end
      onPanEnd: (details) {
        // Swipe velocity and direction
        final velocity = details.velocity.pixelsPerSecond;
        setState(() {
          if (velocity.dx.abs() > velocity.dy.abs()) {
            // Horizontal swipe
            _direction = velocity.dx > 0 ? "Swipe right" : "Swipe left";
          } else {
            // Vertical swipe
            _direction = velocity.dy > 0 ? "Swipe down" : "Swipe up";
          }
        });
      },
      child: Container(
        width: 300,
        height: 200,
        color: Colors.grey[200],
        child: Center(
          child: Text(
            _direction,
            style: const TextStyle(fontSize: 18),
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

3. Scale Gestures

GestureDetector can also detect scale gestures for functions like image zooming:

class ScaleExample extends StatefulWidget {
  const ScaleExample({super.key});

  @override
  State<ScaleExample> createState() => _ScaleExampleState();
}

class _ScaleExampleState extends State<ScaleExample> {
  double _scale = 1.0;
  double _previousScale = 1.0;

  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onScaleStart: (details) {
        _previousScale = _scale;
      },
      onScaleUpdate: (details) {
        setState(() {
          _scale = _previousScale * details.scale;
          // Limit scale range
          _scale = _scale.clamp(0.5, 3.0);
        });
      },
      child: Transform.scale(
        scale: _scale,
        child: Image.network(
          "https://picsum.photos/400/300",
          width: 400,
          height: 300,
          fit: BoxFit.cover,
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

4. Gesture Competition and Priority

When multiple gestures might be recognized simultaneously (such as tap and long press), Flutter has a gesture competition mechanism to determine which gesture should be responded to. You can control gesture detection behavior through the behavior property:

GestureDetector(
  behavior: HitTestBehavior.opaque, // Can detect gestures even if child is transparent
  onTap: () {
    print("Container clicked");
  },
  child: Container(
    width: 100,
    height: 100,
    // Transparent container can also respond to gestures
  ),
)
Enter fullscreen mode Exit fullscreen mode

Values for HitTestBehavior include:

  • deferToChild: Default value, child components prioritize responding to gestures
  • opaque: Current component prioritizes responding to gestures, even if transparent
  • translucent: Both current component and child components can respond to gestures

Top comments (0)