What is the Flutter pubspec.lock file?
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 pubspec.lock
file?
Flexible dependencies version
As seen in the official Dart Documentation, there are a couple of different ways of defining a package’s version:
any
or 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:
^version
means /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.12.3
and0.0.2
-> the caret symbol will find the dependency that is contained in the same Minor version. So^0.12.3
will 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.3
will satisfy all versions:>=1.2.3 <2.0.0
In theory (because in practice some libraries can break this rule), we won’t have breaking changes from 0.12.3
to 0.12.6
or from 1.2.3
to 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 pubspec.lock
file.
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 pubspec.lock
file:
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,
version_banner
- Following that, we have the
dependency
type, this tells us if it’s adirect
dependency, i.e., added by us via thepubspec.yaml
file, or atransitive
dependency (as seen in thepackage_info
dependency in the lock file), which is a dependency of a dependency - The
description
will have some more information relevant to the type of dependency (git, local, or hosted) source
tells us how the dependency was added to the project. In this example, we added it via pub.dev (hosted), but we can also have thesource
aspath
orgit
;- And finally, the
version
will tell us what is the concrete version we are using.
If we look closer into the version
, we see that although we defined in pubspec.yaml
version ^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! http: 0.13.3
.
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 http: 0.13.3
?
➜ 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, 0.13.3
:
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
The ^
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.
Conclusion
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 ⛓️
Want to get the latest articles and news? Subscribe to the newsletter here 👇
And for other articles, check the rest of the blog! Blog - Gonçalo Palma