DEV Community

Abubaker alhomidy
Abubaker alhomidy

Posted on

Reusable Components in Flutter: Write Once, Use Everywhere! πŸš€

Have you ever found yourself writing the same UI code repeatedly in Flutter? If so, it's time to embrace Reusable Componentsβ€”one of the best ways to write clean, maintainable, and scalable code.

Why Use Reusable Components?

In Flutter, Widgets are the foundation of everything, and they can be designed to be reusable. Instead of duplicating code, you can create custom widgets and service classesthat can be used across different parts of your app.

Benefits of Reusable Components

βœ… Less Code Duplication – Define once, use anywhere.
βœ… Easier Maintenance– Fix or update in one place, and it's reflected everywhere.
βœ… Better Scalability – Your app grows without turning into a mess of repeated code.

Beyond UI: Where Else Can You Apply This?

βœ”οΈ API Services – Centralizing API calls for better management.
βœ”οΈState Management – Using solutions like Provider or Bloc to avoid unnecessary logic repetition.
βœ”οΈ Form Inputs & Custom Buttons – Standardizing UI components for consistency.

Example 1: A Reusable API Client (Centralized API Calls) 🌐

Instead of writing*API calls multiple times* in different parts of your app, you can create a generic API client to handle all requests in a structured way.
*πŸ“Œ Step 1: Create a Reusable API Client
*

import 'package:dio/dio.dart';

class ApiClient {
  final Dio _dio = Dio(BaseOptions(baseUrl: "https://jsonplaceholder.typicode.com"));

  Future<T> get<T>(String endpoint) async {
    final response = await _dio.get(endpoint);
    return response.data as T;
  }

  Future<T> post<T>(String endpoint, dynamic data) async {
    final response = await _dio.post(endpoint, data: data);
    return response.data as T;
  }

  Future<T> put<T>(String endpoint, dynamic data) async {
    final response = await _dio.put(endpoint, data: data);
    return response.data as T;
  }

  Future<T> delete<T>(String endpoint) async {
    final response = await _dio.delete(endpoint);
    return response.data as T;
  }
}

Enter fullscreen mode Exit fullscreen mode

πŸ“Œ Step 2: Use the API Client in Your App
Instead of manually writing API calls everywhere, you now call the methods with a single line:

void fetchUsers() async {
  List<dynamic> users = await ApiClient().get<List<dynamic>>("/users");
  print(users);
}

void createUser() async {
  Map<String, dynamic> user = await ApiClient().post<Map<String, dynamic>>(
    "/users",
    {"name": "Coder Coder", "email": "[email protected]"},
  );
  print(user);
}
Enter fullscreen mode Exit fullscreen mode

πŸš€ Benefits of This Approach
**
βœ… **Reusability
– One class handles all API requests.
βœ… Flexibility – Supports multiple request types

(GET, POST, PUT, DELETE).
βœ… Maintainability– If you need to modify API logic (e.g., add headers, interceptors), update one file instead of multiple places.

Example 2: A Reusable Button Widget πŸ–±οΈ
Instead of styling and defining buttons repeatedly, create a custom widget that can be reused anywhere in your app.

πŸ“Œ Step 1: Create a Reusable Button Component

import 'package:flutter/material.dart';

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

  const CustomButton({
    Key? key,
    required this.text,
    required this.onPressed,
    this.color = Colors.blue,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      style: ElevatedButton.styleFrom(backgroundColor: color),
      onPressed: onPressed,
      child: Text(text, style: const TextStyle(color: Colors.white)),
    );
  }
}

Enter fullscreen mode Exit fullscreen mode

πŸ“Œ Step 2: Use the Reusable Button in Your UI

CustomButton(
  text: "Click Me",
  onPressed: () {
    print("Button Clicked!");
  },
),
Enter fullscreen mode Exit fullscreen mode

You can customize the component as you want add any property the previous code is just a *simple code * .

πŸš€ Benefits of This Approach
βœ… Consistency– Ensures buttons look the same across the app.
βœ… Ease of Maintenance – Update button styles in one place.
βœ… Less Repeated Code – Just pass text, color, and function when needed.

Final Thoughts πŸ’‘

By making your UI components and API services reusable, you:
βœ… Write less code while maintaining better structure.
βœ… Ensure consistency across your app.
βœ… Make future updates easier, as changes happen in one place.

I will show some photos of UI **with **reusable component

A list of users using the Mock API and use a **reusable component** like **buttons**and **cards**

A list of users using the Mock API and use a **reusable component** like **buttons** and **cards**

Here is the codes usage in the photos example
Custom Button Code

import 'package:flutter/material.dart';

class CustomButton extends StatelessWidget {
  final String text;
  final VoidCallback onPressed;
  final Color color;
  final double borderRadius;
  final EdgeInsetsGeometry padding;
  final TextStyle? textStyle;

  const CustomButton({
    Key? key,
    required this.text,
    required this.onPressed,
    this.color = Colors.blue,
    this.borderRadius = 8.0,
    this.padding = const EdgeInsets.symmetric(horizontal: 16.0, vertical: 12.0),
    this.textStyle,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return ElevatedButton(
      style: ElevatedButton.styleFrom(
        backgroundColor: color,
        padding: padding,
        shape: RoundedRectangleBorder(
          borderRadius: BorderRadius.circular(borderRadius),
        ),
      ),
      onPressed: onPressed,
      child: Text(
        text,
        style: textStyle ?? const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

_Custom User Card Code
_

import 'package:flutter/material.dart';

class UserCard extends StatelessWidget {
  final Map<String, dynamic> user;
  final VoidCallback? onTap;
  final Color cardColor;
  final double elevation;
  final double borderRadius;

  const UserCard({
    Key? key,
    required this.user,
    this.onTap,
    this.cardColor = Colors.white,
    this.elevation = 2.0,
    this.borderRadius = 8.0,
  }) : super(key: key);

  @override
  Widget build(BuildContext context) {
    return Card(
      elevation: elevation,
      color: cardColor,
      shape: RoundedRectangleBorder(
        borderRadius: BorderRadius.circular(borderRadius),
      ),
      margin: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
      child: InkWell(
        onTap: onTap,
        borderRadius: BorderRadius.circular(borderRadius),
        child: Padding(
          padding: const EdgeInsets.all(16.0),
          child: Column(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              Row(
                children: [
                  CircleAvatar(
                    backgroundColor: Colors.blue.shade100,
                    child: Text(
                      user['name'][0].toUpperCase(),
                      style: const TextStyle(color: Colors.blue, fontWeight: FontWeight.bold),
                    ),
                  ),
                  const SizedBox(width: 16.0),
                  Expanded(
                    child: Column(
                      crossAxisAlignment: CrossAxisAlignment.start,
                      children: [
                        Text(
                          user['name'],
                          style: const TextStyle(fontSize: 16.0, fontWeight: FontWeight.bold),
                        ),
                        const SizedBox(height: 4.0),
                        Text(
                          user['email'],
                          style: TextStyle(fontSize: 14.0, color: Colors.grey.shade700),
                        ),
                      ],
                    ),
                  ),
                ],
              ),
              if (user['company'] != null) ...[  
                const SizedBox(height: 12.0),
                Text(
                  'Company: ${user['company']['name']}',
                  style: TextStyle(fontSize: 14.0, color: Colors.grey.shade800),
                ),
              ],
              if (user['phone'] != null) ...[  
                const SizedBox(height: 8.0),
                Text(
                  'Phone: ${user['phone']}',
                  style: TextStyle(fontSize: 14.0, color: Colors.grey.shade800),
                ),
              ],
            ],
          ),
        ),
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

Custom API Client Class Code

import 'package:dio/dio.dart';

class ApiClient {
  final Dio _dio = Dio(BaseOptions(baseUrl: "https://jsonplaceholder.typicode.com"));

  Future<T> get<T>(String endpoint) async {
    try {
      final response = await _dio.get(endpoint);
      return response.data as T;
    } catch (e) {
      throw Exception("Failed to load data");
    }
  }

  Future<T> post<T>(String endpoint, dynamic data) async {
    try {
      final response = await _dio.post(endpoint, data: data);
      return response.data as T;
    } catch (e) {
      throw Exception("Failed to create data");
    }
  }

  Future<T> put<T>(String endpoint, dynamic data) async {
    try {
      final response = await _dio.put(endpoint, data: data);
      return response.data as T;
    } catch (e) {
      throw Exception("Failed to update data");
    }
  }

  Future<T> delete<T>(String endpoint) async {
    try {
      final response = await _dio.delete(endpoint);
      return response.data as T;
    } catch (e) {
      throw Exception("Failed to delete data");
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

_Code of UI _

import 'package:flutter/material.dart';
import 'package:test_sentry/api_client.dart';
import 'package:test_sentry/widgets/user_card.dart';
import 'package:test_sentry/widgets/custom_button.dart';

class UserListScreen extends StatefulWidget {
  @override
  _UserListScreenState createState() => _UserListScreenState();
}

class _UserListScreenState extends State<UserListScreen> {
  late Future<List<dynamic>> _users;
  bool _isGridView = false;

  @override
  void initState() {
    super.initState();
    _users = ApiClient().get<List<dynamic>>("/users");
  }

  void _refreshUsers() {
    setState(() {
      _users = ApiClient().get<List<dynamic>>("/users");
    });
  }

  void _toggleViewMode() {
    setState(() {
      _isGridView = !_isGridView;
    });
  }

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(
        title: Text('Users List'),
        actions: [
          IconButton(
            icon: Icon(_isGridView ? Icons.list : Icons.grid_view),
            onPressed: _toggleViewMode,
          ),
        ],
      ),
      body: Column(
        children: [
          Padding(
            padding: const EdgeInsets.all(16.0),
            child: Row(
              mainAxisAlignment: MainAxisAlignment.spaceBetween,
              children: [
                CustomButton(
                  text: 'Refresh Data',
                  onPressed: _refreshUsers,
                  color: Colors.green,
                ),
                CustomButton(
                  text: 'Create User',
                  onPressed: () {
                    // Demonstrate API client post method
                    ScaffoldMessenger.of(context).showSnackBar(
                      SnackBar(content: Text('Creating user...')),
                    );

                    ApiClient().post<Map<String, dynamic>>(
                      "/users",
                      {"name": "New User", "email": "[email protected]"},
                    ).then((response) {
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(content: Text('User created with ID: ${response["id"]}')),
                      );
                    }).catchError((error) {
                      ScaffoldMessenger.of(context).showSnackBar(
                        SnackBar(content: Text('Error: $error')),
                      );
                    });
                  },
                  color: Colors.blue,
                ),
              ],
            ),
          ),
          Expanded(
            child: FutureBuilder<List<dynamic>>(
              future: _users,
              builder: (context, snapshot) {
                if (snapshot.connectionState == ConnectionState.waiting) {
                  return Center(child: CircularProgressIndicator());
                } else if (snapshot.hasError) {
                  return Center(child: Text('Error: ${snapshot.error}'));
                } else if (!snapshot.hasData || snapshot.data!.isEmpty) {
                  return Center(child: Text('No data available.'));
                } else {
                  var users = snapshot.data!;

                  if (_isGridView) {
                    return GridView.builder(
                      padding: const EdgeInsets.all(8.0),
                      gridDelegate: SliverGridDelegateWithFixedCrossAxisCount(
                        crossAxisCount: 2,
                        childAspectRatio: 0.8,
                        crossAxisSpacing: 10,
                        mainAxisSpacing: 10,
                      ),
                      itemCount: users.length,
                      itemBuilder: (context, index) {
                        return UserCard(
                          user: users[index],
                          onTap: () {
                            ScaffoldMessenger.of(context).showSnackBar(
                              SnackBar(content: Text('Selected: ${users[index]["name"]}')),
                            );
                          },
                        );
                      },
                    );
                  } else {
                    return ListView.builder(
                      itemCount: users.length,
                      itemBuilder: (context, index) {
                        return UserCard(
                          user: users[index],
                          onTap: () {
                            ScaffoldMessenger.of(context).showSnackBar(
                              SnackBar(content: Text('Selected: ${users[index]["name"]}')),
                            );
                          },
                        );
                      },
                    );
                  }
                }
              },
            ),
          ),
        ],
      ),
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

πŸ’¬** How do you manage reusability in your Flutter projects? Let’s discuss in the comments! πŸš€**
Want more Flutter tips?
πŸ”Ή Join my Telegram channel for more Flutter guides: Telegram Link
πŸ”Ή Follow my Facebook page for daily coding insights: Facebook Link

Top comments (0)