Dependencies are slowly degrading your Flutter codebase

Dependencies are slowly degrading your Flutter codebase

Some reasons why using dependencies might be or not be a good choice.

Introduction

You start off by adding Riverpod, thinking it will make your life easier.

dependencies:
flutter:
sdk: flutter
# State Management
flutter_riverpod: ^2.4.5

Then you add get_it, and then freezed, and before long you have a project with 43 dependencies.

dependencies:
flutter:
sdk: flutter
# State Management
flutter_riverpod: ^2.4.5
# Navigation & Routing
go_router: ^12.1.3
# Animations
animations: ^2.0.11
# Networking
dio: ^5.4.0
web_socket_channel: ^2.4.0
# Database & Storage
drift: ^2.16.0
shared_preferences: ^2.2.2
# Image & File Handling
image_picker: ^1.0.7
path_provider: ^2.1.2
# Background Processing
workmanager: ^0.5.1
# Utilities
intl: ^0.18.1
uuid: ^4.2.1
url_launcher: ^6.2.5
device_info_plus: ^9.1.2
package_info_plus: ^4.2.0
connectivity_plus: ^5.0.2
# Permissions
permission_handler: ^11.2.0
# Logging & Debugging
logger: ^2.0.2
sentry_flutter: ^7.14.0
# Security
flutter_secure_storage: ^9.0.0
# Firebase
firebase_core: ^2.24.2
firebase_auth: ^4.14.0
cloud_firestore: ^4.13.6
firebase_messaging: ^14.7.9
# UI Components
flutter_svg: ^2.0.9
shimmer: ^3.0.0
carousel_slider: ^4.2.1
expandable: ^5.0.1

Now, instead of working on features for your application, you spend the majority of your time updating and submitting issues. Welcome to dependency hell.

Dependency Hell

Here’s the real question: Should you really be off-loading so much of your application to third parties?

giphy-5

Let’s be clear. Dependencies aren’t bad, but you should understand the code you depend on and the consequences that occur if this dependency changes, becomes outdated, or has bugs.

Let’s take get_it as an example. It is a great package for creating a locator that can be used throughout your app.

Screenshot 2025-03-27 at 08.00.44

With a locator, you can instantiate a class and reference that specific instance wherever you need it. But do you need all the functionality that comes from the dependency?

final locator = GetIt.instance;
void setup() {
locator.registerSingleton<AppModel>(AppModel());
}
MaterialButton(
child: Text("Update"),
// given that your AppModel has a method update
onPressed: locator<AppModel>().update
),

There is registerSingleton, registerLazySingleton, registerFactory, and the list goes on. How many of these features will you actually use?

If you are only using one or two of these methods, you might consider creating your own implementation. By doing that, you remove a potential vulnerability of having a dependency, and you usually reduce the complexity as well.

Maybe we only need registerSingleton

Here is a simple example of what a service locator is. The example code is not as fully featured as the get_it package, but depending on your app you might not need it to be. This simple example can be built out to have more of those features as you need them.

class ServiceLocator {
static final ServiceLocator instance = ServiceLocator._();
ServiceLocator._();
final Map<Type, dynamic> _services = {};
void registerSingleton<T>(T instance) {
_services[T] = instance;
}
T get<T>() {
return _services[T] as T;
}
}

Here you can see an example of how you would use this custom built service locator. You first instantiate an instance, then register the singleton, and use it wherever needed.

// Example usage:
final locator = ServiceLocator.instance;
class MyService {
void doSomething() => print('Doing something!');
}
void main() {
locator.registerSingleton(MyService());
final myService = locator.get<MyService>();
myService.doSomething(); // Output: Doing something!
}

Now going back to the implementation code again, this is only 14 lines of code. Granted, some functionality you might want is missing, but if that is the only thing you need, does this warrant a third party dependency? We don’t think so.

Let’s take another example, the Dio package.

Screenshot 2025-03-27 at 08.10.38

The Dio package is for making HTTP requests easier. It comes with lots of pre-baked functionality such as interceptors, DioException, and timeout handling.

Screenshot 2025-03-27 at 08.11.19

Screenshot 2025-03-27 at 08.11.46

That’s great, but what if there is a large rework or a full deprecation of the package? This is not an unlikely hypothetical. Two years ago this actually happened. Dio was deprecated. The community freaked out about how there was no longer a maintainer and the package was considered “dead”.

Screenshot 2025-03-27 at 08.12.59

Luckily, the Chinese team “CFUG” forked the Dio package and continued the maintenance.

Screenshot 2025-03-27 at 08.13.32

Screenshot 2025-03-27 at 08.14.12

I can’t imagine the amount of stress people had to go through given that Dio was tightly coupled throughout their entire codebase. Still, they got lucky. Dio was a large package, and luckily many companies depended on it, and it got resurrected.

Screenshot 2025-03-27 at 08.15.14

Screenshot 2025-03-27 at 08.15.23

Screenshot 2025-03-27 at 08.15.40

Screenshot 2025-03-27 at 08.16.02

Screenshot 2025-03-27 at 08.16.50

Screenshot 2025-03-27 at 08.16.57

But what if it didn’t? What if your codebase is filled with less popular packages? Or popular packages, but nobody is willing to keep working on it.

This is not a scenario you want to experience. You would have to do a large rewrite of your application, potentially spending months, just to get back to a working app. And all of it could have been avoided by spending a little time at the beginning owning the core implementation of your application.

Otherwise you might spend months

Still, some dependencies will be needed or even required in Flutter. There are quite a few packages maintained by the Flutter and Dart teams that are used for core app logic. For example, the http package is required for making HTTP requests or the path package for manipulating the file system. These, as well as 27 more packages, are being maintained directly by the Dart team.

Xnapper-2025-03-25-13.39.02

I’ll be honest, implementing logic myself to handle HTTP requests or file system management is just not worth the tradeoff. And that is the key point of this discussion. That’s not to say you shouldn’t use packages at all, but oftentimes we don’t think about the tradeoff that is being made whenever we add a package to our projects. Many developers have an idea, see there is a package for it, then import it into their project without thinking about it.

packages and dependencies are fine, but think twice

Before adding any package, think about the following:

  • What would happen if this package got deprecated?
  • How intertwined will this package be within my code?
  • Is it easily replaceable?
  • How much time does this package save?

All these options should be weighed depending on what stage you are in your app development journey. Because there is also the opposite problem.

If you are a beginner, and you just want to get an app out to your friends, then you probably would just use a lot of packages. Or you’re a startup and you never get your product into users’ hands because you’re reimplementing solutions that already exist.

Sometimes it is just fine

Here’s our framework for using packages. We start out using all the packages that solve the time-consuming need, because the main objective is to get a finished product in the hands of the users. Once it’s in the user’s hands, we collect feedback to see if this product is worth pursuing further and if it’s something that can generate revenue.

many to one_1

Once we have gotten to a point where we know the product has value, and people want what we are building, only then do we start going back and removing packages to make our codebase more robust. This point in the product development process is called product-market fit. From this point on, every package will be thoroughly thought through to determine whether the tradeoffs are worth it.

many to one

If it has a long history of support and a lot of big corporations using it, we are likely to use it as well. If it’s a small package that isn’t widely used and doesn’t have consistent updates, we will only use it if it’s very contained and easy to swap out. But the ideal case is that we don’t use those packages at all, and minimize the total dependencies as much as it makes sense.

Not always, think about your case

The most popular packages are state management packages. We don’t believe in adding these into our applications because of how engrained state management is throughout the entire application.

MVVM Architecture

We use MVVM

This would be a nightmare to replace if it was deprecated. Instead, we use the MVVM architecture with ValueNotifiers, and you can learn our exact structure in this video.

You might also find this next article on Learning our MVVM architecture for Flutter interesting.