DEV Community

Cover image for  How to Create Dart Packages for Your Flutter Apps
Andrea Bizzotto
Andrea Bizzotto

Posted on • Edited on • Originally published at codewithandrea.com

How to Create Dart Packages for Your Flutter Apps

This article was originally published on my website.

Watch the Video Tutorial on YouTube.

In this tutorial I'll show you how to create Dart packages for your Flutter apps, so that you can improve and reuse your code.

Why is this important?

With large applications, it is challenging to keep folders organized, and minimise inter-dependencies between files and different parts of the app.

Dart packages solve this problem by making apps more modular and dependencies more explicit.

So if you have a single large application, or multiple apps that need to share some functionality, extracting reusable code into packages is the way forward.

How this tutorial is organized

We will start with a step-by-step guide and convert a sample BMI calculator application to use internal packages within the same project.

Then, we will talk about:

  • Dealing with existing, large apps
  • Reusing packages across multiple apps
  • Local vs remove (git) packages
  • Versioning packages and the humble changelog

Let's get started!

Example: BMI calculator

To follow along each step, you can download the starter project here.

Suppose we have a single page BMI calculator app, composed of these four files:

lib/
    bmi_calculation_page.dart
    bmi_calculator.dart
    bmi_formatter.dart
    main.dart
Enter fullscreen mode Exit fullscreen mode

The most interesting functionality is in bmi_calculator.dart and bmi_formatter.dart:

// bmi_calculator.dart
double calculateBMI(double weight, double height) {
  return weight / (height * height);
}
Enter fullscreen mode Exit fullscreen mode
// bmi_formatter.dart
import 'package:intl/intl.dart';

String formattedBMI(double bmi) {
  final formatter = NumberFormat('###.#');
  return formatter.format(bmi);
}
Enter fullscreen mode Exit fullscreen mode

The UI is built with a single BMICalculationPage widget class. This shows two input text fields for the weight and height, and one output text field for the BMI (full source here):

BMI calculator screenshot

This app is simple enough that we can keep all files inside lib. But how can we reuse the BMI calculation and formatting logic across other projects?

We could copy-paste bmi_calculator.dart and bmi_formatter.dart on each new project.

But copy pasting is rarely a good thing. If we want to change the number of decimal places in the formatter code, we have to do it in each project. Not very DRY. 🌡

Creating a new package

A better approach is to create a new package for all the shared code.

In doing this, we should consider the following:

  • By convention, all packages should go inside a packages folder.
  • When starting from a single application, it's simpler to add the new package(s) inside the same git repo.
  • If we need to share packages across multiple projects, we can move them to a new git repo (more on this below).
  • We can keep multiple packages inside a single repository. The FlutterFire monorepo is a good example of this, and I recommend we do the same for simplicity.

For this example, we'll add a new package and keep it inside the same git repo.

From the root of your project, we can run this:

mkdir packages
cd packages
flutter create --template=package bmi
Enter fullscreen mode Exit fullscreen mode

This will create a new Flutter package in packages/bmi, but the main.dart file with the usual runApp(MyApp()) code is missing. Instead, we have a bmi.dart file with some default boilerplate:

library bmi;

/// A Calculator.
class Calculator {
  /// Returns [value] plus 1.
  int addOne(int value) => value + 1;
}
Enter fullscreen mode Exit fullscreen mode

As we don't need the Calculator class, we can replace it with the BMI calculation and formatting code from the main app.

The simplest way to do this is to add all the code from lib/bmi_calculator.dart and lib/bmi_formatter.dart to packages/bmi/lib/bmi.dart (we will see later on how to have multiple files inside a package):

// bmi.dart
library bmi;

import 'package:intl/intl.dart';

double calculateBMI(double weight, double height) {
  return weight / (height * height);
}

String formattedBMI(double bmi) {
  final formatter = NumberFormat('###.#');
  return formatter.format(bmi);
}
Enter fullscreen mode Exit fullscreen mode

Note that this code depends on intl, so we need to add this to the pubspec.yaml file of our package:

dependencies:
  flutter:
    sdk: flutter
  intl: ^0.16.1
Enter fullscreen mode Exit fullscreen mode

Using the new package

Now that we have a bmi package, we need to add it as a dependency to our app's pubspec.yaml file:

dependencies:
  flutter:
    sdk: flutter
  # Used by bmi_calculation_page.dart
  flutter_hooks: ^0.9.0
  # we no longer need to import intl explicitly, as bmi already depends on it
  bmi:
    path: packages/bmi
Enter fullscreen mode Exit fullscreen mode

Here we use a path argument to tell Flutter where to find our new package. This works as long as the package lives in the same repo.


After running flutter pub get (from the root of the project), the package will be installed and we can use it, just like we would do with any other Dart package.

So we can update the imports in our bmi_calculator_page.dart from this:

import 'package:bmi_calculator_app_flutter/bmi_calculator.dart';
import 'package:bmi_calculator_app_flutter/bmi_formatter.dart';
Enter fullscreen mode Exit fullscreen mode

to this:

import 'package:bmi/bmi.dart';
Enter fullscreen mode Exit fullscreen mode

And viola! Our code works and we can now remove bmi_calculator.dart and bmi_formatter.dart from the main app project. 🏁

In summary, to extract existing code to a separate package we have to:

  • create a new package and move our code inside it.
  • add any dependencies to the pubspec.yaml file for the package.
  • add the new package as a dependency to pubspec.yaml for our application.
  • replace the old imports with the new package where needed.
  • delete all the old files.

Bonus: Adding multiple files to a package with part and part of

This example is simple enough that we can keep the BMI calculation and formatting code in bmi.dart.

But as our package grows, we should split the code into multiple files.

So rather than keeping everything in one file like this:

// bmi.dart
library bmi;

import 'package:intl/intl.dart';

double calculateBMI(double weight, double height) {
  return weight / (height * height);
}

String formattedBMI(double bmi) {
  final formatter = NumberFormat('###.#');
  return formatter.format(bmi);
}
Enter fullscreen mode Exit fullscreen mode

We can move the calculateBMI and formattedBMI methods in separate files, just like we had them at the beginning:

// bmi_calculator.dart
part of bmi;

double calculateBMI(double weight, double height) {
  return weight / (height * height);
}
Enter fullscreen mode Exit fullscreen mode
// bmi_formatter.dart
part of bmi;

String formattedBMI(double bmi) {
  final formatter = NumberFormat('###.#');
  return formatter.format(bmi);
}
Enter fullscreen mode Exit fullscreen mode

Then, we can update bmi.dart to specify its parts:

// bmi.dart
library bmi;

import 'package:intl/intl.dart';

part 'bmi_calculator.dart';
part 'bmi_formatter.dart';
Enter fullscreen mode Exit fullscreen mode

A few notes:

  • Files declared with part of should not contain any imports, or we'll get compile errors.
  • Instead, all imports should remain in the main file that specifies all the parts.

In essence, we're saying that bmi_calculator.dart and bmi_formatter.dart are part of bmi.dart.

When we import bmi.dart in the main app, all public symbols defined in all its parts will be visible.

In other words, our main app just needs to import 'package:bmi/bmi.dart';, and have access to all the methods declared in all its parts.

Job done! You can find the finished project for this tutorial here.

Note: Using part and part of works well if you have just a few files in the same folder. One library that uses this extensively is flutter_bloc, where it's common to define state, event and bloc classes together.

For more complex apps it's advisable (and recommended by the Dart team) to export library files instead. See this official guide on creating packages for more details.

Top tip: moving code into packages is a great opportunity to move existing tests, or write new ones.

Creating a package for a simple app is easy enough, but how do we do this when we have complex apps?

Dealing with existing, large apps

For more complex apps, we can incrementally move code into self-contained packages.

This forces us to think harder about the dependencies between packages.

But if there are already a lot of inter-dependencies, how do we get started?

A bottom-up approach is most effective. Consider this example:

// lib/a.dart
import 'b.dart';
import 'c.dart';

// lib/b.dart
import 'c.dart';

// lib/d.dart
import 'a.dart';
import 'c.dart';

// lib/c.dart
// no imports
Enter fullscreen mode Exit fullscreen mode

As we can see, c.dart doesn't depend on any files.

So moving c.dart into a c package would be a good first step.

Then, we could move b.dart into a separate b package, making sure to add c as a dependency:

# packages/b/pubspec.yaml
dependencies:
  flutter:
    sdk: flutter
  c:
    path: packages/c
Enter fullscreen mode Exit fullscreen mode

And we can repeat this process until all dependencies are taken care of.

It's still our job to decide how many packages to create, and what goes in each one.

Reusing packages across multiple apps

Up until now we've seen how to create new packages within the same project.

If we want to reuse packages across multiple projects, we can move them to a common shared repository.

Using the BMI calculator example, we could update the pubspec.yaml file to point to the new repository:

dependencies:
  ...
  bmi:
    git:
      url: https://github.com/your-username/bmi
      path: packages/bmi
Enter fullscreen mode Exit fullscreen mode

I followed this approach when refactoring my starter architecture project, and ended up with 6 new packages that I now reuse in other projects.

To learn more about to the package dependencies syntax, read this page on the Dart documentation.

Local vs remove (git) packages

Dart packages make our code cleaner and increase code reuse, but do they slow us down?

As long as we specify dependencies with path in our pubspec.yaml file, we can edit, commit and push all our code (main app and packages) just as we did before.

But if we move our packages to a separate repo, getting the latest code for our packages becomes more difficult:

dependencies:
  ...
  bmi:
    git:
      url: https://github.com/your-username/bmi
      path: packages/bmi
Enter fullscreen mode Exit fullscreen mode

That's because pub will cache packages when we use git as a source. Running flutter pub get will keep the old cached version, even if we have pushed changes to the package.

As a workaround we can:

  • comment out our package
  • run flutter pub get (to delete it from cache)
  • uncomment the package
  • run flutter pub get again (to get the latest changes)

Repeating these steps every time we make a change is not fun. πŸ€”

Bottom line: we get the fastest turnaround time by keeping all our code (apps and packages) in the same git repo. That way we can specify package dependencies with path and always use the latest code.

Versioning packages

When we import packages from pub.dev, we normally specify which version we want to use.

Each version corresponds to a release, and we can preview the changelog to see what changes across releases (example changelog from Provider).

This makes it a lot easier to see what changed and when, and can help pinpoint bugs/regressions to a specific release.

As you develop your own packages, I encourage you to also have a changelog. And don't forget to update the package version, which lives at the top of the pubspec.yaml file:

name: bmi
description: Utility functions to calculate the BMI.
version: 0.0.1
author:
homepage:
Enter fullscreen mode Exit fullscreen mode

This is a good and useful habit if other devs will use your code, or you plan to publish your package on pub.dev.

Challenge: Refactor your apps

Time to put things in practice with a challenge.

Try extracting some code from one of your Flutter projects, and move it to one of more packages.

And use this as an opportunity to think about dependencies in your own projects.

Conclusion

Dart packages are a good way to scale up projects, whether you work for yourself or as part of a big organization.

They make code more modular and reusable, and with clearly defined dependencies.

They also make it easier to split code ownership with other collaborators.

You should consider using them, whether you're working on a single large app, or many separate apps.

After all, there's a reason we have an entire package ecosystem on pub.dev. 😎

Happy coding!

Top comments (0)