In this post, I want to look closely at how dart pub handles dependency versions. The end goal is to help you understand how to manage your flutter dependencies versions with some finesse. So, without further ado, let’s get started.
Dart package manager (PUB) doesn’t allow multiple versions of the same dependency. Instead, it uses version constraints to give you flexibility in situations where you have two dependencies you are depending on depend on different versions of the same a dependency.
This means that, instead of having a specific version of a dependency, you will instead specify a range of versions that can work with your application or library. For instance, if two dependencies require the same but different versions of a dependency. Then using version constraints increases the chances that the specified version ranges will overlap. The latest version where both two packages version requirement overlap is used.
For instance, Let’s say you have two dependencies. Where the first dependency depends on
package-x with version range of
1.3 to 1.8. While the second dependency depended on the same
package-x but between version
1.6 to 2.0. In this case, dart package manager would install version
1.7.x, where x is the latest patch version. This is because it’s the latest version that both dependencies depend on.
This works well in a perfect world. But the world we live in is anything but perfect. There are some cases where you might find that your dependencies version ranges don’t overlap. In such cases and other similar cases, you might have to choose one dependency over the other.
This is just an overview of how dart pub package works, you find detailed documentation here.
In order to use version constraints, you need to know which future versions of your dependencies won’t break your app or library. This is where Semantic versioning come in. Semantic versions provide guidelines on how dependencies are upgraded. With Semantic versioning, a dependency version is in the form of X.Y.Z where X is the major version, Y is the minor version and Z is the patch version. For more information, check the full documentation on semantic versions here.
With Semantic versioning, when a backward compatible feature is added to the public API, then the minor version is incremented. If a non-backward compatible feature is added to the public API, then the major version is incremented. And the patch version is incremented only when bug fixes which are backward compatible are introduced.
With that in mind, you can specify the version constraints of your dependencies for your library and application full knowing that a minor or parch version won’t break anything. You can learn more about Semantic versions here.
NB: Pub uses version 2.0.0-rc.1 of semantic versioning which allow packages to use build identifies to differentiate different versions. This is why you can come across dependency versions looking like this
NB: When the major version is at zero – i.e.
0.1.5– dart convention defers slightly from semantic versioning. Instead, the X.Y.Z interpretation above is shifted down a slot. This makes Y behave like the major version, and Z like the minor version.
In order to indicate the patch version, then a build identifier is used i.e.
0.Y.Z+1. This behaves the same as the patch version.
This is because, prior to version 1.0.0, semantic versioning does not promise compatibility between versions. Dart on the other hand offers promise of version compatibility between different versions.
Pub supports two types of dependencies – regular and dev. Dev dependencies are those dependencies that are not part of your application or library but rather used for development workflow. For instance, test dependencies –
tests – for running tests or generators dependency for generating PODOs for your application. Regular dependency are dependencies that are used inside your application and library – i.e. they are kind of important for your application to run.
You add your dependencies in the
pubspec file – located at the root of your application. This is automatically generated when creating a new flutter application. Regular dependencies are added under
dependencies field while dev dependencies are listed under
dev_dependencies field. You can learn more about fields of a
pubspec file here.
To add a dependency in dart, you simply specify the name of the dependency and the version you want to use.
dependencies: flutter: sdk: flutter font_awesome_flutter: 8.4.0 bloc: 0.14.4 flutter_bloc: 0.18.3
For an application with few dependencies, you can specify an exact version and manually update as new versions get released. On the other hand, you might want to use version constraints to get the latest package without manually updating your
pubspec file. Because dart uses semantic versioning, it is easy to predict up to which future version of a dependency you can use inside your application without breaking anything.
In case you are developing a dart/flutter library, version constraints are highly recommended. As we discussed above, it gives your library high chance of compatibility with other libraries that an app developer is using.
To specify version constraints, you use the
greater than (>),
less than (<),
greater than or equal to (>=) and
less than or equal to (<=) symbols as shown below:
>=1.0.0– any version that is greater or equal to
>1.0.0– any version greater than
<=1.0.0– any version less than or equal to 1.0.0 i.e.
bloc: <=1.0.0– This sets the upper limit of the version to install as version 1 and any version lower than that.
<1.0.0– any version less than 1.0.0 i.e.
bloc: <1.0.0– This basically does the same as the above except it excludes version 1.0.0.
any– any version of that dependency – not recommended for performance reasons. The same as leaving the version section empty, but more explicit.
On top of that, you can also combine the above versions constraints to provide an upper and lower limit for a dependency. For instance:
bloc: '>=1.0.0 < 2.0.0' which limits the versions for your bloc package to between 1 and less than two. This is recommended especially for libraries.
You may have come across this, where a dependency version is specified with a caret before it –
^version. This provides a simple way of expressing common version constraints discussed above. When using the caret syntax, you are setting the version constraints to be greater or equal to the listed version, but less than the next major version. You simply don’t want a version with breaking changes being installed. For instance,
^1.2.3 is the equivalent of
>=1.2.3 <2.0.0 while
^0.1.2 is equivalent of
For libraries, it is recommended not to commit the dependency
lockfile. For applications on the other hand, you should commit the dependency
lockfile. The reason for committing the
lockfile for applications is to ensure that all developers are working on exact the same version of dependencies. The
lockfile is only updated when you upgrade dependencies (
flutter pub upgrade) but not when you get packages (
flutter pub get). You can learn more here about what to commit and not to commit.
Libraries are the biggest beneficiaries for using version constraints. If you maintain a flutter/dart library, you should switch to version constraints if you haven’t already. For application, I also think using version constraints is beneficial.
For instance, you might want to have minor and patch versions of a dependency installed when available, without having to manually track them yourself. This allows you to run the
flutter pub upgrade and it will update the dependency versions in the
lockfile within version constraints on the
NB: For flutter applications, you should not use
pub upgradecommands to manage yours apps dependencies. Instead, you should use
flutter pub getor
flutter pub upgrade.