Art of Refactoring and Cleaning Code in Flutter
As software projects grow in complexity, it’s common for code to become cluttered, hard to read, and difficult to maintain. In Flutter, a popular mobile app development framework, it’s important to practice the art of refactoring and cleaning code to ensure that the app is scalable, maintainable, and efficient.
Refactoring is the process of restructuring existing code without changing its external behavior. This can involve simplifying complex code, reducing redundancy, improving performance, and increasing readability. Here are some tips for effective refactoring in Flutter:
Keep functions short and focused
Functions that do too much can quickly become unwieldy and difficult to understand. It’s best to keep functions short and focused, with a clear purpose. This makes them easier to test, debug, and modify in the future. For example:
// Bad example
void calculateInvoiceTotal(List<Item> items) {
double total = 0;
for (var item in items) {
total += item.price * item.quantity;
}
print('Total: $total');
}
// Good example
double calculateTotal(List<Item> items) {
double total = 0;
for (var item in items) {
total += item.price * item.quantity;
}
return total;
}
Remove duplication
Duplication in code can lead to inconsistencies and bugs. It’s important to identify and remove duplicate code wherever possible. For example:
// Bad example
void login() {
if (_emailController.text.isEmpty) {
showError('Please enter an email');
return;
}
if (_passwordController.text.isEmpty) {
showError('Please enter a password');
return;
}
// login logic
}
// Good example
void login() {
if (_validateInputs()) {
// login logic
}
}
bool _validateInputs() {
if (_emailController.text.isEmpty) {
showError('Please enter an email');
return false;
}
if (_passwordController.text.isEmpty) {
showError('Please enter a password');
return false;
}
return true;
}
Use meaningful variable names
Using descriptive variable names can make code much easier to read and understand. Avoid using abbreviations or short variable names that may not be clear to other developers. For example:
// Bad example
void calculateBMI(double w, double h) {
double bmi = w / (h * h);
print('BMI: $bmi');
}
// Good example
void calculateBMI(double weight, double height) {
double bmi = weight / (height * height);
print('BMI: $bmi');
}
Avoid unnecessary code
It’s important to remove any code that is no longer necessary, as this can clutter the codebase and make it harder to read. For example, removing comments that are no longer relevant can improve readability:
// Bad example
// TODO: remove this line of code before release
print('Debug info');
// Good example
// print('Debug info'); // commented out for release
Use Flutter’s built-in widgets
Flutter provides a wide range of built-in widgets that can be used to simplify code and improve performance. For example, instead of creating a custom button widget from scratch, it’s better to use Flutter’s ElevatedButton
or FlatButton
widgets:
// Bad example
Widget customButton(String text, VoidCallback onPressed) {
return Container(
decoration: BoxDecoration(
color: Colors.blue,
borderRadius: BorderRadius.circular(10),
),
child: TextButton(
onPressed: onPressed,
child: Text(text),
),
);
}
// Good example
ElevatedButton(
onPressed: onPressed,
child: Text(text),
)
Now let’s take a look at an example of cleaning up and refactoring code in Flutter. Suppose we have the following code for a login screen:
class LoginScreen extends StatefulWidget {
@override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final _emailController = TextEditingController();
final _passwordController = TextEditingController();
bool _isObscured = true;
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'Email',
),
),
SizedBox(height: 16.0),
TextField(
controller: _passwordController,
obscureText: _isObscured,
decoration: InputDecoration(
labelText: 'Password',
suffixIcon: IconButton(
icon: Icon(_isObscured ? Icons.visibility : Icons.visibility_off),
onPressed: () => setState(() => _isObscured = !_isObscured),
),
),
),
SizedBox(height: 32.0),
ElevatedButton(
onPressed: _validateInputs,
child: Text('Login'),
),
],
),
),
);
}
void _validateInputs() {
if (_emailController.text.isEmpty) {
_showError('Please enter an email');
return;
}
if (_passwordController.text.isEmpty) {
_showError('Please enter a password');
return;
}
// login logic
}
void _showError(String message) {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: Text('Error'),
content: Text(message),
actions: [
TextButton(
onPressed: () => Navigator.of(context).pop(),
child: Text('OK'),
),
],
),
);
}
}
Here are some ways we can improve this code:
Extract widgets into separate functions
We can extract the text fields and the login button into separate functions to make the code more modular and easier to read:
Widget _buildEmailTextField() {
return TextField(
controller: _emailController,
keyboardType: TextInputType.emailAddress,
decoration: InputDecoration(
labelText: 'Email',
),
);
}
Widget _buildPasswordTextField() {
return TextField(
controller: _passwordController,
obscureText: _isObscured,
decoration: InputDecoration(
labelText: 'Password',
suffixIcon: IconButton(
icon: Icon(_isObscured ? Icons.visibility : Icons.visibility_off),
onPressed: () => setState(() => _isObscured = !_isObscured),
),
),
);
}
Widget _buildLoginButton() {
return ElevatedButton(
onPressed: _validateInputs,
child: Text('Login'),
);
}
Then we can call these functions in the Column
widget:
Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildEmailTextField(),
SizedBox(height: 16.0),
_buildPasswordTextField(),
SizedBox(height: 32.0),
_buildLoginButton(),
],
),
Use named parameters
We can use named parameters to make the code more readable and avoid confusion when there are many parameters:
AlertDialog(
title: Text('Error'),
content: Text('Something went wrong'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: Text('OK'),
),
],
);
In the example above, we can see that named parameters are used for the title
, content
, and actions
properties of the AlertDialog
widget. This makes the code more readable and easier to understand.
Use helper methods
We can use helper methods to break down complex logic into smaller, more manageable chunks. This makes the code easier to read and maintain.
For example, consider the following code:
if (user != null) {
if (user.isVerified) {
if (user.hasSubscription) {
// Show premium content
} else {
// Show free content
}
} else {
// Show unverified user message
}
} else {
// Show login screen
}
This code can be refactored using helper methods:
void showPremiumContent() {
// Show premium content
}
void showFreeContent() {
// Show free content
}
void showUnverifiedUserMessage() {
// Show unverified user message
}
void showLoginScreen() {
// Show login screen
}
if (user != null) {
if (user.isVerified) {
if (user.hasSubscription) {
showPremiumContent();
} else {
showFreeContent();
}
} else {
showUnverifiedUserMessage();
}
} else {
showLoginScreen();
}
Use third-party packages
Finally, we can use third-party packages to further clean up our code. For example, we can use the flutter_form_builder
package to simplify the code for building forms:
import 'package:flutter_form_builder/flutter_form_builder.dart';
class LoginScreen extends StatelessWidget {
final _formKey = GlobalKey<FormBuilderState>();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: FormBuilder(
key: _formKey,
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
FormBuilderTextField(
name: 'email',
decoration: InputDecoration(labelText: 'Email'),
validator: FormBuilderValidators.email(context),
),
FormBuilderTextField(
name: 'password',
decoration: InputDecoration(labelText: 'Password'),
obscureText: true,
validator: FormBuilderValidators.minLength(context, 8),
),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
if (_formKey.currentState!.saveAndValidate()) {
print(_formKey.currentState!.value);
}
},
child: Text('Login'),
),
],
),
),
),
);
}
}
In the example above, we use the flutter_form_builder
package to create a login form with just a few lines of code. This makes the code more readable and easier to maintain.
Use constants and enums
We can use constants and enums to avoid magic numbers and strings:
enum LoginValidation { EMAIL, PASSWORD }
class LoginScreen extends StatefulWidget {
@override
_LoginScreenState createState() => _LoginScreenState();
}
class _LoginScreenState extends State<LoginScreen> {
final TextEditingController _emailController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
@override
Widget build(BuildContext context) {
return Scaffold(
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
TextFormField(
controller: _emailController,
decoration: InputDecoration(
labelText: 'Email',
),
validator: (value) {
if (value!.isEmpty) {
return 'Please enter your email';
} else if (!EmailValidator.validate(value)) {
return 'Invalid email address';
}
return null;
},
),
SizedBox(height: 16),
TextFormField(
controller: _passwordController,
decoration: InputDecoration(
labelText: 'Password',
),
obscureText: true,
validator: (value) {
if (value!.isEmpty) {
return 'Please enter your password';
} else if (value.length < 8) {
return 'Password must be at least 8 characters long';
}
return null;
},
),
SizedBox(height: 16),
ElevatedButton(
onPressed: () {
if (_emailController.text.isEmpty) {
showSnackbar(context, LoginValidation.EMAIL);
} else if (_passwordController.text.isEmpty) {
showSnackbar(context, LoginValidation.PASSWORD);
} else {
// Perform login logic
}
},
child: Text('Login'),
),
],
),
),
);
}
void showSnackbar(BuildContext context, LoginValidation validationType) {
String message = '';
switch (validationType) {
case LoginValidation.EMAIL:
message = 'Please enter your email';
break;
case LoginValidation.PASSWORD:
message = 'Please enter your password';
break;
}
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text(message),
),
);
}
}
In the example above, we use an enum LoginValidation
to specify the validation types for email and password. We also use constants such as SizedBox(height: 16)
and strings such as 'Please enter your email'
to avoid magic numbers and strings.
By using constants and enums, we can make our code more readable and maintainable by reducing the number of magic numbers and strings scattered throughout our code.
Conclusion
Refactoring and cleaning code in Flutter is essential to maintain code quality and make it more readable, maintainable, and efficient. By following best practices such as separating concerns, using constants and enums, using named parameters, and using helper methods, and leveraging third-party packages, we can make our code cleaner and more manageable.