18 Dependency Management Ii How to Manage Third Party Component Libraries in Flutter

18 Dependency Management (Part 2): How to Manage Third-Party Component Libraries in Flutter? #

Hello, I’m Chen Hang.

In the previous article, I introduced the resource management mechanism of Flutter projects. In Flutter, resources are managed using a declaration-first-usage mechanism, which means you need to declare the resource path in pubspec.yaml before you can use it.

For images, Flutter separates them into different resolution directories based on pixel density, but you only need to declare them once in pubspec.yaml. As for fonts, Flutter supports various styles such as regular, bold, and italic based on style support. Finally, since Flutter requires a native runtime environment, special resources like launch images and app icons need to be set separately in the native project before Flutter is launched.

In addition to managing these resources, pubspec.yaml plays a more important role in managing the dependencies of Flutter project code. It can be used to manage third-party libraries, Dart runtime environment, and Flutter SDK version. So, pubspec.yaml is similar in functionality to Podfile in iOS, build.gradle in Android, and package.json in frontend development.

Therefore, in today’s article, I will mainly share with you how to manage project code dependencies in Flutter through the configuration file.

18 Dependency Management II How to Manage Third-Party Component Libraries in Flutter #

Dart provides the package management tool Pub for managing code and resources. Essentially, a package is a directory that includes the pubspec.yaml file, which can contain files such as code, resources, scripts, tests, and documentation. Packages include functionality abstractions that can be depended on externally and can also depend on other packages.

Similar to JCenter/Maven in Android, CocoaPods in iOS, and npm libraries in frontend development, Dart provides the official package repository Pub. Through Pub, we can easily find useful third-party packages.

However, this doesn’t mean that we can simply piece together other people’s libraries to create an application. The true purpose of Dart’s package management tool Pub is to allow you to find truly useful libraries that have been validated extensively online, reuse others’ contributions to shorten development cycles, and improve software quality.

In Dart, both libraries and applications are considered packages. The pubspec.yaml file is the package configuration file, which contains metadata about the package (such as name and version), runtime environment (Dart SDK and Flutter SDK versions), external dependencies, and internal configurations (such as resource management).

In the example below, we declare a configuration file for a flutter_app_example application. Its version is 1.0, and it supports Dart runtime environments from 2.1 to 3.0, with dependencies on flutter and cupertino_icons:

name: flutter_app_example # Application name
description: A new Flutter application. # Application description
version: 1.0.0
# Dart runtime environment range
environment:
  sdk: ">=2.1.0 <3.0.0"
# Flutter dependencies
dependencies:
  flutter:
    sdk: flutter
  cupertino_icons: ">0.1.1"

The part after the colon in the runtime environment and the cupertino_icons dependencies contains version constraint information, which consists of a set of space-separated version descriptions. It supports three version constraint methods: specifying a specific version, specifying a version range, or specifying any version. For example, in the example above, cupertino_icons references a version greater than 0.1.1.

It is important to note that since metadata and names are separated by spaces, spaces cannot appear in version numbers. Additionally, since the greater than symbol “>” is also a folding newline symbol in YAML syntax, quotes must be used when specifying version ranges. For example, “>=2.1.0 < 3.0.0”.

For packages, it is usually recommended to specify version ranges rather than specific versions, as package updates occur frequently. If other packages directly or indirectly depend on different versions of this package, conflicts will occur frequently.

For the runtime environment, it is suggested to fix the Dart and Flutter SDK environments if working in a team. This helps maintain a unified development environment to avoid issues caused by API differences resulting from mismatched SDK versions.

For example, in the above example, we can fix the Dart SDK version to 2.3.0 and the Flutter SDK version to 1.2.1:

environment:
  sdk: 2.3.0
  flutter: 1.2.1

When referencing third-party packages based on version, they need to be publicly published on Pub. We can visit https://pub.dev/ to find available third-party packages. For packages that are not publicly published or currently in a development/debugging phase, we need to set the data source and declare them using local paths or Git addresses.

In the example below, we declare two packages, package1 and package2, using path and Git dependencies, respectively:

dependencies:
  package1:
    path: ../package1/  # Path dependency
  package2:
    git:
      url: https://github.com/xxx/package2.git # Git dependency

During application development, we can declare package dependencies using version ranges instead of specific version numbers. However, once an application is built, the specific versions of dependencies used at runtime must be determined. Therefore, in addition to managing third-party dependencies, another responsibility of the package management tool Pub is to find a set of packages that satisfy the version constraints of each package. Once the package versions are determined, the corresponding package versions can be downloaded.

For different data sources in the dependencies, Dart manages them differently, and ultimately, all remote packages will be downloaded to the local system. For example, when declaring Git dependencies, Pub will clone the Git repository, and for version numbers, Pub will download the packages from pub.dartlang.org. If packages have other dependencies, such as package1 depending on package3, Pub will download all of them together.

After downloading all the required packages, Pub will create a .packages file in the root directory of the application, which maps the names of the dependencies to the paths of the package files in the system cache for maintenance purposes.

Finally, Pub will automatically create a pubspec.lock file. The purpose of the pubspec.lock file is similar to Podfile.lock in iOS or package-lock.json in frontend development, which records the specific sources and versions of the directly and indirectly installed dependencies at the current state.

Since frequently updated third-party packages can result in conflicts, it is recommended to include the pubspec.lock file in the version management of the code when working on a collaborative Flutter application. This ensures that everyone in the team installs the exact same dependencies when using the application, thus avoiding issues such as missing library functions or other dependency errors.

In addition to providing dependency management for functionality and code, packages can also provide resource dependencies. When the same resources are already declared in the pubspec.yaml file of a dependency package, we should reuse those resources to save space in the installed application package.

In the example below, our application depends on a package called package4, which has the following directory structure:

pubspec.yaml    
└──assets
    ├──2.0x
    │   └── placeholder.png
    └──3.0x
        └── placeholder.png

Here, placeholder.png is a reusable resource. Therefore, in the application, we can use the package parameter provided by Image and AssetImage to load the image based on the actual device resolution.

Image.asset('assets/placeholder.png', package: 'package4');

AssetImage('assets/placeholder.png', package: 'package4');

Example

Example #

Next, let’s demonstrate how to use a third-party library with an example of date formatting.

In Flutter, there is a data structure called DateTime that can express a wide range of dates. It can represent any moment within 100,000,000 days after January 1, 1970, UTC. However, if we want to format and display dates and times, DateTime does not provide a very convenient method. We have to extract the year, month, day, hour, minute, and second ourselves in order to customize the display.

Fortunately, we can achieve our goal using the third-party package called date_format: it provides several commonly used date formatting methods that make formatting dates very convenient.

First, let’s find the date_format package on Pub and check its usage instructions:

Figure 1: Usage instructions of date_format

The latest version of the date_format package is 1.0.6, so next, let’s add date_format to pubspec.yaml:

dependencies:
  date_format: 1.0.6

Then, the IDE (Android Studio) detects changes in the configuration file and prompts us to update package dependencies. So, we click “Get dependencies” to download date_format:

Figure 2: Downloading package dependencies

After the download is complete, we can use date_format in our project to format dates:

print(formatDate(DateTime.now(), [mm, '月', dd, '日', hh, ':', n]));
//Output: 2019年06月30日01:56
print(formatDate(DateTime.now(), [m, '月第', w, '周']));
//Output: 6月第5周

Summary #

Well, that’s all for today’s sharing. Let’s briefly review the content.

In Flutter, resources and project code dependencies belong to the scope of package management, and they are managed collectively through the package configuration file pubspec.yaml.

We can use pubspec.yaml to set the package metadata (such as the package name and version), runtime environment (such as Dart SDK and Flutter SDK versions), external dependencies, and internal configuration.

For specifying dependencies, we can determine the compatibility range of version using interval notation, or specify them from local paths, Git, or Pub. The package management tool will find versions of packages that satisfy all the dependency constraints, then download them one by one. It establishes a mapping between the download cache and the package names through the .packages file, and finally records the specific sources and version numbers of each installed package under the current state in the pubspec.lock file.

Most modern programming languages come with their own dependency management mechanisms. The core function is to find suitable versions of code libraries that are directly or indirectly dependent in the project. However, this is not easy. For example, earlier versions of the dependency manager npm for frontend development had long computation times and rapidly expanding dependency folders due to the use of inefficient algorithms. It was jokingly referred to as a “black hole” by developers. However, the PubGrub algorithm used in Dart’s Pub dependency management mechanism solves these problems and is considered as the next-generation version dependency resolution algorithm. It was acquired by Apple in late 2018 and became the dependency management algorithm used in Swift.

Of course, if your project has many dependencies and complex dependency relationships, even the most advanced dependency resolution algorithm will take a long time to calculate the appropriate library versions. If we want to reduce the time spent by the dependency management tool in finding code library dependencies, a simple approach is to fix the versions of those third-party libraries with complex dependencies and the versions of their recursively dependent third-party libraries in the pubspec.yaml file.

Thought Questions #

Finally, I have two thought questions for you.

  1. What are the specific roles of pubspec.yaml, .packages, and pubspec.lock in package management?
  2. Should .packages and pubspec.lock be version controlled in code? Why or why not?

Feel free to leave your thoughts in the comments section. I will be waiting for your response in the next article! Thank you for listening and feel free to share this article with more friends to read together.