DEV Community

Cover image for Common pitfalls when designing widgets in Flutter
Harsh Bangari Rawat
Harsh Bangari Rawat

Posted on

Common pitfalls when designing widgets in Flutter

Flutter empowers developers to create beautiful and interactive user interfaces (UIs) with its extensive widget library. However, crafting efficient and maintainable widgets requires understanding common pitfalls that can hinder your development process.

Here are some common pitfalls to avoid when designing widgets in Flutter:

1. Excessive Nesting:

Reduced Readability: Deeply nested widget hierarchies can make your code difficult to read and maintain.
Performance Woes: It can also lead to performance issues as Flutter needs to rebuild the entire widget tree for changes in deeply nested parts.

Tip: Break down complex layouts into smaller, reusable widgets to improve readability and performance. Consider using InheritedWidget or Provider for state management across non-parent/child widget relationships.

//With Excessive Nesting
class UserProfile extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Container( // Container 1
      padding: EdgeInsets.all(16),
      child: Column( // Column 1
        children: [
          Row( // Row 1
            children: [
              CircleAvatar( // CircleAvatar 1
                backgroundImage: NetworkImage(
                  "https://sample.com/64", // NetworkImage 1
                ),
              ),
              SizedBox(width: 16), // SizedBox 1
              Column( // Column 2 (Nested within Row)
                crossAxisAlignment: CrossAxisAlignment.start,
                children: [
                  Text( 
                    "User Name",
                    style: TextStyle(fontSize: 18),
                  ),
                  SizedBox(height: 4), // SizedBox 2
                  Text(
                    "@user",
                    style: TextStyle(color: Colors.grey),
                  ),
                ],
              ),
            ],
          ),
          SizedBox(height: 16), // SizedBox 3
          Text( 
            "This is a very long bio describing the user's interests and experience. It wraps to multiple lines.",
            style: TextStyle(fontSize: 14),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Improved Widget

//Improved Code 
class UserProfile extends StatelessWidget {
  final String name;
  final String username;
  final String bio;
  final String imageUrl;

  const UserProfile({
    required this.name,
    required this.username,
    required this.bio,
    required this.imageUrl,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16),
      child: Column(
        children: [
          buildUserInfo(imageUrl, name, username),
          SizedBox(height: 16),
          Text(
            bio,
            style: TextStyle(fontSize: 14),
          ),
        ],
      ),
    );
  }

  Widget buildUserInfo(String imageUrl, String name, String username) {
    return Row(
      children: [
        CircleAvatar(
          backgroundImage: NetworkImage(imageUrl),
        ),
        SizedBox(width: 16),
        Column(
          crossAxisAlignment: CrossAxisAlignment.start,
          children: [
            Text(
              name,
              style: TextStyle(fontSize: 18),
            ),
            SizedBox(height: 4),
            Text(
              username,
              style: TextStyle(color: Colors.grey),
            ),
          ],
        ),
      ],
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Improvements:

  • Reusable buildUserInfo Widget: This widget encapsulates user information, improving code reuse and maintainability.
  • Reduced Nesting: We have a flatter hierarchy with fewer nested widgets for better readability.
  • Clearer Structure: The code is easier to understand and modify as each widget has a defined purpose.

2. The Misunderstood SizedBox:

The SizedBox widget offers a convenient way to add space between elements. However, overusing it can make your layout inflexible and unresponsive:

Limited Control: SizedBox only defines width and height, offering minimal control over layout behavior.
Solution: Utilize layout widgets like:

  • Row: Arranges widgets horizontally.
  • Column: Stacks widgets vertically.
  • Flex: Provides more control over sizing and alignment within a layout.
  • Stack: Overlays widgets on top of each other, enabling creative layering effects.
class ProductCard extends StatelessWidget {
  final String imageUrl;
  final String title;
  final String description;

  const ProductCard({
    required this.imageUrl,
    required this.title,
    required this.description,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16),
      child: Column(
        children: [
          // Image with excessive SizedBox
          SizedBox(
            height: 200, // Fixed height
            child: Image.network(imageUrl),
          ),
          SizedBox(height: 8), // Spacing between image and title
          Text(
            title,
            style: TextStyle(fontSize: 18),
          ),
          SizedBox(height: 4), // Spacing between title and description
          Text(
            description,
            maxLines: 2, // Limit description lines
            overflow: TextOverflow.ellipsis,
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

This code uses SizedBox extensively:

  • Fixed Height Image: The image has a fixed height (200) which might not adapt well to different screen sizes.
  • Repetitive Spacing: We use SizedBox for spacing between elements, making the layout inflexible.

Here's a more flexible approach using alternative widgets:

class ProductCard extends StatelessWidget {
  final String imageUrl;
  final String title;
  final String description;

  const ProductCard({
    required this.imageUrl,
    required this.title,
    required this.description,
  });

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16),
      child: Column(
        children: [
          // AspectRatio for flexible image
          AspectRatio(
            aspectRatio: 3 / 2, // Adjust as needed
            child: Image.network(imageUrl),
          ),
          Padding(
            padding: const EdgeInsets.symmetric(vertical: 8.0), // Consistent spacing
            child: Text(
              title,
              style: TextStyle(fontSize: 18),
            ),
          ),
          Text(
            description,
            maxLines: 2,
            overflow: TextOverflow.ellipsis,
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Improvements:

  • AspectRatio: We use AspectRatio to constrain the image while maintaining its aspect ratio, making it more responsive.
  • Padding Widget: We utilize Padding for consistent spacing between elements, improving layout control.
  • Reduced SizedBox: We minimize the use of SizedBox for a more flexible and adaptable layout.

3. Duplication Dilemma:

Duplicating similar widget code leads to redundancy and maintenance headaches. Imagine making a change that needs to be applied in multiple places – not fun!

Solution: Identify common UI patterns and create reusable widgets. This promotes:

  • Improved Code Organization: Less code duplication leads to cleaner and easier-to-maintain codebases.
  • Efficiency: Changes made to a reusable widget propagate throughout your app, saving time and effort.

4. The State Management Minefield:

Managing complex state within a single widget can quickly become messy and difficult to reason about.

Solution: Implement state management solutions like:

1. StatefulWidget: Manages internal state for dynamic UIs.
2. Provider: Enables state sharing across the widget tree without direct parent/child relationships Link.

Scenario using hospital app where:

-A Patient class stores patient information (name, ID, etc.).
-A HospitalProvider manages a list of patients.
-We want to display a list of patients in a PatientList widget and allow adding new patients from an AddPatient widget.

class Patient {
  final String name;
  final String id;

  Patient(this.name, this.id);
}
Enter fullscreen mode Exit fullscreen mode
class HospitalProvider extends ChangeNotifier {
  List<Patient> _patients = [];

  List<Patient> get patients => _patients;

  void addPatient(Patient patient) {
    _patients.add(patient);
    notifyListeners(); // Notify any listening widgets about the change
  }
}
Enter fullscreen mode Exit fullscreen mode
class MyApp extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Provider<HospitalProvider>(
      create: (_) => HospitalProvider(),
      child: MaterialApp(
        home: MyHomePage(),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
class MyHomePage extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Patient List'),
      ),
      body: Column(
        children: [
          PatientList(),
          AddPatient(),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
class PatientList extends StatelessWidget {
  @override
  Widget build(BuildContext context) {
    final patients = Provider.of<HospitalProvider>(context).patients;

    return ListView.builder(
      itemCount: patients.length,
      itemBuilder: (context, index) {
        final patient = patients[index];
        return Text('${patient.name} - ${patient.id}');
      },
    );
  }
}
Enter fullscreen mode Exit fullscreen mode
class AddPatient extends StatefulWidget {
  @override
  _AddPatientState createState() => _AddPatientState();
}

class _AddPatientState extends State<AddPatient> {
  String _name = '';
  String _id = '';

  void _handleAddPatient() {
    final hospitalProvider = Provider.of<HospitalProvider>(context);
    hospitalProvider.addPatient(Patient(_name, _id));
    // Clear input fields after adding
    setState(() {
      _name = '';
      _id = '';
    });
  }

  @override
  Widget build(BuildContext context) {
    return Container(
      padding: EdgeInsets.all(16.0),
      child: Column(
        children: [
          TextField(
            decoration: InputDecoration(labelText: 'Name'),
            onChanged: (value) => setState(() => _name = value),
          ),
          TextField(
            decoration: InputDecoration(labelText: 'ID'),
            onChanged: (value) => setState(() => _id = value),
          ),
          ElevatedButton(
            onPressed: _handleAddPatient,
            child: Text('Add Patient'),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Explanation:

  • HospitalProvider is created at the top level in MyApp, making it accessible to descendant widgets.
  • PatientList and AddPatient access the provider using Provider.of<HospitalProvider>(context).
  • When a new patient is added in AddPatient, it directly interacts with the provider, notifying any listening widgets (like PatientList) about the change.

3. Bloc: Provides a predictable way to manage application state with events and streams Link.

5. Reinventing the Wheel:

Flutter boasts a rich set of built-in widgets – don't reinvent the wheel unless absolutely necessary.

Solution: Leverage the existing widget library for common UI elements. This ensures:

  • Consistent Look and Feel: Adherence to built-in widget styles creates a cohesive user experience.
  • Saved Development Time: Utilize pre-built functionality instead of spending time replicating it.

6. Limited Testing:

Insufficient testing can lead to bugs and a frustrating user experience. Don't let bugs lurk in the shadows!

Solution:

Write Unit Tests: Test your widgets in isolation to ensure they behave as expected under various conditions.
Test on Different Devices and Screen Sizes: Verify your widgets render correctly across various devices and screen sizes to ensure a smooth experience for all users.

7. Documentation Drought:

Undocumented widgets are like cryptic messages – difficult to understand for you and others.

Solution:

  • Embrace Comments: Add clear and concise comments within the widget code explaining its purpose, usage, and behavior.
  • Example Power: Include code examples demonstrating how to use the widget effectively in various scenarios.
  • Explain Properties: Document each property of the widget, detailing its purpose, expected data types, and potential effects.
  • API Reference: Consider creating an API reference document summarizing the widget's functionality and usage for easy reference.

By following these guidelines and staying curious, you'll be well on your way to crafting exceptional Flutter widgets that contribute to a smooth and delightful user experience. Now, go forth and build something amazing!

Happy Coding! 🧑🏻‍💻

Top comments (0)