Flutter Custom Widgets

Building reusable components

🎨 What are Custom Widgets?

Custom widgets are reusable components you create by combining existing widgets. They help organize code, promote reusability, and make your app easier to maintain and scale effectively.


// Simple custom widget
class MyButton extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: () {},
      child: Text('Click Me'),
    );
  }
}
                                    

Widget Types

🔷

StatelessWidget

Immutable widgets

class MyWidget extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
🔶

StatefulWidget

Widgets with state

class MyWidget extends StatefulWidget {
  @override
  _MyWidgetState createState() => _MyWidgetState();
}

class _MyWidgetState extends State<MyWidget> {
  @override
  Widget build(BuildContext context) {
    return Container();
  }
}
📦

Parameters

Pass data to widgets

class MyWidget extends StatelessWidget {
  final String title;
  
  MyWidget({required this.title});
  
  @override
  Widget build(BuildContext context) {
    return Text(title);
  }
}
🎯

Callbacks

Handle user actions

class MyWidget extends StatelessWidget {
  final VoidCallback onTap;
  
  MyWidget({required this.onTap});
  
  @override
  Widget build(BuildContext context) {
    return GestureDetector(
      onTap: onTap,
      child: Container(),
    );
  }
}

🔹 Basic Custom Widget

Create a simple reusable button widget:

import 'package:flutter/material.dart';

class CustomButton extends StatelessWidget {
  final String text;
  final VoidCallback onPressed;
  final Color color;

  CustomButton({
    required this.text,
    required this.onPressed,
    this.color = Colors.blue,
  });

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      onPressed: onPressed,
      style: ElevatedButton.styleFrom(
        backgroundColor: color,
        padding: EdgeInsets.symmetric(horizontal: 30, vertical: 15),
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(10),
        ),
      ),
      child: Text(
        text,
        style: TextStyle(fontSize: 16, color: Colors.white),
      ),
    );
  }
}

// Usage
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      body: Center(
        child: CustomButton(
          text: 'Click Me',
          onPressed: () {
            print('Button pressed!');
          },
          color: Colors.green,
        ),
      ),
    );
  }
}

Output:

🔹 Custom Card Widget

Create a reusable card component:

class ProfileCard extends StatelessWidget {
  final String name;
  final String role;
  final String imageUrl;

  ProfileCard({
    required this.name,
    required this.role,
    required this.imageUrl,
  });

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: 4,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(15),
      ),
      child: Padding(
        padding: EdgeInsets.all(16),
        child: Row(
          children: [
            CircleAvatar(
              radius: 30,
              backgroundImage: NetworkImage(imageUrl),
            ),
            SizedBox(width: 16),
            Column(
              crossAxisAlignment: CrossAxisAlignment.start,
              children: [
                Text(
                  name,
                  style: TextStyle(
                    fontSize: 18,
                    fontWeight: FontWeight.bold,
                  ),
                ),
                SizedBox(height: 4),
                Text(
                  role,
                  style: TextStyle(
                    fontSize: 14,
                    color: Colors.grey[600],
                  ),
                ),
              ],
            ),
          ],
        ),
      ),
    );
  }
}

// Usage
ProfileCard(
  name: 'John Doe',
  role: 'Software Developer',
  imageUrl: 'https://example.com/avatar.jpg',
)

Output:

👤
John Doe
Software Developer

🔹 Stateful Custom Widget

Create a widget with internal state:

class CounterWidget extends StatefulWidget {
  final int initialValue;

  CounterWidget({this.initialValue = 0});

  @override
  _CounterWidgetState createState() => _CounterWidgetState();
}

class _CounterWidgetState extends State<CounterWidget> {
  late int _counter;

  @override
  void initState() {
    super.initState();
    _counter = widget.initialValue;
  }

  void _increment() {
    setState(() {
      _counter++;
    });
  }

  void _decrement() {
    setState(() {
      _counter--;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(20),
      decoration: BoxDecoration(
        color: Colors.blue[50],
        borderRadius: BorderRadius.circular(15),
      ),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Text(
            'Counter',
            style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
          ),
          SizedBox(height: 10),
          Text(
            '$_counter',
            style: TextStyle(fontSize: 48, fontWeight: FontWeight.bold, color: Colors.blue),
          ),
          SizedBox(height: 10),
          Row(
            mainAxisSize: MainAxisSize.min,
            children: [
              IconButton(
                icon: Icon(Icons.remove),
                onPressed: _decrement,
                color: Colors.red,
              ),
              SizedBox(width: 20),
              IconButton(
                icon: Icon(Icons.add),
                onPressed: _increment,
                color: Colors.green,
              ),
            ],
          ),
        ],
      ),
    );
  }
}

Output:

Counter
5

🔹 Custom Input Widget

Create a styled text input field:

class CustomTextField extends StatelessWidget {
  final String label;
  final IconData icon;
  final TextEditingController controller;
  final bool obscureText;

  CustomTextField({
    required this.label,
    required this.icon,
    required this.controller,
    this.obscureText = false,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      margin: EdgeInsets.symmetric(vertical: 10),
      child: TextField(
        controller: controller,
        obscureText: obscureText,
        decoration: InputDecoration(
          labelText: label,
          prefixIcon: Icon(icon, color: Colors.blue),
          border: OutlineInputBorder(
            borderRadius: BorderRadius.circular(12),
          ),
          focusedBorder: OutlineInputBorder(
            borderRadius: BorderRadius.circular(12),
            borderSide: BorderSide(color: Colors.blue, width: 2),
          ),
          filled: true,
          fillColor: Colors.grey[100],
        ),
      ),
    );
  }
}

// Usage
CustomTextField(
  label: 'Email',
  icon: Icons.email,
  controller: emailController,
)

Output:

📧

🔹 Custom Widget Best Practices

Tips for creating custom widgets:

  • Keep it focused - One widget, one purpose
  • Use StatelessWidget when possible for better performance
  • Make it configurable - Use parameters for flexibility
  • Document your widget - Add comments explaining usage
  • Follow naming conventions - Use descriptive names
  • Separate concerns - Keep logic and UI separate

🧠 Test Your Knowledge

Which widget type should you use for widgets that don't change?