8 Must-Have Flutter Packages to Keep Your Code DRY and Development Fast

BatCat
4 min readMay 6, 2024

--

We’ve all heard the mantra: duplication is the enemy of clean, maintainable code. We at CyberHut share the same opinion. In the world of software development, sticking to principles like DRY (Don’t Repeat Yourself) is one of the pillars for building high-quality software. With Flutter we can easily avoid repetition and streamline our codebase.

Dive into a world of cleaner, more efficient Flutter code with these 8 essential packages that will help keep your code super DRY. Say goodbye to redundant code and hello to a more productive development process!

If you are an experienced Flutter dev, you might have a different list, feel free to share yours :)

UI Components

For styling, Flutter comes with great options.

Building authentication can be simplified with pre-built UI components and login screens. Along with the mentioned, the flutter_login package also offers animation effects so we don’t have to build them ourselves.

Networking

When we need to fetch data from anywhere, we will probably send an HTTP request. I prefer to use the http package for its simplicity in combination with http_interceptor. Using an interceptor is great when you want to glue the auth token to every request, log requests and their outcome or just check if the device has access to the Internet.

  • http: HTTP client for making network requests.
// An example to load weather data for city of Zagreb using the http package:
// Based on the official documentation
import 'package:http/http.dart' as http;

void main() async {
var apiKey = 'api_key';
var city = 'Zagreb';
var url = Uri.https('api.openweathermap.org', '/data/2.5/weather', {'q': city, 'appid': apiKey});

// Make a GET request to the weather API
var response = await http.get(url);

// Handle successful request
if (response.statusCode == 200) {
print('Response body: ${response.body}');
} else {
// Print an error message if the request failed
print('Failed to fetch weather data. Status code: ${response.statusCode}');
}
};
  • http_interceptor: a plugin that allows intercepting requests and responses from Dart’s http package.
// An example to add the authentication token to the headers using an interceptor:
// Based on the official documentation
import 'package:http_interceptor/http_interceptor.dart';

class AuthInterceptor implements InterceptorContract {
final String authToken;

AuthInterceptor(this.authToken);

@override
Future<RequestData> interceptRequest({required RequestData data}) async {
// Add the authentication token to the request headers
final headers = data.headers ?? {};
headers['Authorization'] = 'Bearer $authToken';

return data.copyWith(headers: headers);
}

@override
Future<ResponseData> interceptResponse({required ResponseData data}) async {
// No need to modify the response
return data;
}
}

Environment Variables

For loading configuration at runtime from a .env file, I used to rely on flutter_dotenv. Often this was the first package I installed when building a new application. However, I recently read that flutter_dotenv should be avoided and instead we should check out the envied package for a more secure experience. envied provides type safety for environment variables, allows defining default values for variables, custom parsing logic, and generate documentation for the environment variables. However, it requires additional setup since it does not use the plain .env format like flutter_dotenv, but besides the .env file, it requires creating an Env class with the configuration of each variable:

// Envied 
// Based on the official docs, link above
// .env at the root of the project
KEY1=VALUE1
KEY2=VALUE2

// lib/env/env.dart
import 'package:envied/envied.dart';

part 'env.g.dart';

@Envied(path: '.env.dev')
abstract class Env {
@EnviedField(varName: 'KEY1')
static const key1 = _Env.key1;
@EnviedField(obfuscate: true)
static const KEY2 = _Env.KEY2;
@EnviedField(defaultValue: 'test_')
static const key3 = _Env.key3;
}

// usage
print(Env.key1);
print(Env.KEY2);
// Flutter Dotenv
// Based on the official docs, link above
// .env at the root of the project
KEY1=VALUE1
KEY2=VALUE2

// main.dart
import 'package:flutter_dotenv/flutter_dotenv.dart' as DotEnv;

Future main() async {
await DotEnv.load(fileName: ".env");
// usage
print(env['KEY1']);
//...runapp
}
Flutter Dotenv is still more popular than Envied, however, Envied seems to slowly catch up!

State Management

Using the provider pattern is probably the most popular way to handle state management. The provider package is a wrapper around the InheritedWidget that reduces boilerplate and makes resource allocation and disposal simpler. The state of the application is visible in the Flutter devtool.

  • provider: simple state management solution using the provider pattern.

Persistence

Sometimes we need to store data that is not critical, and can be easily re-generated if needed, like authentication tokens or user name. Shared preferences will be the right place to keep such data.

  • shared_preferences: allows storing simple data on the device without guarantee that the data will be persisted.
final SharedPreferences prefs = await SharedPreferences.getInstance();

// Save an integer value to 'authToken' key.
await prefs.setString('authToken', 'your_auth_token_here');

// Try reading data from the 'authToken' key. If it doesn't exist, returns null.
final String? authToken = prefs.getString('authToken');

// Remove data for the 'authToken' key.
await prefs.remove('authToken');

Performance

We very rarely want to download all images every time our app loads. We mostly want to cache our images. The cached_network_image package helps the app load images faster by storing them locally, improving user experience with placeholder widgets and offline support, all while saving bandwidth and server resources.

. Caching network images is very easy and can be implemented with a few lines of code using this package:

CachedNetworkImage(
imageUrl: "http://my-image.com/200x200",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
),

Do you have a favorite list? Share it in the comments below!

The End.

--

--

BatCat
BatCat

Written by BatCat

My mission is to share some of the solutions I find during my journey as a data engineer. I mostly write about PostgreSQL and Python.

No responses yet