“Across the pond” in the Node.js world, project dependencies are typically managed by a tool called NPM. Over the past few months (and years), we have seen quite a few security breaches: Sync found over 200 malicious NPM packages, some developer intentionally corrupted their own libraries, and the appearance of Protestware-as-a-Service.
However, it’s not just security at stake, but also maintainability. The definition of dependency is a codebase someone else has written. I am not trying to make you paranoid, but there are good reasons third-party code might be potentially dangerous. Aside from the security aspects, you should also consider the following:
What if a library is no longer maintained? a) Outdated version of Dart: is it null safe, and does it use the latest language features? b) Abiding by obsolete best practices potentially costs performance or safety c) Unpatched issues that might lead to implicit bugs
Version-resolving conflicts or potential need for dependency overrides
Breaking changes in the future may demand immediate refactoring, the scope of which is impossible to predict
Potential performance issues and increased app size (remember, tree shaking isn’t infinitely good)
I would argue that the job of a software developer is to make sure every segment of our codebase is held up to the highest possible standard. As a thought experiment, let us imagine a Flutter app without any third-party dependencies.
Quick overview of typical dependencies Flutter developers use
Every flutter developer has their own opinion on stage management. And because of that, everyone likes a different solution, from BLoC to Riverpod, to GetX, etc. And because the signing for mobile means designing a lot of screens, we have to have navigation. Popular solutions include go_router, auto_route, qlevar_router, and the list goes on.
Hopefully, we try to bring our applications to as many users as possible, so we need to take care of internationalization. There’s intl.
No app would be perfect without being a glorified web app, so we need to contact some API. Two of the most popular HTTP clients are http and dio. We should also consider persisting data locally in a relational (SQLite) or non-relational database (Hive or Isar). Simple key-value pairs? Or maybe we are a start-up that wants to use Firebase?
Not to forget UI-related libraries such as fonts and icon packs, audio/video players, and web views. How about libraries that help improve static code analysis? Or something that brings functionalities from an underlying platform, think camera, file picker, location, content sharing, URL launcher…
So it's not surprising that we end up with pubspec.yaml including 20+ dependencies.
Can these dependencies be replaced?
As a developer, you should have a deep understanding of why a dependency is needed, how it works, and how it can be replaced.
Let’s look at how:
- State management: InheritedWidget, setState
- Routing: built-in Navigator
- Internationalization: provide and parse ARB files on your own.
- Networking: built-in HttpClient (or HttpRequest for web apps)
- Local persistence: custom file-based database
- Firebase: you can use their REST APIs instead of relying on libraries
- Platform-specific libraries: you can write your own native code and expose it via platform channels
Important question: Is it worth it?
Every dependency is replaceable. It is up to the developer to weigh the pros and cons of replacing it with a custom solution.
Let’s look at the positive aspects:
Complete control over the source code a. Codebase should be kept up-to-date with language and standard library evolution b. Decreased dependency footprint
Following KISS: prefer a custom solution for a particular use case over an abstracted “one size fits all” approach
However, we need to mention the downsides of custom solutions:
- Increased development time and consequently the technical debt
- The problem of reinventing the wheel: is your wheel “rounder” than the one already developed?
Here you have to pick a side: are you on the side of lowering the cost of development and potentially risking headaches down the line if being overly reliant on a dependency breaks something, or are you on the side of trusting yourself enough to justify extra time by delivering a higher-quality codebase?
Is it all doom and gloom?
No! Thus far we have been focusing on third-party dependencies, but there are packages that you can (in most cases) safely reach for and trust. I’m talking about official packages developed by the Flutter/Dart team or the officially-endorsed community ones.
You may have a certain hesitance from using the codebase developed by engineers from these big tech companies. But for the most part, these are projects developed by some of the brightest minds in our industry, who are professionally invested in developing Flutter/Dart anyways. Therefore, being as close as possible to the development of the actual programming language or the UI toolkit gives them an insight an outsider perhaps does not have.
On the other hand, there are also vetted and trusted developers out there who might have the status of an “outsider”, yet their expertise justifies trustworthiness.
Ask yourself, how many dependencies do you actually need?
State management a. Small-scale projects probably don’t need a separate solution b. Medium-to-large scale projects should consider using a recommended solution, and perhaps couple it with other architectural tools (service locators, dependency injection) c. Word of advice: stay away from “one solution for everything” packages, because they introduce too many dangerous codependencies
Internationalization: stick to official solutions and do not trust the “easy” solutions
Networking: for the most part, built-in HTTP clients are more than enough a. Prefer manual JSON serialization/deserialization when/if possible, as it greatly decreases the amount of autogenerated code b. For GraphQL support be very careful with tools autogenerating HTTP calls and types based on schema, it’s better to stick to a simple HTTP POST request and manual deserialization
Local persistence a. You may get away with simple static fields for extremely trivial use cases (although some might argue against that) b. For simple key-value storage consider SharedPreferences c. If a database is needed, consider SQLite or Hive/Isar
Firebase: use official plugins, but don’t be afraid to venture out and reverse-engineer them. Perhaps it saves you a few MBs of app size
Utilities and convenience libraries: a. Combination of packages such as “meta” and “equatable”, alongside properly set linting rules and stricter type checks than the Dart type engine requires should be in your bucket list b. For logging, use a logging solution that enables turning logs off when application is being launched into production
Platform-specific libraries a. If possible, make sure they are developed by the Flutter/Dart team b. Some of them rely on deprecated native APIs, so consider replacing them with custom solutions
UI-related libraries: you probably do not need them. Copy the codebase you need and customize it to your liking a. A minor exception to the rule might be Google Fonts library: you can use it during development and drop it before launching the app to production
As a final thought: do not limit yourself to the premade solutions. However, at the same time, do not be discouraged from sharing your solutions with the world, because it may save someone’s time.