What’s the pubspec.lock file in a Flutter Project?
Each time that we run a new Flutter project and run it, we will see one message:
Running "flutter pub get" in a_flutter_project... 1,206ms
The question is: what happens when we are getting new dependencies?
As we’ve seen in a previous article - Deep Dive Into The Pubspec.yaml File - we have different ways of adding a dependency to a project, but one of the things we haven’t discussed yet is why we sometimes add a caret
^ symbol before our dependency’s version:
dependencies: # ... flutter_bloc: ^7.0.1
What does this symbol do? And what’s its relationship with the
Flexible dependencies version
As seen in the official Dart Documentation, there are a couple of different ways of defining a package’s version:
anyor empty - this will result in the package version being defined either as the latest or figuring out which version should be used relatively to the other packages constraints
>=- allows us to pick versions that are lower, higher, lower and equal or higher, and equal to the stipulated version
But, if we want to have the flexibility and some guarantee of stability, we should consider using the caret
^ symbol. As defined in the documentation:
^versionmeans /the range of all versions guaranteed to be backward compatible with the specified version/.
So, following the semantic version guidelines (in which
1.2.3 is Major 1, Minor 2, and Patch 3), it means that:
- For dependencies that don’t have a major version such as
0.0.2-> the caret symbol will find the dependency that is contained in the same Minor version. So
^0.12.3will satisfy all versions
>= 0.12.3 < 0.13.0.
- For dependencies that have a major version, such as
1.2.3, the caret symbol will find the dependency that is contained in the same Major version. So
^1.2.3will satisfy all versions:
In theory (because in practice some libraries can break this rule), we won’t have breaking changes from
0.12.6 or from
1.6.90, and that is why Dart takes the liberty to upgrade these versions for us.
Now, this begs two different questions:
- When does Dart update our dependency version?
- How does it choose the proper version for the project?
- Where do I find the specific version that’s being used with the app?
To answer these questions, we must understand what is the
The anatomy of a pubspec.lock file
Instead of trying to look at the file as a whole, let’s go about it another way - we create a new project, commit it, and then we add a new dependency:
dependencies: #... version_banner: ^0.2.0
After that, we run
flutter pub get and look at the diff contents of the
diff --git a/pubspec.lock b/pubspec.lock index 640db00..7d62228 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -81,6 +81,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "1.3.0" + package_info: + dependency: transitive + description: + name: package_info + url: "https://pub.dartlang.org" + source: hosted + version: "0.4.3+4" path: dependency: transitive description: @@ -149,5 +156,13 @@ packages: url: "https://pub.dartlang.org" source: hosted version: "2.1.0" + version_banner: + dependency: "direct main" + description: + name: version_banner + url: "https://pub.dartlang.org" + source: hosted + version: "0.2.1" sdks: dart: ">=2.12.0 <3.0.0" + flutter: ">=1.12.13+hotfix.5"
Let’s analyse it bit by bit:
version_banner: dependency: "direct main" description: name: version_banner url: "https://pub.dartlang.org" source: hosted version: "0.2.1"
- At the top, we have the library name definition, in this case,
- Following that, we have the
dependencytype, this tells us if it’s a
directdependency, i.e., added by us via the
pubspec.yamlfile, or a
transitivedependency (as seen in the
package_infodependency in the lock file), which is a dependency of a dependency
descriptionwill have some more information relevant to the type of dependency (git, local, or hosted)
sourcetells us how the dependency was added to the project. In this example, we added it via pub.dev (hosted), but we can also have the
- And finally, the
versionwill tell us what is the concrete version we are using.
If we look closer into the
version, we see that although we defined in
^0.2.0, the resolved version from the lock file is
0.2.1, which means that Dart was able to accept and use the latest version of the library, respecting it’s minor version.
Why we should use
If we are using only one dependency, the benefit of using
^ is only for us to have the latest and greatest version. However, there is another good reason for using
^ in our dependencies - it allows Dart to be flexible enough to choose a dependency version that aligns with all the transient (or dependencies of a dependency) dependencies.
Imagine the following scenario - you are using the
http package in your application with the latest version -
http: 0.13.0. However, you are also using another library, called library_b that also relies on
http, but It uses a newer version!
name: app_a # ... dependencies: library_b: path: ../library_b http: 0.13.0
name: library_b version: 0.0.1 # ... dependencies: http: 0.13.3
So what would happen if we chose to use the exact version
➜ app_a flutter pub get Running "flutter pub get" in app_a... Because app_a depends on library_b from path which depends on http 0.13.3, http 0.13.3 is required. Running "flutter pub get" in app_a... So, because app_a depends on http 0.13.0, version solving failed. Running "flutter pub get" in app_a... pub get failed (1; So, because app_a depends on http 0.13.0, version solving failed.)
Since we are using a hard dependency version, Dart will not be able to find a version that satisfies both constraints. But what if we add
^ to the app’s dependency? In theory that would mean that the app is saying “I can have a flexible version of http, so find one that satisfies both and causes no conflicts”.
name: app_a # ... dependencies: library_b: path: ../library_b http: ^0.13.3
Just like that, we run the
flutter pub get command:
➜ app_a flutter pub get Running "flutter pub get" in app_a... 1,282ms
Looking at the
pubspec.lock file of the app, we see that it now uses the updated version,
http: dependency: "direct main" description: name: http url: "https://pub.dartlang.org" source: hosted version: "0.13.3"
However, what would happen if the versions were different? Let’s look into the case where the
http version for the app is higher than the one on the library:
name: app_a # ... dependencies: library_b: path: ../library_b http: ^0.13.3
name: library_b version: 0.0.1 # ... dependencies: http: 0.13.0
^ symbol only finds a version that is equal to or above the version we’re declaring, which means that if we run
flutter pub get we will be presented with the following error message:
➜ app_a flutter pub get Running "flutter pub get" in app_a... Because app_a depends on library_b from path which depends on http 0.13.0, http 0.13.0 is required. Running "flutter pub get" in app_a... So, because app_a depends on http ^0.13.3, version solving failed. Running "flutter pub get" in app_a... pub get failed (1; So, because app_a depends on http ^0.13.3, version solving failed.)
The only way to fix this would be to have both the library and the app relying on a
^ version, allowing them to find a version that satisfies both.
If we don’t have the possibility to add the
^ notation in a dependency we are using, we can always use a
dependency_overrides in our
pubspec.yaml file, as we have discussed in Deep Dive Into The Pubspec.yaml File.
Upgrading our versions
So if by running
flutter pub get we don’t see any update on our packages via the
pubspec.lock file, how can we then update our dependencies?
The answer is to use the
flutter pub upgrade!
Let’s have as an example the following app dependencies, and let’s assume the
pubspec.lock file uses the same versions:
name: app_a # ... dependencies: http: ^0.13.0 flutter_bloc: ^7.0.0
If we run
flutter pub upgrade we see that all of the dependencies are going to have a higher version that adheres to the standards of
➜ app_a git:(master) flutter pub upgrade Resolving dependencies... bloc 7.0.0 characters 1.1.0 collection 1.15.0 ffi 1.1.2 file 6.1.2 flutter 0.0.0 from sdk flutter > flutter_bloc 7.1.0 (was 7.0.0) > shared_preferences 2.0.6 (was 2.0.0) ... Changed 2 dependencies! 1 package has newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
By looking at the diff, we can see that these versions got updated to the newer:
@@ -47,7 +47,7 @@ packages: name: flutter_bloc url: "https://pub.dartlang.org" source: hosted - version: "7.0.0" + version: "7.1.0" flutter_web_plugins: dependency: transitive description: flutter @@ -136,7 +136,7 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.6" shared_preferences_linux: dependency: transitive description:
But what if we don’t want to upgrade all the dependencies? What if we just want to test out the latest version of a specific package? Thankfully the
upgrade command allows us to specify one dependency, using
flutter pub upgrade <package_name>.
Let’s test it by using
flutter pub upgrade shared_preferences:
➜ app_a git:(master) flutter pub upgrade shared_preferences Resolving dependencies... bloc 7.0.0 characters 1.1.0 collection 1.15.0 ffi 1.1.2 file 6.1.2 flutter 0.0.0 from sdk flutter flutter_bloc 7.0.0 (7.1.0 available) > shared_preferences 2.0.6 (was 2.0.0) ... Changed 1 dependency! 2 packages have newer versions incompatible with dependency constraints. Try `flutter pub outdated` for more information.
As we can see, the command line tells us that although there is a newer version available for
flutter_bloc, it was not applied. We can verify this by looking at the diff:
@@ -136,7 +136,7 @@ packages: name: shared_preferences url: "https://pub.dartlang.org" source: hosted - version: "2.0.0" + version: "2.0.6" shared_preferences_linux: dependency: transitive description:
Looking at the last line from the
flutter pub upgrade command, we see that it mentions the
flutter pub outdated command. This command can be used to check all the latest and compatible versions of our packages, if we wish to update our
pubspec.yaml file directly:
➜ app_a git:(master) ✗ flutter pub outdated Showing outdated packages. [*] indicates versions that are not the latest available. Package Name Current Upgradable Resolvable Latest direct dependencies: flutter_bloc *7.0.0 7.1.0 7.1.0 7.1.0 shared_preferences *2.0.0 2.0.6 2.0.6 2.0.6 transitive dependencies: meta *1.3.0 *1.3.0 *1.3.0 1.7.0 2 upgradable dependencies are locked (in pubspec.lock) to older versions. To update these dependencies, use `flutter pub upgrade`.
The importance of the lock file in applications
Now we may ask: why is it so important to have a lock file?
The answer is simple:
If we are developing an app, the
lock file guarantees that every person that has access to the project will be able to run it using the same versions of the libraries we are using. It is the “source of truth” for the real versions that we are using.
This means that even if we are using the
^ symbol, we won’t have different resolved versions of the same dependency on different machines, which makes it easier to develop the project in parallel with other developers.
However, if we look at What not to commit in the official Dart documentation, we see that the
pubspec.lock of a library should not be committed:
Don’t check the lockfile of a library package into source control, since libraries should support a range of dependency versions. The version constraints of a library package’s immediate dependencies should be as wide as possible while still ensuring that the dependencies will be compatible with the versions that were tested against.
From the pub glossary.
It has been a long hike getting to know the
pubspec.lock file, but we’ve made it to the top 🧗♀️
We can now clearly see its importance - To have an unequivocal way to pinpoint the dependencies of our application.
Furthermore, we saw why we should consider using the
^ symbol when declaring a dependency - it allows us to be more flexible, have fewer dependency errors, and try out newer versions of the libraries that we are using.
However, in this article, we’ve only explored using dependencies from pub.dev. If we look into using our own
git dependencies we will have the bonus of knowing not only the
version but also the commit hash. This information can save us countless hours of debugging by letting us answer one very important question - is the app not working because we’re not using the latest version of every library?
So now you know how to use the power of the
pubspec.lock file - it will be the source of truth for your projects, and a means to share and keep the app locked in the same library versions ⛓️