Gonçalo Palma
February 8, 2021

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 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:

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:

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:

  1. When does Dart update our dependency version?
  2. How does it choose the proper version for the project?
  3. 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"

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 ⛓️

Follow me!

I often share some small insights on Flutter 💙