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;
}
}
π 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);
}
π 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)),
);
}
}
π Step 2: Use the Reusable Button in Your UI
CustomButton(
text: "Click Me",
onPressed: () {
print("Button Clicked!");
},
),
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
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),
),
);
}
}
_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),
),
],
],
),
),
),
);
}
}
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");
}
}
}
_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"]}')),
);
},
);
},
);
}
}
},
),
),
],
),
);
}
}
π¬** 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)