My Go-To Packages for Building Apps in Flutter
Discover the top 10 essential Flutter packages I use in almost every project, from state management with flutter_bloc to networking with dio.
I’ve been building Flutter apps for years now, and I’ve found myself reaching for the same handful of packages every time I start a new project. They just save so much time and headaches.
Why use third-party packages?
You could build everything from scratch, but why would you? Flutter has a solid foundation, but the community has already solved the boring stuff like networking and state management. The right packages can literally save you weeks of reinventing the wheel.
The Go-To List
Here are the tools I keep coming back to:
1. dio
dio is my go-to for network requests. The built-in http package is fine for simple scripts, but dio handles all the edge cases you might hit in production.
Why dio over http?
Instead of dealing with repetitive http calls, dio natively supports powerful features directly on the client instance:
1. Global Configuration: Set it up once, and every request after that just works. I use this to set my base URLs and inject auth tokens without having to think about it for every single call:
final dio = Dio(BaseOptions(
baseUrl: 'https://api.example.com/',
connectTimeout: const Duration(seconds: 5),
receiveTimeout: const Duration(seconds: 3),
headers: {'Authorization': 'Bearer YOUR_TOKEN'},
));
// Inherits all settings above automatically
final response = await dio.get('/usersProfile');
2. Interceptors:
If you’ve ever tried to handle token refreshes manually across an entire app, you know it’s a nightmare. With dio interceptors, you write the logic once and forget about it:
dio.interceptors.add(
InterceptorsWrapper(
onRequest: (options, handler) {
print('Sending request to ${options.path}');
return handler.next(options);
},
onError: (DioException e, handler) {
if (e.response?.statusCode == 401) {
// Handle token refresh
}
return handler.next(e);
},
)
);
3. Form Data & File Uploads:
Manually constructing multipart requests in standard http feels terrible. dio makes file uploads actually reasonable:
final formData = FormData.fromMap({
'name': 'wendux',
'age': 25,
'file': await MultipartFile.fromFile('./text.txt', filename: 'upload.txt'),
});
final response = await dio.post('/info', data: formData);
2. go_router
go_router completely changed how I think about navigation. It’s maintained by the Flutter team now, which gives me peace of mind that it won’t be abandoned anytime soon.
Why go_router?
1. Declarative Routing: Defining your routes upfront makes everything so much easier to reason about, especially when things get messy with nested navigation.
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
],
);
// In MaterialApp.router:
// routerConfig: _router,
2. Deep Linking:
Deep links used to terrify me. go_router basically handles them for free if the route exists in your setup, the deep link just works without you having to write a bunch of boilerplate.
// DeepLink URL: https://example.com/users
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/users',
builder: (context, state) => const UsersScreen(),
),
],
);
// This will navigate to the users screen when the deep link is opened.
3. Redirection Logic: This alone is worth installing the package for. Want to kick out unauthenticated users and send them to a login screen? It takes about five lines of code.
final _router = GoRouter(
routes: [
GoRoute(
path: '/',
builder: (context, state) => const HomeScreen(),
),
GoRoute(
path: '/login',
builder: (context, state) => const LoginScreen(),
),
],
redirect: (context, state) {
final isLoggedIn = authService.isLoggedIn;
final isGoingToLogin = state.matchedLocation == '/login';
if (!isLoggedIn && !isGoingToLogin) {
return '/login';
}
return null;
},
);
3. flutter_bloc
State management arguments usually start fights, but I’ve landed firmly on flutter_bloc. It has a notoriously steep learning curve, but the structure it forces on you pays off massively as your app grows. I used to rely entirely on provider, but bloc just scales better for complex features. Plus, the documentation is fantastic.
Why flutter_bloc?
1. Immutability: You can’t accidentally mutate state directly. Have you ever spent three days tracking down a bug only to realize someone mutated an object inside a UI component? Immutability stops that entirely.
// emit new state if counter is incremented
class CounterBloc extends Bloc<int, int> {
CounterBloc() : super(0) {
on<CounterIncrementPressed>((event, emit) {
emit(state + 1);
});
}
}
2. Event-Driven Architecture: Everything that happens is an explicit event. This makes tracking exactly what went wrong during complex flows much more straightforward, and writing tests for it is surprisingly easy.
// emit new state if counter is incremented
final counterBloc = CounterBloc();
counterBloc.add(CounterIncrementPressed());
3. Uses provider under the hood:
If you already know provider, the transition isn’t as scary as it looks. It actually uses provider heavily under the hood, so concepts like BlocProvider will immediately feel familiar:
BlocProvider(
create: (context) => CounterBloc(),
child: const CounterScreen(),
)
BlocBuilder(
builder: (context, state) {
return Text(state.toString());
},
)
4. get_it
Passing dependencies down the widget tree gets old really fast. get_it lets me grab my services and repositories from anywhere without the hassle.
final getIt = GetIt.instance;
void setup() {
getIt.registerSingleton<AppModel>(AppModel());
}
// Access anywhere:
// var myAppModel = getIt<AppModel>();
5. equatable
If you’re using flutter_bloc, you basically need equatable. Writing your own == and hashCode overrides for every single model is mind-numbing boilerplate, and this handles it for you.
class Person extends Equatable {
const Person(this.name);
final String name;
@override
List<Object> get props => [name];
}
6. google_fonts
I genuinely hate managing font files in the pubspec.yaml. google_fonts lets you grab pretty much any font you want on the fly without dragging .ttf files around your project structure.
Text(
'This is Google Fonts',
style: GoogleFonts.lato(
textStyle: TextStyle(color: Colors.blue, letterSpacing: .5),
),
),
7. intl
For apps that might support multiple languages or need specific date/number formatting, intl is required. It provides facilities for message translation, plurals and genders, date/number formatting and parsing, and bidirectional text.
var format = DateFormat.yMd('en_US');
var dateString = format.format(DateTime.now());
// Output: 3/3/2026
var currency = NumberFormat.simpleCurrency(name: 'USD');
print(currency.format(1200000));
// Output: $1,200,000.00
8. wolt_modal_sheet
Bottom sheets are common in mobile UX, but standard ones can be limiting. wolt_modal_sheet provides a flexible, multi-page bottom sheet that handles complex navigation flows within the sheet itself. I use this for multi-step forms or settings menus.
WoltModalSheet.show<void>(
context: context,
pageListBuilder: (modalSheetContext) {
return [
WoltModalSheetPage(
child: const Text('Page 1 Content'),
),
];
},
);
9. cached_network_image
Loading images from the internet is slow. cached_network_image takes care of fetching and caching images locally, so they load instantly the next time. It also provides easy ways to show loading placeholders and error widgets.
CachedNetworkImage(
imageUrl: "http://via.placeholder.com/350x150",
placeholder: (context, url) => CircularProgressIndicator(),
errorWidget: (context, url, error) => Icon(Icons.error),
)
10. flutter_form_builder
Forms in Flutter can require a lot of controllers and validation logic. flutter_form_builder reduces the boilerplate by grouping fields together and handling validation, saving me a ton of time on data entry screens.
final _formKey = GlobalKey<FormBuilderState>();
FormBuilder(
key: _formKey,
child: Column(
children: [
FormBuilderTextField(
name: 'email',
decoration: const InputDecoration(labelText: 'Email'),
validator: FormBuilderValidators.compose([
FormBuilderValidators.required(),
FormBuilderValidators.email(),
]),
),
],
),
)
Conclusion
Everyone’s stack looks a bit different, but these are the packages I keep coming back to. Did I miss any obvious ones that you swear by? Let me know!