February 20, 2026 · 3 min read
The Flutter Packages I Reach For
More packages isn't better. The pub.dev dependencies that consistently earn their place in my Flutter projects — bloc, dio, freezed, go_router, get_it and friends — and how I decide.
The Dart ecosystem on pub.dev is one of Flutter's quiet superpowers. But more packages isn't better — every dependency is code you don't control and have to maintain around. These are the ones that consistently earn their place in my projects, and why.
State management: bloc
flutter_bloc makes state explicit and testable. Events go in, states come out, and your UI is a pure function of the current state.
class CounterCubit extends Cubit<int> {
CounterCubit() : super(0);
void increment() => emit(state + 1);
}
// In the widget tree:
BlocBuilder<CounterCubit, int>(
builder: (context, count) => Text('$count'),
);
If you prefer a lighter, provider-style API, Riverpod is excellent and compile-safe. Both are good — pick one and be consistent.
Networking: dio
dio is http with the things you'll eventually need built in: interceptors, timeouts, cancellation, and form data.
final dio = Dio(BaseOptions(baseUrl: 'https://api.example.com'))
..interceptors.add(InterceptorsWrapper(
onRequest: (options, handler) {
options.headers['Authorization'] = 'Bearer $token';
handler.next(options);
},
));
final res = await dio.get('/articles');
Pair it with retrofit to generate a typed API client from an annotated interface.
Immutable models: freezed + json_serializable
Writing copyWith, ==, hashCode, and fromJson by hand is error-prone busywork. freezed generates all of it, plus sealed unions for your states.
@freezed
class Article with _$Article {
const factory Article({
required String id,
required String title,
@Default(false) bool published,
}) = _Article;
factory Article.fromJson(Map<String, dynamic> json) => _$ArticleFromJson(json);
}
Run dart run build_runner build and you get value equality, copyWith, and JSON — for free. build_runner is the code-gen engine the whole ecosystem shares.
Navigation: go_router
Declarative, URL-based routing that works with deep links and the web out of the box.
final router = GoRouter(
routes: [
GoRoute(path: '/', builder: (_, __) => const HomeScreen()),
GoRoute(
path: '/article/:id',
builder: (_, state) => ArticleScreen(id: state.pathParameters['id']!),
),
],
);
auto_route is a strong alternative if you'd rather generate type-safe routes from annotations.
Dependency injection: get_it (+ injectable)
A service locator to wire your layers together without passing objects through five constructors. Add injectable to register dependencies with annotations instead of by hand.
final sl = GetIt.instance;
sl.registerLazySingleton<ArticleRepository>(() => ArticleRepositoryImpl(sl()));
final repo = sl<ArticleRepository>();
The unglamorous essentials
- logger — readable, leveled logs instead of scattered
printcalls. - equatable — value equality without code-gen when
freezedis overkill. - shared_preferences / hive / isar — key-value and local database options, from simple to full offline persistence.
- cached_network_image — image loading with caching and placeholders, solved.
Ops and quality
Beyond app code, a few tools make delivery serious:
- FVM pins the Flutter SDK version per project, so "works on my machine" stops being a phrase.
- Flutter flavors separate dev/staging/prod builds with their own configs and app IDs.
- freeRASP adds runtime app self-protection (root/jailbreak, tamper, debugger detection).
- RevenueCat handles the cross-platform subscription plumbing you don't want to write.
- Sentry and Microsoft Clarity give you crash reports and real user-session insight after launch.
How I choose
Before adding a dependency I ask: is it well-maintained, does it have a clear owner, and would it be a nightmare to replace later? Favor small, focused packages that do one thing, keep them behind your own abstractions where you can, and treat every addition as a long-term commitment — because it is.