Flutter March 3, 2026 • 6 min read

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.

My Go-To Packages for Building Apps in Flutter

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!