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?
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.
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.
The Dio package is for making HTTP requests easier. It comes with lots of pre-baked functionality such as interceptors, DioException
, and timeout handling.
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”.
Luckily, the Chinese team “CFUG” forked the Dio package and continued the maintenance.
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.
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.
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.
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.
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.
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.